• strict warning: Non-static method view::load() should not be called statically in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/views.module on line 906.
  • strict warning: Declaration of views_handler_argument::init() should be compatible with views_handler::init(&$view, $options) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/handlers/views_handler_argument.inc on line 744.
  • strict warning: Declaration of views_handler_filter::options_validate() should be compatible with views_handler::options_validate($form, &$form_state) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/handlers/views_handler_filter.inc on line 607.
  • strict warning: Declaration of views_handler_filter::options_submit() should be compatible with views_handler::options_submit($form, &$form_state) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/handlers/views_handler_filter.inc on line 607.
  • strict warning: Declaration of views_handler_filter_boolean_operator::value_validate() should be compatible with views_handler_filter::value_validate($form, &$form_state) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/handlers/views_handler_filter_boolean_operator.inc on line 159.
  • strict warning: Declaration of views_plugin_style_default::options() should be compatible with views_object::options() in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/plugins/views_plugin_style_default.inc on line 24.
  • strict warning: Declaration of views_plugin_row::options_validate() should be compatible with views_plugin::options_validate(&$form, &$form_state) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/plugins/views_plugin_row.inc on line 134.
  • strict warning: Declaration of views_plugin_row::options_submit() should be compatible with views_plugin::options_submit(&$form, &$form_state) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/plugins/views_plugin_row.inc on line 134.
  • GeSHi library error: sites/all/modules/geshifilter/geshi is not a directory.
  • GeSHi library error: sites/all/modules/geshifilter/geshi is not a directory.
  • GeSHi library error: sites/all/modules/geshifilter/geshi is not a directory.
  • strict warning: Non-static method view::load() should not be called statically in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/views.module on line 906.
  • strict warning: Declaration of views_handler_field_comment::init() should be compatible with views_handler_field::init(&$view, $options) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/modules/comment/views_handler_field_comment.inc on line 49.
  • strict warning: Declaration of views_handler_filter_node_status::operator_form() should be compatible with views_handler_filter::operator_form(&$form, &$form_state) in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/modules/node/views_handler_filter_node_status.inc on line 13.
  • strict warning: Non-static method view::load() should not be called statically in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/views.module on line 906.
  • strict warning: Non-static method view::load() should not be called statically in /home/ordinal/ordinalmalaprop.com/engine/sites/all/modules/views/views.module on line 906.

April 2009

Slight Return; thoughts on a Lightweight Interface to Drupal

I wrote previously that I had done some reasonably considerable work integrating Drupal with Second Life, which of course means integrating the Drupal System with the Linden Scripting Language.

LSL has the llHTTPRequest function, of course, by which one can send information to Drupal (or indeed any other system on the Wider Aethernet). The main issues that I have encountered, though, are:

  • The sum of data to be sent and received. A typical Drupal page is really quite enormous and unsuitable for the standard LSL limit of 4096 characters. Furthermore, many existing interfaces to web services, involving the XML or JSON formats, are unsuitable for use with LSL, which really takes an age to do any sort of String Processing. Both request and response need to be of very short formats.

  • The problem of authenticating who, indeed, the person sending the request is. Sending the registered Drupal username and password hash is quite a simple thing to do, but in most cases, this is unsuitable for use with LSL. People do not wish to have to enter usernames and passwords into scripts or notecards unless they absolutely cannot help it.

    In the past, with Rezzable's "Avatar Linking" system, I solved this by giving the resident concerned the ability to create a Drupal user account for themselves if they did not already have one, automatically from the grid, with the entry of a few details such as their Email Address; the name and key of the resident was passed along with this, and stored in Drupal. Other Scripted Gadgets which they might use in future then authenticate themselves with some sort of Master Password, and pass the key of the operating avatar along with other data. The Drupal system looks the appropriate user account up by matching the key to stored and confirmed ones, and carries out operations for that user - for instance, leaving comments on the node for a particular Artwork.

Both of these are common issues across all sorts of Systems for which one would wish to use Drupal, and it occurs to me that a Common Framework would be useful in this instance. I am moved to create such a framework for my personal and professional use, and also the use of anyone else who might be interested.

