NNTP (#76)(an instance of Generic Utilities Package made by Hacker)     A lot like Dennis the Menace, with a bag of newspapers slung over one shoulder. HELP MANUAL:
     The MOO NNTP agent fetches and posts USENET news articles. It is designed to be
used primarily as a programmer's tool, especially in situations where it is necessary,
from a hypertext document, to refer to a news article.
     The interface consists primarily of two verbs: #33339:fetch_{article,head,body},
which fetch an article, the header of an article, or the body of an article respectively,
and #33339:post which posts an item to a newsgroup (if posting is enabled and the
user is either a wizard or trusted to post news.)
     For help on individual verbs, type "help #33339:fetch_article", or "help #33339:post".
VERB SOURCE CODE: open:
":open()";
"open a network connection to the NNTP server specified by this.host and this.port.";
if (caller != this)
return E_PERM;
endif
connexion = $network:open(this.host, this.port);
if (typeof(connexion) == ERR)
this:log_error("open", connexion);
return connexion;
endif
"the server should always open with a `200 ...' message.";
"this snarfs that message, so's it doesn't end up in an article";
line = this:read_line(connexion);
if ((!line) || (line[1] != "2"))
this:log_error("init", line);
return E_INVARG;
endif
return connexion;
.
article head body:
":article(connection,article)";
":head(connection,article)";
":body(connection,article)";
if (caller != this)
return E_PERM;
endif
"fetch an article, given a connection and an aritcle ID in one of two forms:";
"either `usenet.group.name article-number', or `
check_id: "checks to see if the argument (a string) is a valid article indentifier"; "Article identifiers are one of the following forms:"; " nntp_read:
"read an NNTP body, body being defined as something that ends with a period on a
blank line. expects a connection to read from.";
if (caller != this)
return E_PERM;
endif
server = args[1];
article = {};
count = this.line_limit;
now = time();
while (((typeof(line = this:read_line(server)) != ERR) && ((count = count - 1) >
0)) && ((now + this.timeout) > (now = time())))
if (line && (line[1] == "."))
if (line == ".")
return article;
endif
article = {@article, line[2..length(line)]};
else
article = {@article, line};
endif
endwhile
if (count == 0)
while ((line = this:read_line(server)) != ".")
endwhile
return {@article, "<
init_for_core:
if (caller_perms().wizard)
this.host = "";
this.port = 119;
this:flush_cache();
this.error_log = {};
this.debug = 0;
this.connections = {};
this.in_use = {};
this.connect_max = 0;
this.last_groups_update = 100000000;
this.newsgroups = {};
this.cache_limit = 10;
this.line_limit = 2000;
this.timeout = 30;
this.distributions = {};
this.expiry = 432000;
if (this.expire_task)
$code_utils:task_valid(this.expire_task) && kill_task(this.expire_task);
this.expire_task = 0;
endif
this.expire_delay = 172800;
pass(@args);
endif
.
close:
"quit from the NNTP server and close the passed connection.";
if (caller != this)
return E_PERM;
endif
"Use release_connection instead";
this:send_line(args[1], "QUIT");
$network:close(args[1]);
this.in_use = setremove(this.in_use, args[1]);
.
fetch_article fetch_body fetch_head:
":Fetch_article(article_id)";
":fetch_body(article_id)";
":fetch_head(article_id)";
"This function fetches the article referred to by article-id from an NNTP server.";
"The article-id may be any of the forms accepted by check_id";
"When fetching headers or complete articles, it will often be useful to pass the
header of the article to :fixup_headers(), which will attempt to join multiline headers
into single lines";
if (typeof(connection = this:get_connection()) != ERR)
text = this:(verb[index(verb, "_") + 1..length(verb)])(connection, args[1]);
this:release_connection(connection);
return text;
else
return connection;
endif
.
@update:
"Update the groups list stored on the NNTP object. This list is used in determining
which groups are valid for registration, and should be run regularly.";
"This command may take some time to execute.";
if (!$perm_utils:controls(player, this))
return E_PERM;
endif
connection = this:get_connection();
if (typeof(connection) != ERR)
notify(connection, "NEWGROUPS " + $time_utils:time_sub(this.timeformat, this.last_groups_update));
line = read(connection);
if (line[1] != "2")
this:log_error("update", line);
return E_TYPE;
endif
groups = this:nntp_read(connection);
this:release_connection(connection);
if (typeof(groups) == LIST)
for froup in (groups)
$command_utils:suspend_if_needed(0);
this.newsgroups = {@this.newsgroups, $string_utils:explode(froup)[1]};
endfor
player:tell(("Newsgroup update procedure added " + tostr(length(groups)))
+ " newsgroup(s)");
this.last_groups_update = time();
else
player:tell("An error occurred updating the list of newsgroups.");
this:log_error("update-read", groups);
return groups;
endif
endif
.
fixup_headers:
"#33339:fixup_headers({header-text})";
"This function takes the header of an nntp article (as a list of strings) and joins
multiline headers into single lines. It does no cleanup on the result, so there
may be extra spaces in the output, which is a list of strings.";
"this may break if the first line isn't really a header.";
result = {};
for line in (args[1])
if (index(line, ":") && index($string_utils.alphabet, $string_utils:lowercase(line[1])))
"hokey, but technically it's the most accurate check we're allowed.";
result = {@result, line};
else
result[length(result)] = result[length(result)] + line;
endif
endfor
return result;
.
get_connection:
"$NNTP maintains a list of open, available connections.";
"This verb returns a connection from that list, and removes the connection from the
list of actives, adding it to a list of in-use connections.";
"The maximum number of connections is maintained by this.connect_max.";
"If this.connect_max is 0, there is no limit to the number of connections.";
"If no connections are available, return E_RANGE.";
if (caller != this)
return E_PERM;
endif
if (this.connections)
"A connection is available...";
connect = this.connections[1];
this.connections = setremove(this.connections, connect);
if (!$network:is_connected(connect))
"The connection is defunct. scrap it and get a new one.";
connect = this:open();
endif
else
"No connection is available. If there's space for it (or we're";
"not enforcing limits,) create a new connection and add it to";
"this.in_use.";
if ((!this.connect_max) || (length(this.in_use) < this.connect_max))
connect = this:open();
else
"Whoops. can't get a connection.";
return E_RANGE;
endif
endif
if (typeof(connect) != ERR)
"just in case the connection failed.";
this.in_use = {@this.in_use, connect};
endif
return connect;
.
release_connection:
"#33339:release_connection(connection)";
"Release_connection frees a connection obtained with get_connection().";
"It is assumed that any connection requested will eventually be released.";
if (caller != this)
return E_PERM;
endif
if (args[1] in this.in_use)
this.in_use = setremove(this.in_use, args[1]);
endif
this.connections = {args[1], @this.connections};
.
post:
":post(newsgroup(s), subject, lines, distributions)";
"Post an article to news.";
"Newsgroup(s) should either be a string or a list of strings.";
"Subject should be a string.";
"Lines should be a list of strings.";
"Distributions should either be a string or a list of strings containing valid distributions.";
"returns 0 if successful, otherwise error or error string";
if (!this:posting_ok(caller_perms()))
return E_PERM;
endif
if (typeof(args[1]) == LIST)
newsgroups = $string_utils:from_list(args[1], ", ");
else
newsgroups = args[1];
endif
subject = args[2];
body = {};
for line in (args[3])
body = {@body, (line[1] == ".") ? "." + line | line};
endfor
if (length(args) == 4)
if (typeof(args[4]) == LIST)
distribution = $string_utils:from_list(args[4], ", ");
else
distribution = args[4];
endif
else
distribution = "";
endif
from = $network:return_address_for(player);
reply_to = from;
path = ($network.moo_name + "!") + player.name;
organization = tostr($network.moo_name, ", ", $network.site, " ", $network.port);
date = ctime();
"-----";
"Begin conversations with the server:";
player:tell("Connecting to the NNTP server at ", this.host);
if (typeof(connect = this:get_connection()) == ERR)
player:tell("Failed to connect: ", connect);
return connect;
endif
player:tell("Initiating POST");
this:send_line(connect, "POST");
line = this:read_line(connect);
if (line[1..3] == "440")
this:release_connection(connect);
player:tell("Posting to this site refused: ", line);
return E_PERM;
endif
player:tell("Sending post...");
"send headers";
this:send_line(connect, "Path: " + path);
this:send_line(connect, "From: " + from);
this:send_line(connect, "Reply-to: " + reply_to);
this:send_line(connect, "Newsgroups: " + newsgroups);
player:tell("Newsgroups: " + newsgroups);
if (distribution)
this:send_line(connect, "Distribution: " + distribution);
endif
this:send_line(connect, "Subject: " + subject);
this:send_line(connect, "Date: " + date);
this:send_line(connect, "Organization: " + organization);
this:send_line(connect, "");
for line in (body)
this:send_line(connect, line);
endfor
this:send_line(connect, ".");
line = this:read_line(connect);
this:release_connection(connect);
if (line[1] == "4")
this:log_error(verb, line);
return E_INVARG;
endif
player:tell("NNTP Posting complete.");
return 0;
.
posting_ok: return this.posting_allowed && (args[1].wizard || (args[1] in this.posting_ok)); . check_cache:
"cache maintenance for NNTP.";
"return an item if it's in the cache, {} otherwise.";
"args[1] should be an article reference, args[2] one of head, body,";
"or article.";
if (tmp = {args[1], args[2]} in this.cache_requests)
if (tmp != 1)
"got a hit. move it to the front of the cache.";
this:reorder_cache(tmp);
endif
return this.cache_values[1];
elseif ((args[2] != "article") && (tmp = {args[1], "article"} in this.cache_requests))
if (tmp != 1)
this:reorder_cache(tmp);
endif
break_ = "" in this.cache_values[1];
if (args[2] == "head")
return this.cache_values[1][1..break_ - 1];
else
return this.cache_values[1][break_ + 1..length(this.cache_values[1])];
endif
else
return {};
endif
.
reorder_cache:
"Cache maintenance for NNTP.";
"on a hit, we move the item to the head of the cache, so future";
"lookups go faster. hi jay.";
this.cache_values = {this.cache_values[args[1]], @setremove(this.cache_values, this.cache_values[args[1]])};
this.cache_requests = {this.cache_requests[args[1]], @setremove(this.cache_requests,
this.cache_requests[args[1]])};
this.cache_times = {time(), @setremove(this.cache_times, this.cache_times[args[1]])};
.
insert_cache:
if (caller != this)
return E_PERM;
endif
"Cache maintenance for NNTP";
"Inserts an article into the cache, timestamping it.";
"args[1] should be an article identifier, args[2] should be one of";
"head, body, or article, args[3] should be the text.";
if ((length(this.cache_requests) == this.cache_limit) && (!this:expire_cache()))
"if the cache is full and we can't expire anything, pull off the";
"last (presumably LRU) item.";
this.cache_requests = {{args[1], args[2]}, @this.cache_requests[1..length(this.cache_requests)
- 1]};
this.cache_values = {args[3], @this.cache_values[1..length(this.cache_values)
- 1]};
this.cache_times = {time(), @this.cache_times[1..length(this.cache_times) - 1]};
else
this.cache_requests = {{args[1], args[2]}, @this.cache_requests[1..length(this.cache_requests)]};
this.cache_values = {args[3], @this.cache_values[1..length(this.cache_values)]};
this.cache_times = {time(), @this.cache_times[1..length(this.cache_times)]};
endif
.
log_error:
if (this.debug && (caller == this))
this.error_log = {@this.error_log, {ctime(), @args}};
endif
.
flush_cache:
if ((!caller) == this)
return E_PERM;
endif
this.cache_values = this.cache_times = this.cache_requests = {};
.
expire_cache:
"called by the expiry task to remove dusty items from the cache.";
"returns the number of items it removed.";
if (caller != this)
return E_PERM;
endif
expiry = time() - this.expiry;
removed = 0;
for item in (this.cache_times)
if (expiry > item)
ind = item in this.cache_times;
this.cache_values = setremove(this.cache_values, this.cache_values[ind]);
this.cache_requests = setremove(this.cache_requests, this.cache_requests[ind]);
this.cache_times = setremove(this.cache_times, this.cache_times[ind]);
removed = removed + 1;
endif
endfor
return removed;
.
@expire @expire-task @expire-kill:
"Either expire NNTP's caches now, or start or kill the expiration task.";
if (!$perm_utils:controls(player, this))
return E_PERM;
endif
if (length(verb) == 7)
player:tell(("Expiry procedure removed " + tostr(this:expire_cache())) + " item(s)
from the NNTP cache.");
elseif (index(verb, "task"))
if (this.expire_task)
player:tell("Expire task seems to be already running.");
else
fork task (0)
this:expire_task();
endfork
this.expire_task = task;
endif
else
if (!this.expire_task)
player:tell("Expire task seems not to be running.");
else
kill_task(this.expire_task);
this.expire_task = 0;
endif
endif
.
expire_task:
if (caller != this)
return E_PERM;
endif
if (this.cache_values)
this:expire_cache();
endif
fork task (this.expire_delay)
this:expire_task();
endfork
this.expire_task = task;
.
fetch_newnews:
":Fetch_newnews(newsgroups, last-read)";
"Return a list of message-IDs posted to newsgroups since last-read.";
"newsgroups is either a string containing a single newsgroup, or a list of strings.
last-read is a time.";
if (typeof(connection = this:get_connection()) != ERR)
text = this:(verb[index(verb, "_") + 1..length(verb)])(connection, @args);
this:release_connection(connection);
return text;
else
return connection;
endif
.
newnews:
if (caller != this)
return E_PERM;
endif
"Fetch a list of new message IDs from a newsgroup, given a connection, a set of newsgroups,
and a last-this:read_line time.";
conn = args[1];
if (typeof(args[2]) == LIST)
groups = $string_utils:from_list(args[2], ",");
else
groups = args[2];
endif
last_read = args[3];
this:send_line(conn, (((verb + " ") + groups) + " ") + $time_utils:time_sub(this.timeformat,
last_read));
line = this:read_line(conn);
if (line[1] != "2")
this:log_error("newnews", line);
return E_INVARG;
endif
return this:nntp_read(conn);
.
read_line:
if (caller != this)
return E_PERM;
endif
value = read(@args);
return value;
.
send_line:
if (caller != this)
return E_PERM;
endif
notify(@args);
.
fetch_xhdr:
":fetch_xhdr(header, messages)";
"return a list of headers associated with the article identifier(s) passed. header
is a valid header line, such as 'subject'. 'message' should either be a message-id
or a list of article identifiers.";
"Note that since may NNTP servers do not properly implement the XHDR command (especially
those running on a certain proprietary VAX operating system which Shall Remain Nameless,)
this command may be disabled.";
if (!this.xhdr_enabled)
return E_PERM;
endif
if (typeof(connection = this:get_connection()) != ERR)
if (typeof(args[2]) != LIST)
args[1] = {args[1]};
endif
text = this:(verb[index(verb, "_") + 1..length(verb)])(connection, @args);
this:release_connection(connection);
return text;
else
return connection;
endif
.
xhdr:
":xhdr(connection,header,articles)";
if (caller != this)
return E_PERM;
endif
conn = args[1];
header = args[2];
text = {};
for item in (args[3])
if (typeof(article = this:check_id(item)) == ERR)
return E_INVARG;
endif
if (typeof(article) == LIST)
this:send_line(conn, "GROUP " + article[1]);
line = this:read_line(conn);
if (line[1] != "2")
this:log_error("group", line);
return E_INVARG;
endif
this:send_line(conn, (((verb + " ") + header) + " ") + article[2]);
line = this:read_line(conn);
if (line[1] != "2")
this:log_error(verb, line);
return E_INVARG;
endif
result = this:nntp_read(conn);
"bit of weirdness here. for no particular reason, the XHDR command prefixes
the header line with its message-id or article number.";
text = {@text, @result[length(article[2]) + 2..length(result[1])]};
else
this:send_line(conn, (((verb + " ") + header) + " ") + article);
line = this:read_line(conn);
if (line[1] != "2")
this:log_error(verb, line);
return E_INVARG;
endif
result = this:nntp_read(conn);
"bit of weirdness here. for no particular reason, the XHDR command prefixes
the header line with its message-id or article number.";
text = {@text, result[1][length(article) + 2..length(result[1])]};
endif
endfor
return text;
.
PROPERTY DATA:       host       port       cache_times       cache_values       cache_requests       error_log       debug       last_groups_update       newsgroups       timeformat       connect_max       connections       in_use       posting_allowed       posting_ok       cache_limit       help_msg       line_limit       timeout       distributions       expiry       expire_task       expire_delay       xhdr_enabled |