• GeSHi library error: sites/all/modules/geshifilter/geshi is not a directory.
  • GeSHi library error: sites/all/modules/geshifilter/geshi is not a directory.

Twitter

A Modification to the Twitterbox, such that it Works Once More

I have been informed more than once over the past few days that my Twitterbox appears not to be operating as Desired - more specifically, that Twitterboxers were able to send updates quite satisfactorily, but not actually receive any, which was understandably frustrating for them.

On further investigation it appears that the Twitterfolk decided at some point to restrict requests to receive an Updated List of Friends' Tweets to those using the GET method. I have no idea why, but who am I to argue? In any case I have modified my own server script so that things should now Operate As Expected. For those using the script on their own Aethernet Servers, they may find the latest version here:

There should be no need for Twitterbox users inworld to do anything at all regarding this - it is mostly for Informational Purposes. No updating or such is required, as long as you are using version 0.4 or above.

Twitterbox LSL

Code summary: 

This is the LSL code for the latest version of the Twitterbox itself.

// Twitterbox v0.5 // Post to Twitter and receive updates from within SL // Ordinal Malaprop // 2007-02-26 // Last updated 2007-05-12 // Free for distribution and use, but if you use it in something else // I would like at least a mention. // Full instructions and the latest version are always at: // http://ordinalmalaprop.com/twitter/ //------------------------------------------------------------------ // Modifications from 0.3: // - only works when attached to prevent boxes on the floor spamming you // - keeps list of 10 most recent tweets, menu item added for this // - option to show or hide your own tweets, showing by default // - immediately checks for new tweets on startup // - option to repeat the last tweet you made, useful for failures // - checks for updates (separate script) // Changes from 0.4: // - option to block posts over 140 characters //------------------------------------------------------------------ // Edit these to your own specifications // The email address you signed up to Twitter with string EMAIL = "ordinal.malaprop@fastmail.fm"; // Your Twitter password string PASS = "h8gtied"; // Your public RSS feeds (leave blank if you don't have any or you don't know what this means) list FEEDS = [ "FLICKR", "http://api.flickr.com/services/feeds/photos_public.gne?id=25972087@N00&format=rss_200", "VIMEO", "http://www.vimeo.com/user:ordinal/clips/rss", "BLOG", "http://feeds.feedburner.com/engine-proceeding" ]; // Seconds between checks, change if desired float CHECK_INTERVAL = 120.0; // This is the URL of the intermediary script. Don't change it unless // you are using an intermediary of your own. string TWITTERPING = "http://ordinalmalaprop.com/twitter/control-0.4.php"; // Leave these alone. integer gTime = 0; integer gCode = 0; string gScreenName = ""; integer gManual = FALSE; list gRecent = []; integer gShowMine = TRUE; string gLastTweet = ""; // gNotify defines the type of notification // 0 = none // 1 = private sound // 2 = full animation and public sound integer gNotify = 2; integer gBlock140 = TRUE; //------------------------------------------------------------------ key twitter_send(string action, string subject) { if (EMAIL == "" || PASS == "") { llOwnerSay("No email/password set - edit script and try again"); return NULL_KEY; } else if (gBlock140 && llStringLength(subject) > 140) { llOwnerSay("Lawks! Your post was over 140 characters and has been blocked - please try again or touch me to turn this option off. Your post was:"); llOwnerSay(subject); return NULL_KEY; } else { vector pos = llGetPos(); return llHTTPRequest(TWITTERPING, [HTTP_METHOD, "POST"], EMAIL + "\n" + PASS + "\n" + action + "\n" + subject + "\n" + llGetRegionName() + "/" + (string)llRound(pos.x) + "/" + (string)llRound(pos.y) + "/" + (string)llRound(pos.z) + "/\n" + llDumpList2String(FEEDS, ",")); } } menu() { list buttons = ["Check Now", "Web", "List Feeds", "Recent", "Repeat", "Notify", "Help"]; string text = "Current email: " + EMAIL + "\n"; if (gShowMine) { text += "Showing your own tweets.\n"; buttons = ["Hide Mine"] + buttons; } else { text += "Hiding your own tweets.\n"; buttons = ["Show Mine"] + buttons; } if (gBlock140) { text += "Blocking posts > 140 chars\n"; buttons = ["Allow >140"] + buttons; } else { text += "Allowing posts > 140 chars\n"; buttons = ["Block >140"] + buttons; } if (gLastTweet != "") text += "Your last tweet: '" + gLastTweet + "'\n"; llDialog(llGetOwner(), text + "\nPlease select an option:", buttons, 282); } list_feeds() { integer feeds = llGetListLength(FEEDS); if (feeds == 0) { llOwnerSay("You have no feed keywords defined at the moment."); } else { llOwnerSay("Your current feed keywords are..."); integer f = 0; do { llOwnerSay(llList2String(FEEDS, f) + " - " + llList2String(FEEDS, f + 1)); f += 2; } while (f < feeds); } llOwnerSay("Edit the script to add or remove feed keywords - for more information see http://ordinalmalaprop.com/twitter/"); } string notify_level() { if (gNotify == 0) return "Quiet"; else if (gNotify == 1) return "Private"; else if (gNotify == 2) return "Public"; return "** something illegal **"; } help() { if (EMAIL == "" || PASS == "") { llOwnerSay( "BEWARE! You have not configured an `email and password! " + "Open the object, open the TwitterBox script, and fill in " + "the variables at the top."); } llOwnerSay( "To send a tweet, say '/282 ', or touch the TwitterBox " + "HUD for more options."); } about() { llOwnerSay("A simple SL client for Twitter - http://twitter.com/ " + "- by Ordinal Malaprop"); llOwnerSay( "TwitterBox will automatically check for new tweets from your " + "friends every minute, or when you tell it to manually from " + "the menu, obtainable by touching the HUD."); llOwnerSay( "To use, you need to have registered with Twitter, and edited " + "the script to include your email address and password."); llOwnerSay( "For more information or the latest version, visit " + "http://ordinalmalaprop.com/twitter/"); } twitterball() { llStartAnimation("Twitter"); llSleep(1.0); llRezObject("Twitterball", llGetPos() + <0.0, 0.0, 1.5>, ZERO_VECTOR, ZERO_ROTATION, 1); } startup() { // Get screen name again whenever it is attached, // as this may change twitter_send("get id", ""); // Check for new updates immediately twitter_send("check", ""); // Also display the help message help(); list_feeds(); llRequestPermissions(llGetOwner(), PERMISSION_TRIGGER_ANIMATION); llSetTimerEvent(CHECK_INTERVAL); } startup_check() { if (llGetAttached()) startup(); else llOwnerSay("I must be attached to operate - please wear me"); } list push(string tweet, list tweetlist) { tweetlist = tweetlist + [tweet]; if (llGetListLength(tweetlist) > 8) { // remove first element tweetlist = llDeleteSubList(tweetlist, 0, 0); } return tweetlist; } list_recent() { llOwnerSay("Recent tweets received:"); if (gRecent == []) { llOwnerSay("None received yet"); } integer f = 0; integer n = llGetListLength(gRecent); do { llOwnerSay(llList2String(gRecent, f)); } while (++f < n); } //------------------------------------------------------------------ default { state_entry() { llOwnerSay("Initialising..."); // At the start... // reset the clock to now // gTime = llGetUnixTime(); // and also start listening for commands llListen(282, "", llGetOwner(), ""); // and start everything up if attached startup_check(); } on_rez(integer p) { if (llGetAttached() == 0) { llOwnerSay("I must be attached to operate - please wear me"); } } changed(integer change) { if (change & CHANGED_OWNER) { llResetScript(); } } attach(key id) { // Turn off check when not attached if (id == NULL_KEY) { llSetTimerEvent(0.0); llOwnerSay("Detached - regular check has been cancelled"); } else { startup(); } } timer() { twitter_send("check", ""); } touch_start(integer n) { // On owner touch, launch a control menu if (llDetectedKey(0) != llGetOwner()) return; menu(); } listen(integer c, string name, key id, string msg) { if (msg == "Check Now") { llOwnerSay("Checking latest entries..."); gManual = TRUE; twitter_send("check", ""); llSetTimerEvent(CHECK_INTERVAL); } else if (msg == "List Feeds") { list_feeds(); } else if (msg == "TestTwit") { llSleep(1.0); twitterball(); } else if (msg == "Web") { llLoadURL(id, "Visit your own Twitter page", "http://twitter.com/" + gScreenName); } else if (msg == "Show Mine") { llOwnerSay("Showing your tweets"); gShowMine = TRUE; } else if (msg == "Hide Mine") { llOwnerSay("Hiding your tweets"); gShowMine = FALSE; } else if (msg == "Help") { about(); } else if (msg == "Notify") { llDialog(llGetOwner(), "Please select an option for notifications - currently " + notify_level(), ["Quiet", "Private", "Public", "Cancel"], 282); } else if (msg == "Quiet") { gNotify = 0; llOwnerSay("No sound or animation notifications"); } else if (msg == "Private") { gNotify = 1; llOwnerSay("Private sound only for notifications"); } else if (msg == "Public") { gNotify = 2; llOwnerSay("Animations and public sound when twittering, private sound for new tweets"); } else if (msg == "Recent") { list_recent(); } else if (msg == "Repeat") { llOwnerSay("Repeating your last update..."); twitter_send("update", gLastTweet); } else if (msg == "Allow >140") { llOwnerSay("Allowing posts over 140 characters"); gBlock140 = FALSE; } else if (msg == "Block >140") { llOwnerSay("Blocking posts over 140 characters"); gBlock140 = TRUE; } else if (msg != "Cancel") { llOwnerSay("Twittering..."); gLastTweet = msg; twitter_send("update", msg); } } http_response(key id, integer status, list metadata, string body) { if (llGetSubString(body, 0, 1) == "OK") { // A success list lines = llParseString2List(body, ["\n"], []); string firstLine = llList2String(lines, 0); if (llGetListLength(lines) == 1) { // A successful update, or an ID check list bits = llParseString2List(firstLine, [","], []); gCode = llList2Integer(bits, 1); gScreenName = llList2String(bits, 2); if (llList2String(bits, 3) == "posted") { llOwnerSay("Successfully Twittered!"); if (gNotify == 1) { llPlaySound("c923f3d9-83a6-99dc-9b7d-bbcdb3c30789", 1.0); } else if (gNotify == 2) { twitterball(); } } } else { integer f = 1; integer maxTime = gTime; string tweet = ""; do { string user = llList2String(lines, f); integer time = llList2Integer(lines, f + 2); // llOwnerSay("gTime = " + (string)gTime); // llOwnerSay(user + " @ " + (string)time); if ((gShowMine || user != gScreenName) && time > gTime) { if (maxTime == gTime) { // First new tweet, play sound if (gNotify != 0) { llPlaySound("c923f3d9-83a6-99dc-9b7d-bbcdb3c30789", 1.0); } } tweet = llList2String(lines, f) + ": " + llList2String(lines, f + 1); llOwnerSay(tweet); gRecent = push(tweet, gRecent); maxTime = time; } f += 3; } while (f < llGetListLength(lines)); if (maxTime == gTime) { if (gManual) { llOwnerSay("No new entries"); gManual = FALSE; } } else { gTime = maxTime; } } } else { if (body == "") { // SL sends back a blank entry if it can't get in touch at all body = "Could not contact intermediary server " + TWITTERPING; } else { if (status == 200) { // Intermediary server worked okay llOwnerSay("I received an error - '" + body + "'"); } else { // Something else went wrong, don't bother the user with the exact details // If they are techie enough to know what the details mean, they can edit // this script so that it tells them :) llOwnerSay("I received an error with code " + (string)status + " - please try again in a bit"); } } } } }