The most important features of this framework (which I have been calling "Slight" - SecondLife Lightweight) as I seem them are

  1. that it can receive data easily, pass them on to other modules for use, receive responses from those modules, and return the responses to an LSL script in a format that can be quickly processed and understood;

  2. that it has the ability to authenticate users and pass the details of the authenticated user on to sundry other Drupal systems. This authentication could be performed by the main module itself as well as other, optional ones which folk might choose to write.

Those familiar with the Drupal Beast will be aware of the concept of a "hook". Invoking a hook allows other modules which are aware of it to perform actions on data passed along with that hook. I am thinking along the lines of having two hook invocations here - firstly, one for authentication, which passes along the data sent and allows other modules to modify a user object. Then, one for the action to be performed, which allows modules to perform actions on the basis of those data and the authenticated user, and add responses, which will be passed back to the LSL script to be delivered to the resident.

Perhaps an example might be of use here. Say one wishes to have a system which allows the creation of "blog" entries via LSL. The LSL script would take the key of the avatar (this is unforgeable) and combine it with a hardcoded password to produce a hash. It would send this to the Slight interface - which possesses a specific URL, say http://ordinalmalaprop.com/engine/interface/slight/ - along with the avatar's key in plain text, and the actual text of the blog entry. Our Drupal administrator here has three relevant modules installed - the Slight framework module, a Slight Second Life authentication module, and a Slight blog entry module.

Slight would initially believe this entry to be unauthenticated, as there would be no information as to the Drupal username and password. It would, however, invoke a hook for authentication with a missing user object, giving other modules the opportunity to authenticate whoever it was. "Aha!" the Slight Second Life authentication module says, "this person has sent a hash of key and master password which I recognise - I will modify the user object to correspond to the user whose avatar has this key". And it proceeds to do so.

Upon receiving the results of the invocation of the authentication hook, Slight then sends out another, "action" hook, with the user object that has resulted from the authentication process. The Slight blog entry module at this point says "aha! I see that there is an authenticated user here" (this user having been provided by the Slight Second Life authentication module) "and that the command is to add a blog entry. I will do such a thing, and, if it works, send them back a message saying that it has been posted, and giving them the URL of the new entry".

Slight takes the results of what has happened after the action hook invocation, and passes anything appropriate back to the requester. This might be highly technical data for use by a script, or it might (in this case) be a simple message to be sent to the owner via llOwnerSay or some such. Whatever it is, it sends it back. The LSL blogging widget script takes this response and, here, sends the confirmation to the resident, who no doubt proceeds directly to the page concerned to see which words they have misspelled.

This sounds like a rather involved process but in practice, the time required would be tiny fractions of a second for all.

As a note, Slight would not be simply of use for Second Life. Since it would be simply receiving GET parameters and sending a plain text response, it could be used by any system.

One significant use that I believe would be very handy is for use in Opensim, which currently (as far as I am aware) has no particular permission system set up. Upon login, the Opensim server itself could request the current role of an account linked to an avatar from a Drupal system, for use in determining whether that avatar could enter parcels or regions or change settings or whatnot. Changing an avatar's access would then be as simple as ticking a few boxes in a form that was produced by the appropriate module. This is in fact something which I hope to implement in practice, and as soon as such as thing is ready, I shall mention it on the appropriate site. (To be fair, if I have been working on things in Company Time, details of them really should appear on Company Blogs, though Slight as I envisage it will certainly be open source and so on.)

I would be interested to read any Comments or Criticisms that anyone might have regarding this Proposed Framework, so please, if you have any, by all means leave them here. I shall post here once I have the actual Code for the system in any Working State, which should not take very long to do.

Rampant Traffic Bottery to be Eliminated? Perhaps, a Little.

The figure in the foreground is a notorious and exploitative Bot, and requires Immediate Removal from Everything. Do not trust it!

Almost everyone agrees that using Bots to manipulate traffic (and therefore Search rankings) is unfair. Not only with respect to Search itself but also due to the load on Mainland Region resources and how that can impact other Residents in the area.

Therefore we are setting policy that attempting to gain an unfair Search advantage, by the use of Bots to inflate the Traffic for a parcel, will be considered a violation. This policy applies to both Mainland and Private Estates as both are represented in Search. (Conclusion to the Blog Post on Bots)

