diff --git a/CHANGESTODO b/CHANGESTODO
index d294745..fcad647 100644
--- a/CHANGESTODO
+++ b/CHANGESTODO
@@ -6,6 +6,9 @@ Essentially: whenever you fix something, move that line to the end of file.
________________________________________________________________________
== NEXT RELEASE ========================================================
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
+- autorefetch twitter isnt working
+ http/fetch is too complicated. throw out the queue and callback logic. kiss!
+
? support tls multiplexing on all suitable ports
? bugs in psyced install procedure
@@ -35,6 +38,28 @@ ________________________________________________________________________
im psyc://psyced.org/~lynx Context festgestellt.
? who's gonna clean up the mess of having too many websites ?
+
+- when provided with a _focus pointing to yourself, _request_execute will
+ still try to forward a command to the "current place" - a uniform or
+ object is passed to parsecmd(data, dest) but dest is ignored later in cmd()
+
+- _source_relay should point to tg, not foo (only happens with local users)
+ .
+ :_source.psyc://x-net.hu/~foo
+ :_target.psyc://3e44aa49.XXX:-52801/
+ :_source_relay.psyc://x-net.hu/~foo
+
+ :_nick.tg
+ :_time_INTERNAL.1239787956
+ :_nick_target.psyc://x-net.hu/~foo
+ _message_private
+ halo
+ .
+
+- the linking blues...
+ tgX sagt: on reconnect i didn't get any _echo_place_enter_login's from this room, then no response for _request_do_enter, and after a _request_do_leave i get replies for enter/leave but instead of _echo_place_enter/leave i get _notice_place_enter/leave
+
+? should /load inform that errors go to the console?
________________________________________________________________________
== psyced 1.0 ==========================================================
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
@@ -3895,3 +3920,12 @@ spyc/* psyc/*
install.sh
+ don't leave _host_IP uncommented
+ mkdir data/* log/*
+=== 200904 ============================================================
+net/twitter
++ polly: periodically fetch updates from a twitter account and distribute to
+ hundreds of newsfeed contexts
++ listing: provide a listing of subscribed feeds
+http/fetch
+- minor api cleanups to serve a more generic job
+library/dns
++ honor #define _flag_disable_trust_localhost
diff --git a/bin/psyconf b/bin/psyconf
index 7b4d5ab..e84a32b 100755
--- a/bin/psyconf
+++ b/bin/psyconf
@@ -489,6 +489,10 @@ fi
if test "\$1" = "-m"
then
+ # git is seriously unsuited for this job. we need to "check in" the changes
+ # to file permissions etc or otherwise we can't update things at all...
+ (cd $base && git commit -a -m 'automatic by psyced -m')
+ # merging will of course fail.. sigh
(cd $base && git merge -s resolve origin)
exit
fi
diff --git a/world/default/en/html.textdb b/world/default/en/html.textdb
index 33b8540..91c5c51 100644
--- a/world/default/en/html.textdb
+++ b/world/default/en/html.textdb
@@ -1423,6 +1423,24 @@ _HTML_examine_friend_new
_HTML_examine_button_call
| {_TEXT_button_call}
+_HTML_listing_head_twitter
+|
twitter polly listing of available feeds
+|
+|
+|
+
+_HTML_listing_item_twitter
+|
+|
+|## [_nick]
+|[_name]
+|[_description]
+|[_uniform_context]
+|
+
_HTML_call
|
|
diff --git a/world/default/en/plain.textdb b/world/default/en/plain.textdb
index dcdb5ba..23a3a5e 100644
--- a/world/default/en/plain.textdb
+++ b/world/default/en/plain.textdb
@@ -696,6 +696,9 @@ _message_public
_message_friends
|[_nick] {_TEXT_action_shouts}: [_data]
+_message_twitter
+|[_nick] ... [_data]
+
_message_echo_private
|You tell [_nick_target]: [_data]
diff --git a/world/drivers/ldmud/include/interface.h b/world/drivers/ldmud/include/interface.h
index a1dd9e0..493f7df 100644
--- a/world/drivers/ldmud/include/interface.h
+++ b/world/drivers/ldmud/include/interface.h
@@ -95,6 +95,7 @@
// nosave? static? volatile. only for variables, not methods!
// another nice word for the opposite of persistent would be "shed"
#define volatile nosave
+#define persistent
// every lpc dialect has its own foreach syntax. aint that cute?
#define each(ITERATOR, LIST) foreach(ITERATOR : LIST)
diff --git a/world/drivers/ldmud/master/accept.c b/world/drivers/ldmud/master/accept.c
index 00fff09..429a3fb 100644
--- a/world/drivers/ldmud/master/accept.c
+++ b/world/drivers/ldmud/master/accept.c
@@ -372,6 +372,8 @@ object compile_object(string file) {
// match both http:/ and https:/ objects ;D
if (abbrev("http", file)) {
rob = clone_object(HTTP_PATH "fetch");
+ // driver has the habit of removing double slash in object name
+ file = replace(file, ":/", "://");
if (rob) rob->fetch(file[..<3]);
return rob;
}
diff --git a/world/drivers/pike/include/interface.h b/world/drivers/pike/include/interface.h
index 2bfbefa..2fbd5a4 100644
--- a/world/drivers/pike/include/interface.h
+++ b/world/drivers/pike/include/interface.h
@@ -83,6 +83,7 @@
// nosave? static? volatile. no idea if pike has something like this
#define volatile static
+#define persistent
// every lpc dialect has its own foreach syntax. aint that cute?
#define each(ITEM, LIST) foreach(LIST; ; ITEM)
diff --git a/world/net/connect.c b/world/net/connect.c
index f7c9301..34c70e2 100644
--- a/world/net/connect.c
+++ b/world/net/connect.c
@@ -23,6 +23,7 @@ protected int block() { destruct(ME); return 0; }
protected connect_failure(mc, text) {
is_connecting = 0;
+ P1(("connect failure %O in %O, %O.\n", mc, ME, text))
monitor_report("_failure_network_connect"+ mc,
object_name(ME) +" · "+ text);
}
@@ -42,7 +43,7 @@ protected int logon(int failure) {
}
is_connecting = 0;
if (failure == -1 || !interactive(ME)) {
- PT(("Warning: Failed connect attempt in %O\n", ME))
+ P3(("Warning: Failed connect attempt in %O\n", ME))
#if __EFUN_DEFINED__(errno)
connect_failure("_attempt", sprintf("connect failed: errno %s",
errno()));
diff --git a/world/net/http/fetch.c b/world/net/http/fetch.c
index 5800b77..4f3ebb0 100644
--- a/world/net/http/fetch.c
+++ b/world/net/http/fetch.c
@@ -2,8 +2,13 @@
//
// generic HTTP GET client, mostly used for RSS -
// but we could fetch any page or data with it, really
-// tobij even made the object have the URL as its object name. fancy! ;)
-//
+// tobij even allowed the object to have the URL as its object name. fancy! ;)
+
+#ifdef Dfetch
+# undef DEBUG
+# define DEBUG Dfetch
+#endif
+
#include
#include
#include
@@ -24,7 +29,7 @@ volatile string modificationtime, etag, http_message;
volatile string useragent = SERVER_VERSION;
volatile int http_status, port, fetching, ssl;
volatile string buffer, thehost, url, fetched, host, resource;
-volatile string basicauth;
+volatile string basicauth = "";
int parse_status(string all);
int parse_header(string all);
@@ -33,22 +38,28 @@ int buffer_content(string all);
string qHost() { return thehost; }
void fetch(string murl) {
- if (url) return;
- url = replace(murl, ":/", "://");
- P3(("%O: fetch(%O)\n", ME, url))
- connect();
+ if (url != murl) {
+ // accept.c does this for us:
+ //url = replace(murl, ":/", "://");
+ // so we can use this method also in a normal way
+ url = murl;
+ // resource may need to be re-parsed (other params)
+ resource = 0;
+ // re-parse the hostname?
+ //thehost = port = 0;
+ }
+ P3(("%O: fetch(%O)\n", ME, url))
+ unless (fetching) connect();
}
object load() { return ME; }
-void setHTTPBasicAuth(string user, string password) {
- basicauth = "Authorization: Basic " + encode_base64(user + ":" + password) + "\r\n";
+void sAuth(string user, string password) {
+ basicauth = "Authorization: Basic "+
+ encode_base64(user +":"+ password) +"\r\n";
}
-string sAgent(string a) {
- PT(("sAgent(%O) in %O\n", a, ME))
- return useragent = a;
-}
+string sAgent(string a) { return useragent = a; }
// net/place/news code follows.
@@ -65,7 +76,7 @@ void connect() {
thehost = lower_case(thehost);
ssl = t == "s";
}
- P3(("URL, THEHOST: %O, %O\n", url, thehost))
+ P4(("URL, THEHOST: %O, %O\n", url, thehost))
unless (port)
unless (sscanf(thehost, "%s:%d", thehost, port) == 2)
port = ssl? HTTPS_SERVICE: HTTP_SERVICE;
@@ -73,18 +84,18 @@ void connect() {
::connect(thehost, port);
}
-varargs int real_logon(int arg) {
+varargs int real_logon(int failure) {
string scheme;
headers = ([ ]);
http_status = 500;
http_message = "(failure)"; // used by debug only
- unless(::logon(arg)) return -1;
+ unless(::logon(failure)) return -1;
unless (url) return -3;
unless (resource) sscanf(url, "%s://%s/%s", scheme, host, resource);
- buffer = "";
+ buffer = basicauth;
if (modificationtime)
buffer += "If-Modified-Since: "+ modificationtime + "\r\n";
if (useragent) buffer += "User-Agent: "+ useragent +"\r\n";
@@ -92,7 +103,8 @@ varargs int real_logon(int arg) {
// emit("If-None-Match: " + etag + "\r\n");
// we won't need connection: close w/ http/1.0
//emit("Connection: close\r\n\r\n");
- PT(("%O using %O\n", ME, buffer))
+ P1(("%O fetching /%s from %O\n", ME, resource, host))
+ P4(("%O using %O\n", ME, buffer))
emit("GET /"+ resource +" HTTP/1.0\r\n"
"Host: "+ host +"\r\n"
+ buffer +
@@ -103,7 +115,7 @@ varargs int real_logon(int arg) {
return 0; // duh.
}
-varargs int logon(int arg, int sub) {
+varargs int logon(int failure, int sub) {
// net/connect disables telnet for all robots and circuits
#if 0 //__EFUN_DEFINED__(enable_telnet)
// when fetching the spiegel rss feed, telnet_neg() occasionally
@@ -112,9 +124,9 @@ varargs int logon(int arg, int sub) {
enable_telnet(0);
#endif
// when called from xmlrpc.c we can't do TLS anyway
- if (sub) return ::logon(arg);
+ if (sub) return ::logon(failure);
if (ssl) tls_init_connection(ME, #'real_logon);
- else real_logon(arg);
+ else real_logon(failure);
return 0; // duh.
}
@@ -124,8 +136,9 @@ int parse_status(string all) {
sscanf(all, "%s%t%s", prot, state);
sscanf(state, "%d%t%s", http_status, http_message);
- P3(("%O got %O %O from %O\n", ME, http_status, http_message, host));
if (http_status != R_OK) {
+ P1(("%O got %O %O from %O\n", ME,
+ http_status, http_message, host));
monitor_report("_failure_unsupported_code_HTTP",
S("http/fetch'ing %O returned %O %O", url || ME,
http_status, http_message));
@@ -136,9 +149,9 @@ int parse_status(string all) {
int parse_header(string all) {
string key, val;
- D2(D("htroom::parse is: " + all + "\n");)
// TODO: parse status code
if (all != "") {
+ P2(("http/fetch::parse_header %O\n", all))
if (sscanf(all, "%s:%1.0t%s", key, val) == 2) {
headers[lower_case(key)] = val;
// P2(("ht head: %O = %O\n", key, val))
@@ -147,6 +160,7 @@ int parse_header(string all) {
return 1;
} else {
// das wollen wir nur bei status 200
+ P2(("%O now waiting for http body\n", ME))
next_input_to(#'buffer_content);
return 1;
}
@@ -154,70 +168,72 @@ int parse_header(string all) {
}
int buffer_content(string all) {
+ P2(("%O body %O\n", ME, all))
buffer += all + "\n";
next_input_to(#'buffer_content);
return 1;
}
disconnected(remainder) {
- headers["_fetchtime"] = isotime(ctime(time()), 1);
- if (headers["last-modified"])
+ P2(("%O got disconnected.. %O\n", ME, remainder))
+ headers["_fetchtime"] = isotime(ctime(time()), 1);
+ if (headers["last-modified"])
modificationtime = headers["last-modified"];
- if (headers["etag"])
+ if (headers["etag"])
etag = headers["etag"]; // heise does not work with etag
- fetched = buffer;
- if (remainder) fetched += remainder;
- fheaders = headers;
- buffer = headers = 0;
- switch (http_status) {
- case R_OK:
- mixed *waiter;
- while (qSize(ME)) {
- waiter = shift(ME);
- funcall(waiter[0], fetched, waiter[1] ? fheaders : copy(fheaders));
+ fetched = buffer;
+ if (remainder) fetched += remainder;
+ fheaders = headers;
+ buffer = headers = 0;
+ switch (http_status) {
+ case R_OK:
+ mixed *waiter;
+ while (qSize(ME)) {
+ waiter = shift(ME);
+ P2(("%O calls back.. body is %O\n", ME, fetched))
+ funcall(waiter[0], fetched, waiter[1] ? fheaders : copy(fheaders));
+ }
+ break;
+ default:
+ // doesn't seem to get here when HTTP returns 301 or 302. strange.
+ // fall thru
+ case R_NOTMODIFIED:
+ qDel(ME);
+ qInit(ME, 150, 5);
}
- break;
- default:
- // doesn't seem to get here when HTTP returns 301 or 302. strange.
- // fall thru
- case R_NOTMODIFIED:
- qDel(ME);
- qInit(ME, 150, 5);
- }
-
- fetching = 0;
- return 1; // presume this disc was expected
+ fetching = 0;
+ return 1; // presume this disc was expected
}
varargs string content(closure cb, int force, int willbehave) {
- if (cb) {
- if (fetched) {
- if (force) {
- funcall(cb, fetched, willbehave ? fheaders : copy(fheaders));
+ if (cb) {
+ if (fetched) {
+ if (force) {
+ funcall(cb, fetched, willbehave ? fheaders : copy(fheaders));
+ }
+ } else {
+ enqueue(ME, ({ cb, willbehave }));
}
- } else {
- enqueue(ME, ({ cb, willbehave }));
}
- }
- return fetched;
+ return fetched;
}
varargs mapping headers(int willbehave) {
- return willbehave ? fheaders : copy(fheaders);
+ return willbehave ? fheaders : copy(fheaders);
}
string qHeader(mixed key) {
- if (mappingp(fheaders)) return fheaders[key];
- return 0;
+ if (mappingp(fheaders)) return fheaders[key];
+ return 0;
}
varargs void refetch(closure cb, int willbehave) {
- enqueue(ME, ({ cb, willbehave }));
- unless (fetching) connect();
+ enqueue(ME, ({ cb, willbehave }));
+ unless (fetching) connect();
}
protected create() {
- qCreate();
- qInit(ME, 150, 5);
+ qCreate();
+ qInit(ME, 150, 5);
}
diff --git a/world/net/http/xmlrpc.c b/world/net/http/xmlrpc.c
index 75c8a67..e65292d 100644
--- a/world/net/http/xmlrpc.c
+++ b/world/net/http/xmlrpc.c
@@ -189,6 +189,7 @@ mixed unMarshal(XMLNode parsed) {
}
}
+// why is this almost the same code as in http/fetch?
int disconnected(string remainder) {
mixed *args;
diff --git a/world/net/irc/user.c b/world/net/irc/user.c
index 93fc161..edbd371 100644
--- a/world/net/irc/user.c
+++ b/world/net/irc/user.c
@@ -414,12 +414,20 @@ w(string mc, string data, mapping vars, mixed source) {
# ifdef ALIASES
if (raliases[source]) {
nick2 = raliases[source];
+# if 0
vars["_INTERNAL_source_IRC"] = nick2 +"!"+
u[UNick]? u[UNick] +"@"+ u[UHost]
: (vars["_nick_long"]
|| vars["_INTERNAL_nick_plain"]
|| vars["_nick"])
+"@alias.undefined";
+# else
+ vars["_INTERNAL_source_IRC"] = nick2 +"!"+
+ (u[UNick]? u[UNick] +"@"+ u[UHost]
+ : (vars["_nick_long"]
+ || vars["_INTERNAL_nick_plain"]
+ || vars["_nick"]));
+# endif
}
unless (nick2) {
diff --git a/world/net/library/dns.c b/world/net/library/dns.c
index b558379..cd96d42 100644
--- a/world/net/library/dns.c
+++ b/world/net/library/dns.c
@@ -107,7 +107,7 @@ int legal_host(string ip, int port, string scheme, int udpflag) { // proto.h!
// we sincerely hope our kernel will drop udp packets that
// spoof they are coming from localhost.. would be ridiculous if not
// if (ip == "127.0.0.1" || ip == "127.1" || ip == "0") return 9;
-#ifndef DONT_TRUST_LOCALHOST
+#ifndef _flag_disable_trust_localhost
if (localhosts[ip]) return 9;
if (ip == myIP || !ip) return 8;
#endif
@@ -123,7 +123,7 @@ int legal_domain(string host, int port, string scheme, int udpflag) {
// spoof they are coming from localhost.. would be ridiculous if not
//
// TODO: use is_localhost here?
-#ifndef DONT_TRUST_LOCALHOST
+#ifndef _flag_disable_trust_localhost
if (host == "localhost" || host == SERVER_HOST) return 9;
#endif
// if (host == myLowerCaseHost) return 8;
diff --git a/world/net/library/jsonparser.pike b/world/net/library/jsonparser.pike
index 0cd2b75..dacd0d7 100644
--- a/world/net/library/jsonparser.pike
+++ b/world/net/library/jsonparser.pike
@@ -367,6 +367,7 @@ mapping jsonObject()
}
if (nextClean() != ':') {
+ PT(("jsonFAIL: '%c' at %O\n", nextClean(), myIndex))
THROW("Expected a ':' after a key.\n");
}
diff --git a/world/net/person.c b/world/net/person.c
index eec58b2..2b80bc0 100644
--- a/world/net/person.c
+++ b/world/net/person.c
@@ -1315,6 +1315,7 @@ case "_message_echo_private":
// fall thru
case "_message_echo_public":
case "_message_echo":
+case "_message_twitter":
case "_message_public":
// avoid treating this as _message here
break;
@@ -1323,6 +1324,9 @@ case "_message_audio":
// not being displayed to users other than psyc clients
data = 0;
break;
+// we should judge our messages by their routing method, not by their
+// name! thus, the _public and _private distinction has to exist only
+// for display. FIXME
case "_message":
// this is only visible in person.c, not user.c
// therefore probably useless
@@ -1372,24 +1376,33 @@ case "_request_execute":
// our places
if (places[t]) {
vSet("place", place = t);
+ PT(("REQ-EX place %O\n", t))
} else {
// see if it is a local object
object o = psyc_object(t);
-
- // object one of our places?
- if (o && places[o]) {
- place = o;
- vSet("place", o->qName());
+ if (o) {
+ // object one of our places?
+ if (places[o]) {
+ place = o;
+ vSet("place", o->qName());
+ PT(("REQ-EX o'place %O\n", o))
+ } else {
+ PT(("REQ-EX object %O not found in %s's places %O\n", o, MYNICK, places))
+ }
} else unless (t2) {
// must be a person then
// ME->parsecmd(data, t);
- parsecmd(data, t);
+ PT(("REQ-EX person %O vs %O\n", t, o))
// should be able to put o||t
// here.. TODO
+ parsecmd(data, t);
return 0;
}
}
}
+ else {
+ PT(("REQ-EX non-string %O\n", t))
+ }
// ME->parsecmd(data);
if (t2) {
unless (request(source, t2, vars, data)) {
@@ -1632,8 +1645,12 @@ case "_notice_invitation":
vars["_nick_place"] : vars["_place"]);
// same filtering code as couple lines further below
- if (( IS_NEWBIE || !itsme && FILTERED(source)) &&
- (!profile || profile[PPL_NOTIFY] <= PPL_NOTIFY_PENDING)) {
+ if ((
+#ifndef _flag_enable_unauthenticated_message_private
+ IS_NEWBIE ||
+#endif
+ (!itsme && FILTERED(source)) &&
+ (!profile || profile[PPL_NOTIFY] <= PPL_NOTIFY_PENDING))) {
sendmsg(source, "_failure_filter_strangers", 0,
([ "_nick" : MYNICK ]) );
unless (boss(source))
diff --git a/world/net/place/archetype.gen b/world/net/place/archetype.gen
index 44a5b24..d2be82b 100644
--- a/world/net/place/archetype.gen
+++ b/world/net/place/archetype.gen
@@ -1354,6 +1354,7 @@ msg(source, mc, data, mapping vars) {
else unless (!source
|| qAllowExternal(&source, mc, vars) // mayExternal hook
|| MEMBER(source)
+ || source == "/" // allow root to send everywhere
|| isValidRelay(source)
|| isValidRelay(vars["_source_relay"])
|| isValidRelay(vars["_context"])) {
@@ -1789,6 +1790,7 @@ cmd(a, args, b, source, vars) {
return 1;
# endif
case "hc":
+ case "histclean": // in case you don't remember exactly.. it's ok
case "histclear": // wieso um alles in der welt soll das jeder dürfen?
// liest denn keiner die cvs kommentare? da stands
// drin.. ok wir sind realistisch und schreiben es
diff --git a/world/net/psyc/edit.i b/world/net/psyc/edit.i
index da11828..62c9bc5 100644
--- a/world/net/psyc/edit.i
+++ b/world/net/psyc/edit.i
@@ -177,8 +177,8 @@ static varargs string psyc_render(mixed source, string mc, mixed data,
data = data? to_string(data): "";
#endif
}
- else if (data == S_GLYPH_PACKET_DELIMITER ||
- (data[0] == C_GLYPH_PACKET_DELIMITER && data[1] == '\n')
+ else if (data == S_GLYPH_PACKET_DELIMITER || (strlen(data) > 1 &&
+ data[0] == C_GLYPH_PACKET_DELIMITER && data[1] == '\n')
|| strstr(data, "\n" S_GLYPH_PACKET_DELIMITER "\n") != -1) {
// this check shouldn't be necessary here: we should check what
// people are typing in usercmd
diff --git a/world/net/psyc/parse.i b/world/net/psyc/parse.i
index 631b0e8..9a9e1a5 100644
--- a/world/net/psyc/parse.i
+++ b/world/net/psyc/parse.i
@@ -414,7 +414,8 @@ vamixed parse(string a) {
#ifndef __PIKE__
if (peerip && pongtime + 120 < time()) {
if (same_host(SERVER_HOST, peerip)) {
- P1(("why am i talking psyc to myself?\n"))
+ P1(("Another PSYC node on my IP? Or am I talking to myself? %O\n", ME))
+ // not ponging to ping then...
} else {
#ifdef PSYC_TCP
P2(("%O sending TCP PONG to %O=%O\n",
diff --git a/world/net/twitter/listing.c b/world/net/twitter/listing.c
new file mode 100644
index 0000000..af90246
--- /dev/null
+++ b/world/net/twitter/listing.c
@@ -0,0 +1,126 @@
+// vim:foldmethod=marker:syntax=lpc:noexpandtab
+//
+// http://localhost:33333/net/twitter/listing shows a list of friends
+
+#include
+#include
+#include
+
+volatile object fetcha;
+volatile mixed wurst;
+
+parse(string body) {
+ if (!body || body == "") {
+ P1(("%O failed to get its listing.\n", ME))
+ return;
+ }
+//#if DEBUG > 0
+ rm(DATA_PATH "twitter/friends.json");
+ write_file(DATA_PATH "twitter/friends.json", body);
+ P4((body))
+//#endif
+ unless (pointerp(wurst = parse_json(body))) {
+ P1(("%O failed to parse its listing.\n", ME))
+ return;
+ }
+#ifdef DEVELOPMENT
+ write_file(DATA_PATH "twitter/friends.parsed", sprintf("%O\n", wurst));
+#endif
+ P1(("%O sorting %O subscription names ", ME, sizeof(wurst)))
+ wurst = sort_array(wurst, (:
+ unless (mappingp($1)) return 0;
+ unless (mappingp($2)) return 1;
+// PT(("%O got %O vs %O\n", ME, $1, $2))
+ P1(("."))
+ return lower_case($2["screen_name"] || "") >
+ lower_case($1["screen_name"] || "");
+ :) );
+ P1((" done!\n"))
+}
+
+htget(prot, query, headers, qs, data, noprocess) {
+ string nick;
+ mapping d; //, s;
+ int i;
+
+ //sTextPath(query["layout"] || "twitter", query["lang"], "html");
+ localize(query["lang"], "html");
+
+ unless (pointerp(wurst)) {
+ hterror(R_TEMPOVERL,
+ "Haven't successfully retrieved data yet.");
+ return;
+ }
+ htok(prot); // outputs utf-8 header, but..
+ w("_HTML_listing_head_twitter");
+ for (i=sizeof(wurst)-1; i>=0; i--) {
+ d = wurst[i];
+ unless (mappingp(d)) {
+ P1(("%O got a broken entry: %O.\n", ME, d))
+ continue;
+ }
+//
+// user "foebud" has no updates ;)
+//
+// s = d["status"];
+// unless (mappingp(s)) {
+// P1(("%O got a statusless entry: %O.\n", ME, d))
+// continue;
+// }
+ unless (nick = d["screen_name"]) {
+ P1(("%O got a nickless tweeter.\n", ME))
+ continue;
+ }
+ w("_HTML_listing_item_twitter", 0, ([
+ // should i send text as _action?
+ "_nick": nick,
+ "_amount_updates": d["statuses_count"],
+ // _count_subscribers seems to be better for this
+ // or should it be _recipients? _targets?
+ "_amount_followers": d["followers_count"],
+ "_amount_sources": d["friends_count"],
+ // shows how old listing is.. hmm
+ //"_description_update": s["text"] || "",
+ "_color": "#"+ d["profile_sidebar_fill_color"],
+ "_description": d["description"] || "",
+ "_uniform_context": SERVER_UNIFORM +"@"+ nick,
+ "_page": d["url"] || "",
+ "_name": d["name"] || "",
+ // "_contact_twitter": d["id"],
+ "_reference_reply": d["in_reply_to_screen_name"],
+ // "_twit": d["id"],
+ "_uniform_photo": d["profile_image_url"] || "",
+ "_uniform_photo_background":
+ d["profile_background_image_url"] || ""
+ ]));
+ }
+ w("_HTML_listing_tail_twitter");
+ return 1;
+}
+
+fetch() {
+ fetcha -> content( #'parse, 0, 1 );
+ fetcha -> fetch("http://twitter.com/statuses/friends.json?count=200");
+}
+
+create() {
+ mapping config;
+ object o = find_object(CONFIG_PATH "config");
+
+ if (o) config = o->qConfig();
+ if (!config) {
+ P1(("\nNo configuration for twitter gateway found in %O.\n", o))
+ //destruct(ME);
+ return 1;
+ }
+
+ string body = read_file(DATA_PATH "twitter/friends.json");
+ if (body) return parse(body);
+
+ // we could even choose to inherit this instead...
+ fetcha = clone_object(NET_PATH "http/fetch");
+ //fetcha -> sAgent(SERVER_VERSION " builtin Twitter to PSYC gateway");
+ fetcha -> sAuth(config["nickname"], config["password"]);
+ call_out( #'fetch, 14 );
+}
+
diff --git a/world/net/twitter/polly.c b/world/net/twitter/polly.c
new file mode 100644
index 0000000..4d4b6e6
--- /dev/null
+++ b/world/net/twitter/polly.c
@@ -0,0 +1,117 @@
+// vim:foldmethod=marker:syntax=lpc:noexpandtab
+//
+// yeah yeah twitter.. why twitter?
+// http://about.psyc.eu/Twitter
+
+#include
+
+persistent int lastid;
+
+volatile object feed;
+
+parse(string body, mapping headers) {
+ mixed wurst;
+ string nick;
+ object o;
+ mapping d, p;
+ int i;
+
+ if (!body || body == "") {
+ P1(("%O failed to get its timeline from %O.\n", ME,
+ previous_object()))
+ PT(("Got headers: %O\n", headers))
+ return;
+ }
+//#if DEBUG > 0
+ rm(DATA_PATH "timeline.json");
+ write_file(DATA_PATH "timeline.json", body);
+ P4((body))
+//#endif
+ unless (pointerp(wurst = parse_json(body))) {
+ P1(("%O failed to parse its timeline.\n", ME))
+ return;
+ }
+ unless (sizeof(wurst)) {
+ P1(("%O received an empty structure.\n", ME))
+ return;
+ }
+ if (wurst[0]["id"] <= lastid) {
+ P1(("%O received %d old updates.\n", ME, sizeof(wurst)))
+ return;
+ }
+ lastid = wurst[0]["id"];
+ save_object(DATA_PATH "twitter");
+ for (i=sizeof(wurst)-1; i>=0; i--) {
+ d = wurst[i];
+ unless (mappingp(d)) {
+ P1(("%O got a broken tweet: %O.\n", ME, d))
+ continue;
+ }
+ p = d["user"];
+ unless (mappingp(p)) {
+ P1(("%O got a userless tweet.\n", ME))
+ continue;
+ }
+ unless (nick = p["screen_name"]) {
+ P1(("%O got a nickless tweeter.\n", ME))
+ continue;
+ }
+ P4((" %O", nick))
+ o = find_place(nick);
+
+ // _notice_update_twitter ?
+ sendmsg(o, "_message_twitter", d["text"], ([
+ // should i send text as _action?
+ "_nick": nick,
+ // _count seems to be the better word for this
+ "_amount_updates": p["statuses_count"],
+ "_amount_followers": p["followers_count"],
+ "_amount_sources": p["friends_count"],
+ "_color": "#"+ p["profile_sidebar_fill_color"],
+ "_description": p["description"] || "",
+ "_page": p["url"] || "",
+ "_name": p["name"] || "",
+ // "_contact_twitter": p["id"],
+ "_description_agent_HTML": d["source"],
+ "_reference_reply": d["in_reply_to_screen_name"],
+ // "_twit": d["id"],
+ "_uniform_photo": p["profile_image_url"] || "",
+ "_uniform_photo_background":
+ p["profile_background_image_url"] || ""
+ ]), "/"); // send as root
+
+ // der spiegel u.a. twittern übrigens in latin-1
+ // während psyc utf-8 erwartet.. eine char guess engine
+ // muss her.. FIXME
+ }
+}
+
+fetch() {
+ P1(("%O going to fetch from %O since %O\n", ME, feed, lastid))
+ call_out( #'fetch, 4 * 59 ); // odd is better
+ feed -> content( #'parse, 1, 1 );
+ // twitter ignores since_id if count is present. stupid.
+ feed -> fetch("http://twitter.com/statuses/friends_timeline.json?"
+ // +( lastid? ("since_id="+ lastid) : "count=23"));
+ "count="+( lastid? ("23&since_id="+ lastid) : "23"));
+}
+
+create() {
+ mapping config;
+ object o = find_object(CONFIG_PATH "config");
+
+ if (o) config = o->qConfig();
+ if (!config) {
+ P1(("\nNo configuration for twitter gateway found.\n"))
+ //destruct(ME);
+ return;
+ }
+ restore_object(DATA_PATH "twitter");
+
+ // we could even choose to inherit this instead...
+ feed = clone_object(NET_PATH "http/fetch");
+ //feed -> sAgent(SERVER_VERSION " builtin Twitter to PSYC gateway");
+ feed -> sAuth(config["nickname"], config["password"]);
+ call_out( #'fetch, 14 );
+}
+
diff --git a/world/net/user.c b/world/net/user.c
index a2e132d..9f5a420 100644
--- a/world/net/user.c
+++ b/world/net/user.c
@@ -615,6 +615,9 @@ case "_jabber_iq_set":
case "_jabber":
P1(("%O got %O", ME, mc))
break;
+case "_notice_composing_media":
+ if (v("scheme") != "psyc") return 1;
+ break;
case "_message_video":
case "_message_audio":
// not being displayed to users other than psyc clients
@@ -753,6 +756,7 @@ case "_message_announcement":
case "_message_behaviour_warning":
case "_message_behaviour_punishment":
case "_message_behaviour":
+case "_message_twitter":
unless (stringp(data)) variant = "_default";
else variant = "";
#ifdef USE_THE_NICK
diff --git a/world/static/twitter/listing.css b/world/static/twitter/listing.css
new file mode 100644
index 0000000..6c2dc84
--- /dev/null
+++ b/world/static/twitter/listing.css
@@ -0,0 +1,31 @@
+* {
+ margin:0;padding:0;border-width:0;border-color:transparent;
+}
+a {
+ text-decoration:none;
+ color:#06a;
+}
+a:hover {
+ text-decoration:overline;
+}
+body{
+ font-family: 'Lucida Grande',helvetica,sans-serif;
+ color:#333;
+ background-color:#cc6;
+ margin:44px;
+}
+.listing {
+ background-color:#ddd;
+ border-left: 2px solid black;
+ border-right: 2px solid black;
+}
+
+.listing li:first-child{ border-top:1px dashed black; }
+.listing li{position:relative;padding:7;border-bottom:1px dashed black;line-height:1.1em;}
+.listing li:hover,ol.listing li.hover{background-color:white;}
+
+._photo{display:block;width:50px;height:50px;position:absolute;right:7;margin:0 0 0 0;}
+._photo img{width:48px;height:48px;}
+._description{font-size:70%;}
+._uniform{font-size:80%;}
+._uniform a{color:#c30}