Twitterbox control.php

Code summary: 

This is the latest version of the server PHP script for use with the Twitterbox, which does all of the hard work in contacting Twitter.

"Twitterbox", "X-Twitter-Client-URL" => "http://ordinalmalaprop.com/twitter/", "X-Twitter-Client-Version" => VERSION ); curl_setopt($c, CURLOPT_HTTPHEADER, $headers); // Now send the response off $response = @curl_exec($c); if (chop($response)) { // We got a non-blank response, so try to parse it as JSON $json = json_decode($response, TRUE); curl_close($c); // print_r($json); // Return the parsed data return $json; } else die("No response from Twitter!"); } // Fucntion to get the TinyURL of the first entry in an RSS feed function feedlink_get($url) { $c = curl_init(); curl_setopt($c, CURLOPT_URL, $url); curl_setopt($c, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($c, CURLOPT_HEADER, FALSE); curl_setopt($c, CURLOPT_TIMEOUT, 15); $feed_data = curl_exec($c); curl_close($c); if (!$feed_data) die("Failed to get stream at $url"); else { // Try RSS parsing if (strpos($feed_data, 'rss') !== FALSE && preg_match('/.+?\s*?(\S.+?\S)\s*?<\/link>/is', $feed_data, $matches)) { $feed_data = get_tinyurl($matches[1]); return $feed_data; } // Try Atom parsing else if (strpos($feed_data, '.+?/is', $feed_data, $matches)) { $feed_data = get_tinyurl($matches[1]); return $feed_data; } // Give up else { die("Failed to find new feed link at $url"); } } } // Function to get a tinyurl for something // We probably don't need to use curl here, but, you know, why not? function get_tinyurl($bigurl) { $c = curl_init(); curl_setopt($c, CURLOPT_URL, "http://tinyurl.com/api-create.php?url=$bigurl"); curl_setopt($c, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($c, CURLOPT_HEADER, FALSE); // Don't wait more than 10 seconds for a TinyURL curl_setopt($c, CURLOPT_CONNECTTIMEOUT, 10); $tinyurl = curl_exec($c); curl_close($c); // If we got a tinyurl, return it if ($tinyurl) return $tinyurl; // otherwise just return the original one - Twitter will probably be able to tinyurl it else return $bigurl; } //----------------------------------------------------------------------------- // Main code // Set timezone to be closest to SL time (no San_Francisco timezone code in PHP?) date_default_timezone_set("America/Los_Angeles"); header("Content-type: text/plain"); // Read POST input $post = file('php://input'); // If there isn't anything in the POST, don't do anything if (!$post) { die("go away"); } // Read in the information that was sent $user = urldecode(chop($post[0])); $pass = urldecode(chop($post[1])); $action = urldecode(chop($post[2])); $status = urldecode(chop($post[3])); // location to add slurl for, in the form simname/x/y/z/ $location = urldecode(chop($post[4])); // Text representation of location $loc_array = explode("/", $location); $loc_text = $loc_array[0]. " ". $loc_array[1]. ",". $loc_array[2]. ",". $loc_array[3]; // Keywords and RSS feeds to check and replace $feeds_raw = explode(",", urldecode(chop($post[5]))); $feeds = array(); $f = 0; do { $feeds[$feeds_raw[$f]] = $feeds_raw[$f+1]; $f += 2; } while ($f < sizeof($feeds_raw)); // Unix time of last check - don't return anything before this $since = urldecode(chop($post[6])); ob_flush(); flush(); // Allow for slight clock irregularities $now = time() - 5; // Now process the action requested // All of these request feedback from Twitter in JSON format, which is parsed by the above function. if ($action == "update") { if (strlen($status) > 0) { // "Insert current position" keyword $status = str_replace("SLPOS", $loc_text, $status); // Go through each feed keyword checking to see if it occurs foreach ($feeds as $feed_name => $feed_url) { // Do we have any keywords for that feed here? if (@strpos($status, $feed_name) !== FALSE) { // If so, get the tinyurl for the first element there $tinyurl = feedlink_get($feed_url); // and replace instances of that keyword with it $status = str_replace($feed_name, $tinyurl. " ", $status); ob_flush(); flush(); } } if (strpos($status, "SLURL") !== FALSE && $location) { $loc_array = explode("/", $location); $loc_text = $loc_array[0]. " ". $loc_array[1]. ",". $loc_array[2]. ",". $loc_array[3]; // SLurl page strips out all HTML in the msg - didn't know that... $slurl = "http://slurl.com/secondlife/". str_replace(" ", "%20", $location). "?x=200&y=260&title=". urlencode(gmdate("j M y @ H:i", time() - 28800). " SLT"). "&msg=". rawurlencode(htmlentities(str_replace("SLURL", $loc_text, $status))). "&img=http%3A//ordinalmalaprop.com/twitter/twitterbox-credit.jpg"; $tiny_slurl = get_tinyurl($slurl); $status = str_replace("SLURL", $tiny_slurl, $status); } ob_flush(); flush(); } $json = twitter_send('update', $user, $pass, "status=". rawurlencode($status)); // The update procedure // Check the time that the last update was posted at $created = strtotime($json["created_at"]); // If the time is before now, that means it's failed! // 2007-07-01 - this doesn't seem to work any more, assume it has succeeded // if ($created < $now) { // die("Date was w"); // } // Otherwise say "ok" and coincidentally update the screen name and ID if (sizeof($json, COUNT_RECURSIVE) <= 1) die("Twitter gave me bad output - " + print_r($json, TRUE)); else { echo "OK,". $json["user"]["id"]. ",". $json["user"]["screen_name"]. ",posted"; } } else if ($action == "get id" && ($json = twitter_send('update', $user, $pass, ""))) { // To just update the screen name and ID, send a blank update which will not be displayed // and read in the information. if (sizeof($json, COUNT_RECURSIVE) <= 1) die("Twitter gave me bad output - " + print_r($json, TRUE)); else echo "OK,". $json["user"]["id"]. ",". $json["user"]["screen_name"]; } else if ($action == "check" && ($json = twitter_send('friends_timeline', $user, $pass, ""))) { // Check recent tweets // Go through them and compile a list of the last few, up to an appropriate size limit to send back to LSL // 1500 bytes should work - longer than that and the header gets in the way // LSL doesn't like receiving any real quantity of data $length = 0; $entry = ""; $f = 0; // Sometimes, Twitter doesn't seem to produce proper output, even if it gets through if (sizeof($json, COUNT_RECURSIVE) <= 1) die("Twitter gave me bad output"); $max = sizeof($json); $tweets = array(); do { $length += strlen($entry); if ($entry != "") $tweets[] = $entry; // Format for each tweet: // // () // $unixtime = strtotime($json[$f]["created_at"]); $entry = $json[$f]["user"]["screen_name"]. "\n". $json[$f]["text"]. " (". date("j M H:i", $unixtime). " SLT)\n$unixtime"; ob_flush(); flush(); } while (++$f < $max && $length + strlen($entry) < 1500); // Put them in chronological order $tweets = array_reverse($tweets); // and output echo "OK\n". implode("\n", $tweets); } else echo "pardon?"; ?>