And few would argue with that. (Although the few, pretty much exclusively those selling bots and using bots, do sometimes seem to be attempting to make up for their fewness by repetition.) I would say that this was an extremely Welcome Move; I am having great difficulty in constructing credible arguments opposing my "Trafficbots Are A Bad Thing" position that I might address in the form of Dialogue. Traffic is meant to be an indication of popularity. Populating an area by artificial means makes this statistic, one of the few we have access to, meaningless.

In general I certainly approve of the attitude - previously displayed in the context of Advertisement Farms - that it is the behaviour that matters, not some breach of arbitrary technical rules. Are you attempting to distort traffic statistics by the use of automated facilities? Then you should be prevented from doing so. No, there will not (one would hope) be a series of technical Rules, the Letter of which will be obeyed and which can thus be ingeniously bypassed - Motivation is the key.

I can, though, see that this particular Announcement is not really complete. Certainly it should eliminate those dismal boxes of twenty or thirty homonculi, locked up in a cube at four thousand metres in height. However, the continued presence of Traffic as a statistic will lead to other methods of Gaming becoming more popular, if unchallenged.

Camping, for instance: offering camping chairs, which are then occupied by bots belonging to somebody else, is a symbiotic relationship between landowner and bot operator. In fact, if said chairs are occupied by residents, said residents are effectively becoming trafficbot subcontractors - "crowdsourcing traffic distortion" if you will. Whether or not bots are being used is fairly unimportant, and a campsite owner could certainly claim that they were not using bots, in the knowledge that by putting out large numbers of chairs and paying money to those sitting on them, some bot operator would shortly be along.

Still, there are other hopeful signs:

Going forward we are going to look at ways to allow you to voluntarily identify to us that an account is a Bot, so that we can remove it from Traffic completely.


We will continue to strive toward providing more statistical data to land owners, including the number of visitors they receive.

Excellent! And extremely overdue! "Yesterday" would be a good time for this to happen, I would say.

However, the way these statistics relate to Search ranking will be changing. In the next few months, we will be making both technical and policy changes to the way relevance and ranking works in Search. The "traffic" score will be only one aspect of the ranking logic, and it will be scrubbed and weighted to account for gaming vectors.

Potentially a Good Show depending on the details! Although I thought that this was supposed to already have happened.

In any case, now I believe I shall return to the company of my new-found friends Ngr, Eops and Sfdjk. There is after all the chance of winning a Linden Dollar every four minutes, you know.

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?"; ?>

A Movement Drupalwards

Not, you understand, that there is anything wrong with the popular Journalling System, "Wordpress", but I have for the time being left it alone and moved the Engine Fit For My Proceeding And Apparently Fit For Yours Since You Do Seem To Be Reading It to work using that hive of open-source Communism, "Drupal".

The reasons for this are fairly straightforward, and I shall list them briefly here.

  1. I wished to see whether I could, whilst leaving all of the links to my Previous Journalings intact, and also without interfering with the existing Appearance and Function. (Apparently I could, as far as I can tell, anyway. If something appears Wrong please do inform me as soon as possible.)

  2. I poke and tinker with the DrupalThing on a professional basis very regularly, a circumstance which results in a pleasing Symbiosis. My professional poking and tinkering experience allows me to rapidly and effectively poke and tinker with this system personally, and the more that I interfere with it on a personal basis, the greater my Skill and the greater the benefit to my Employers; thus a Smile is brought to the faces of all parties concerned, and in these trying times, heaven knows the more Smiles that exist in the world, the Better.

  3. Keen observers will have noticed that I do, on occasion, refer to Scripts and Other Code on this Journal. The WordpressWotsit is not particularly good for storing the Inner Workings of said items, being mostly designed to keep track of Posts and Pages. Previously, I have been storing Scripts upon a Wiki, which is a rather crude solution. The DrupalThing, on the other hand, has the benefit of allowing one to create arbitrary "content types" with different fields and Display Options, and also take advantage of types generously provided by the Community; to this end I have utilised the "geshinode" module to allow me to add specific "source code" entries, and I have further tinkered with bits and pieces to have these entries wider than usual and with an area that allows me to easily link them to explanatory journal entries. As well as this, the "views" module allows me to simply generate a categorised list of them, which you, dear reader, may see by visiting the "Scripts" link at the top of the page.

  4. In general, really, the DrupalThing is more easily Configurable and Extensible. Some readers may be aware that my current main Employer is the firm "Rezzable" and I have quite some history of creating various Interactions between their Drupal Site and the Second Life Grid - and, I might add, other grids, if that is not too bizarre a concept. Whilst I have no current plans to do anything along those lines here, the possibilities exist. For instance I may well begin to list my own Products here as well, store documentation for them, that sort of thing.