An Absolute Outrage

I flatter myself to think that I am at least known as someone of mostly Phlegmatic Temperament, even when peculiarities of Second Life are concerned. I am, I believe, reasonable tolerant of Flaws, even if I will occasionally make squawks of protest; these are mostly due to surprise.

There are some things, however, up with which I will not put.

NO NO NO

"Twitterbox say"?

Scatterbrained Once Again

A perpetual problem of mine is that I am unable to concentrate on one thing for more than about, oh, an afternoon, leading to the unfortunate situation where possible projects and Matters Upon Which To Write flare brightly and then burn out within moments, like phosphorus sparrows. No sooner do I plan to write a Definitive Article upon some subject, which must be Gotten Right, than some new shiny object appears and drives the original one completely away from my attention.

The result of this is that I am rather poor at actually writing anything, as perfectionism restrains me from making an entry on something incomplete, yet I do not have the urge to complete it, and it is only when I reach a level of self-disgust at this that I throw out whatever peculiar half-finished objects that remain in my mind into this Journal, often in the form of Bullet Points. I believe that it was easier in my earlier days, when I was rarely thinking about more than three things at one time.

Enough of this preambling though; I will do my best to clear the decks. The subject of this entry is something that I call the Twitterbomb, though really it isn't much of a bomb, unless one considers very slow-moving fragments that are in any case phantom to be dangerous. This was actually mentioned previously by Nick Wilson writing in Metaversed as he is a proper journalist who investigates things and writes about them, whereas I am no such thing and do not even have a picture of my own creation at the time of writing.