I could not, in all Honesty, recommend the DrupalThing for the typical Writer of Sequential Prose - in most cases Wordpress is a far better solution. It does however suit my Tinkering Instincts and thus I have engaged in this Movement. I would hope that nobody is Inconvenienced at all and, in fact, that the General Utility of my Witterings is Greatly Enhanced by this process.

Forward Avatar Intersect

Code summary: 

Detects avatars whose bounding box intersects with a straight line.

// Forward sector avatar intersect // Ordinal Malaprop // 2006-09-22 // Should report the first avatar whose bounding box "sort of" // intersects the forward vector of the toucher, adjusted for eye level. // An assumption is made for simplicity that the box is actually a // cylinder; this won't make any real difference in practice. default { touch_start(integer n) { llSensor("", NULL_KEY, AGENT, 96.0, PI_BY_TWO); } sensor(integer n) { // Adjust position for eye level vector mypos = llGetPos() + <0.0, 0.0, 0.75>; // Current forward

Semaphore Instructions


The Semaphore Animation Device is designed to assist you in sending semaphore signals, by automatically translating plain speech into the appropriate gestures.

Basic Operation

Attach the HUD device to an appropriate HUD point (I suggest the top left). It will indicate whether it is Active, or Disabled. Touching the main device toggles this status. While Disabled, the device will not affect you in any way.

About Me

This Aethernet Journal is a record of the experiences of Ordinal Malaprop in the realm of Second Life as a technologist, natural philosopher, mathematrix and occasional purchaser of shoes.

Mini Gun Balloon
Ordinal is confronted with an obtrusive invention

Ms Malaprop is a resident of the isle of Caledon, where she maintains the original Caledon Lighthouse, though it must be said that other people have now built lighthouses considerably better than hers. Still. Let us not be downhearted.

Ordinal is generally found to be delving into the somewhat esoteric workings of the Second Life universe, at least in terms of scripting objects to make odd noises or explode.

As a reader of this humble journal you will sometimes see reference made to free devices and possibly "the freebie box". Unless otherwise specified, if Ordinal has finished with a project she will place it into this device (to be found at Ordinal Enterprises at the Lighthouse in Caledon) from where it may be taken and examined by any interested party. Since the issue of most concern to Ordinal is scripting and the examination thereof, these items will be modifiable and, usually, distributed with full permissions, so that one may open them up and see how they work.

If perhaps you have stumbled upon this journal by some accident, and have no knowledge of Second Life, do feel free to join, as it is, indeed, free to enter the world, and as of the time of writing does not require even a credit card number or any such thing. (Though one might find it advisable to spend a small sum in order to obtain a little currency within the world.) You might also find it useful to examine some of the links on the side of the main page, to obtain broader knowledge.

It should of course be noted, particularly by any ladies or gentlemen of the legal profession, that Second Life is a trademark of Linden Labs and not anything to do with the author, and all opinions expressed herein are those of the author and not zzzzzzzzzzzzzzzz

In case there was any interest in the matter, Ordinal's skin and hair are by the highly talented Robin Sojourner, and her clothes are mostly from the silentsparrow emporium.

An Inventory Permissions Checking Script

I was reminded recently of a script that I wrote quite some time ago and appear not to have published anywhere. I did mean to. Perhaps I did, and have simply forgotten. In any case, it can be found here:

The task that I use it to accomplish is to check the permissions of the contents of an item before sale or distribution.