The Twitterbomb, as Mr Wilson says and as I mention in the comment section there, is a device for the Visualisation of timed and differently-authored data; the Twitter friends timeline is the easiest to work with, as it collects data into a single feed, but I suppose RSS feeds and such could be used. There is a central "bomb", which reads in Twitter data via a proxy (a stripped-down version of the Twitterbox one) throws out differently-coloured "fragments" - the angle of movement and colour of these fragments is individual to each different author, thus each person's output is represented by a line of tweets stretching out from the centre. Each fragment's size is proportional to the number of letters in the tweet.

The fragments move outwards at a constant speed, with their distance from the bomb being a function of their age. There is a configurable maximum age, with fragments disappearing once they reach this. (Once rezzed, the fragments are independent, and with a busy friends timeline this could result in an awful lot of prims being around, thus best to use this in a fairly empty lot.)

I confess to not having a specific target to achieve here, but it is a toy that enables one to play with the possibilities of visualising data in Three Dimensional Form. One can look at the fragments produced and see, say, how active an individual is and their "rhythm" by observing the "clumping" and size of clumps produced; a wordy but regular poster will have fat fragments evenly spaced, someone who posts in bursts of short pieces will have thin lines separated by empty space and so on. A particularly significant event for one's friends will be marked by a "shell" of tweets all appearing at the same time and expanding outwards. Changes could certainly be made to the way the fragments are emitted to test different concepts of visualisation.

I say all this, but actually, in practice I have Given Up on the Twitterbomb for the moment - which is likely to mean forever - since Twitter has taken to caching my requests and not giving me recent updates reliably at all. This is odd, since the Twitterbox seems to be working perfectly well, or at least is when both Twitter and SL are working at the same time (a combination of reliabilities that one is not advised to bet one's life or significant bodily organs upon). With this in mind, once I am able to return to the world I shall be sure to post the relevant Code here.

Well, that is over with in any case. Next, something else, I believe.

The Twitterbadge

Before I dive back into acts of proper engineering within SL (usually located at <0,0,0>) I would quickly like to note one more Twitter-related product - the Twitterbadge.

Twitterbox 0.4

For what it is worth, I have finally gotten around to releasing the latest version of the Twitterbox, which includes a number of new features and is generally a bit more reliable. Of course, at this time the Grid is... well, it is a Wednesday.

If you wish to view a list of changes from the previous version, by all means read on, and do excuse the scrappiness of my list-making.

Now, hopefully, I might be able to work on, you know, a thing, rather than a succession of ephemeral seance devices. Something which exists in its own right, and allows one to do something upon the Grid itself which one previously could not. Alas, the fractious nature of reality is making this just a tad tricky, with some of my best works now appearing foolish and error-ridden, but the time will come I'm sure when events do not just occur in random orders.

An Unsatisfying Morsel

~ Herein, one may find a few notes dashed off in a hasty hand whilst standing in a damp Caledon drizzle, repeatedly poking a riveted cube.

A Brief Moment of TwitterHeading

Of little interest to those not inclined to develop Twittery Things, but proposals have emerged for those creating Clients (such as the Twitterbox) to be able to identify their Device to the Twitterverse.

Twitterbox 0.3

Oh my giddy aunt, I forgot entirely to announce the release of version 0.3 of the Twitterbox before retiring the previous night.

Well, version 0.3 of the Twitterbox has been released. It is fairly similar to the previous version, as one might expect, with the addition of the ability to automatically post the URL of the latest entry in an "RSS" or "Atom" "feed".

Syndicate content