Difference between revisions of "Twitter OAuth Library"

From Second Life Wiki
Jump to navigation Jump to search
Line 23: Line 23:
integer TWITTER_OAUTH_UPDATE_STATUS = 999003;
integer TWITTER_OAUTH_UPDATE_STATUS = 999003;


Init()
TwitterOAuthInit()
{
{
     // Set up Twitter OAuth library, using application consumer key and secret generated by Twitter.
     // Set up Twitter OAuth library, using application consumer key and secret generated by Twitter.
Line 31: Line 31:
}
}


string GetSLURL()
string TwitterOAuthGetSLURL()
{
{
     string globe = "http://slurl.com/secondlife";
     string globe = "http://slurl.com/secondlife";
Line 42: Line 42:
}
}


string BuildMessage()
string TwitterOAuthBuildMessage()
{
{
     // Example message, change this as appropriate.
     // Example message, change this as appropriate.
     return "I did something amazing in Second Life here " + GetSLURL();
     return "I did something amazing in Second Life here " + TwitterOAuthGetSLURL();
}
 
void TwitterOAuthUpdateStatus(string message, key avatar)
{
    llMessageLinked(LINK_THIS, TWITTER_OAUTH_UPDATE_STATUS, message, avatar);
}
}


Line 52: Line 57:
     state_entry()
     state_entry()
     {
     {
         Init();
         TwitterOAuthInit();
     }
     }
      
      
     on_rez(integer param)
     on_rez(integer param)
     {
     {
         Init();
         TwitterOAuthInit();
     }
     }
      
      
     touch_start(integer total_number)
     touch_start(integer total_number)
     {
     {
         key toucher = llDetectedKey(0);
         TwitterOAuthUpdateStatus(TwitterOAuthBuildMessage(), llDetectedKey(0));  
        llMessageLinked(LINK_THIS, TWITTER_OAUTH_UPDATE_STATUS, BuildMessage(), toucher);
     }
     }
}
}

Revision as of 06:09, 22 February 2010

Twitter OAuth Example Client

<lsl> ////////////////////////////////////////////////////////////////////////////////////// // // Twitter OAuth Client 1.0: An example client that uses the LSL OAuth 1.0a // Library for Twitter by Babbage Linden. // // Released under the Creative Commons Creative Commons Attribution-Share Alike 3.0 // license http://creativecommons.org/licenses/by-sa/3.0/ // //////////////////////////////////////////////////////////////////////////////////////

// Application constants generated by Twitter. // Set up a new Twitter application here: http://twitter.com/oauth_clients string TWITTER_OAUTH_CONSUMER_KEY = "YOUR CONSUMER KEY HERE"; string TWITTER_OAUTH_CONSUMER_SECRET = "YOUR CONSUMER SECRET HERE";

// Message constants defined by Twitter OAuth library. integer TWITTER_OAUTH_SET_CONSUMER_KEY = 999000; integer TWITTER_OAUTH_SET_CONSUMER_SECRET = 999001; integer TWITTER_OAUTH_SET_MAX_RETRIES = 999002; integer TWITTER_OAUTH_UPDATE_STATUS = 999003;

TwitterOAuthInit() {

   // Set up Twitter OAuth library, using application consumer key and secret generated by Twitter.
   llMessageLinked(LINK_THIS, TWITTER_OAUTH_SET_CONSUMER_KEY, TWITTER_OAUTH_CONSUMER_KEY, NULL_KEY);
   llMessageLinked(LINK_THIS, TWITTER_OAUTH_SET_CONSUMER_SECRET, TWITTER_OAUTH_CONSUMER_SECRET, NULL_KEY);
   llMessageLinked(LINK_THIS, TWITTER_OAUTH_SET_MAX_RETRIES, "10", NULL_KEY);

}

string TwitterOAuthGetSLURL() {

   string globe = "http://slurl.com/secondlife";
   string region = llGetRegionName();
   vector pos = llGetPos();
   string posx = (string) llRound(pos.x);
   string posy = (string) llRound(pos.y);
   string posz = (string) llRound(pos.z);
   return globe + "/" + llEscapeURL(region) +"/" + posx + "/" + posy + "/" + posz;

}

string TwitterOAuthBuildMessage() {

   // Example message, change this as appropriate.
   return "I did something amazing in Second Life here " + TwitterOAuthGetSLURL();

}

void TwitterOAuthUpdateStatus(string message, key avatar) {

   llMessageLinked(LINK_THIS, TWITTER_OAUTH_UPDATE_STATUS, message, avatar);

}

default {

   state_entry()
   {
       TwitterOAuthInit();
   }
   
   on_rez(integer param)
   {
       TwitterOAuthInit();
   }
   
   touch_start(integer total_number)
   {
       TwitterOAuthUpdateStatus(TwitterOAuthBuildMessage(), llDetectedKey(0));   
   }

}

</lsl>

Twitter OAuth Library

<lsl> ////////////////////////////////////////////////////////////////////////////////////// // // Twitter OAuth Lib 1.0: An LSL OAuth 1.0a Library for Twitter by Babbage Linden. // Built with Cale Flanagan's LSL HMAC-SHA1 implementation and Strife Onizuka's // LGPL Combined Library and with help from Latif Khalifa. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public License // as published by the Free Software Foundation; // version 3 of the License. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with this library. If not, see <http://www.gnu.org/licenses/> // or write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, // Boston, MA 02111-1307 USA // //////////////////////////////////////////////////////////////////////////////////////

// Library protocol constants. integer TWITTER_OAUTH_SET_CONSUMER_KEY = 999000; integer TWITTER_OAUTH_SET_CONSUMER_SECRET = 999001; integer TWITTER_OAUTH_SET_MAX_RETRIES = 999002; integer TWITTER_OAUTH_UPDATE_STATUS = 999003;

// Library state. string gConsumerKey = ""; string gConsumerSecret = ""; integer gMaxRetries = 10;

// Request state. key gAvatarKey; string gMessage; key gRequestTokenKey; string gRequestToken; string gRequestTokenSecret; key gAccessTokenKey; string gVerifier; string gAuthorizeUrl = "oob"; integer gRetries;

string TrimRight(string src, string chrs)//LSLEditor Unsafe, LSL Safe {

   integer i = llStringLength(src);
   do ; while(~llSubStringIndex(chrs, llGetSubString(src, i = ~-i, i)) && i);
   return llDeleteSubString(src, -~(i), 0x7FFFFFF0);

}

string WriteBase64Integer(string data, integer index, integer value) {

   integer S = 12 - ((index % 3) << 1);
   return  llDeleteSubString(
       llInsertString(
       data,
       index = ((index << 4) / 3),     
           llInsertString(
               llIntegerToBase64(
                   (llBase64ToInteger(llGetSubString((data = llGetSubString(data, index, index+7)) + "AAAAAA", 0, 5)) & (0xFFF00000 << S)) | 
                   ((value >> (12 - S)) & ~(0xFFF00000 << S))
               ), 2, 
               llIntegerToBase64(
                   (llBase64ToInteger(llGetSubString((llDeleteSubString(data, 0, 1)) + "AAAAAA", 0, 5)) & ~(0xFFFFFFFF << S)) | 
                   (value << S)
       )    )    ), index+7, index + 22);//insert it then remove the old and the extra.

}

string DwordListToBase64(list a) {

   integer len = (a != []);
   integer i = -1;
   string out;
   while((i = -~i) < len)
       out = WriteBase64Integer(out, i, llList2Integer(a, i));
   return TrimRight(out,"A");

}

// Takes a dwordblock, adds a string and puts it padded for blocksize into a dwordlist // returns sha1blocks list PrepareShortkey(string s) {

   integer v = 0;
   integer cnt = llStringLength(s);
   integer n = cnt;
   list dw_skey;
       
   for (n = 0; n < cnt; n++)
   {
       v = v | 0xFF & llBase64ToInteger("AAAA" + llStringToBase64(llGetSubString(s, n, n)));
       if (n % 4 == 3)
       {
           dw_skey += [v];
           v = 0;
       }
       else
       {
           v = v << 8;
       }
   }
   
   //pad 0s (could be done dword-wise, after filling up to boundary, later, maybe...)
   for ( ; n < 64; n++)
   {
       if (n % 4 == 3)
       {
           dw_skey += [v];
           v = 0;
       }
       else
       {
           v = v << 8;
       }
   }
   dw_skey += [v];
   return dw_skey;

}

// Takes a dwordblock, adds a string and puts it padded for blocksize into a dwordlist // returns sha1blocks list PrepareSha1Blocks(list dwords, string s) {

   integer v = 0;
   integer cnt = llStringLength(s);
   integer n = cnt;
   integer mcnt = cnt;
   list shablocks = dwords;
   
   mcnt = cnt + (dwords != []) * 4; // add up the dword-data (total message length)
   
   for (n = 0; n < cnt; n++)
   {
       v = v | 0xFF & llBase64ToInteger("AAAA" + llStringToBase64(llGetSubString(s, n, n)));
       if (n % 4 == 3)
       {
           shablocks += [v];
           v = 0;
       }
       else
       {
           v = v << 8;
       }
   }
   
   // pad a 1 and seven 0's
   v = v | 0x80;
   if (n % 4 == 3)
   {
       shablocks += [v];
       v = 0;
   }
   else
   {
       v = v << 8;
   }
   n++;
       
   //we ignored the dwords silently, but now we have to take them into account
   //how many bytes do we need to fill blocks and have 8 bytes left...
   cnt = ((mcnt + 8) / 64 + 1) * 64 - 9;
   
   //pad 0s (could be done dword-wise, after filling up to boundary, later, maybe...)
   for (n += (dwords != []) * 4 ; n < cnt; n++)
   {
       if (n % 4 == 3)
       {
           shablocks += [v];
           v = 0;
       }
       else
       {
           v = v << 8;
       }
   }
   shablocks += [v];
   // pad message length
   shablocks += [0]; // we assume not to have more as 16M messagesize (roughly)
   shablocks += [8 * mcnt];
   
   return shablocks;

}

// Inner core of sha1 calculation, based on FIPS 180-1 // http://www.itl.nist.gov/fipspubs/fip180-1.htm // and some help from http://www.herongyang.com/crypto/message_digest_sha1.html // and a bit from lkalif specialized on dwordlists // // Takes a dwordlist as input and returns hash as dwordlist list ProcessSha1(list dwblocks) {

   integer block;
   integer blocks = (dwblocks != []) / 16;
   integer H0 = 0x67452301;
   integer H1 = 0xEFCDAB89;
   integer H2 = 0x98BADCFE;
   integer H3 = 0x10325476;
   integer H4 = 0xC3D2E1F0;
   for (block = 0; block < blocks; block++)
   {
       list W;
       integer t;
       integer A = H0;
       integer B = H1;
       integer C = H2;
       integer D = H3;
       integer E = H4;
      
       for (t = 0; t < 16; t++)
       {
           W += [llList2Integer(dwblocks, t + block * 16)];
       }
       for ( ; t < 80; t++)
       {
           integer x = llList2Integer(W, t - 3) ^ llList2Integer(W, t - 8) ^ llList2Integer(W, t - 14) ^ llList2Integer(W, t - 16);
           W += [(x << 1) |!!(x & 0x80000000)]; // borrowed from lkalif
       }
       for (t = 0; t < 20; t++)
       {
           integer TEMP = ((A << 5) | ((A >> 27) & 0x1F)) + ((B & C) | ((~B) & D)) + E + llList2Integer(W, t) + 0x5A827999;
           E = D; D = C; C = ((B << 30) | ((B >> 2) & 0x3FFFFFFF)); B = A; A = TEMP;
       }
       for (; t < 40; t++)
       {
           integer TEMP = ((A << 5) | ((A >> 27) & 0x1F)) + (B ^ C ^ D) + E + llList2Integer(W, t) + 0x6ED9EBA1;
           E = D; D = C; C = ((B << 30) | ((B >> 2) & 0x3FFFFFFF)); B = A; A = TEMP;
       }
       for (; t < 60; t++)
       {
           integer TEMP = ((A << 5) | ((A >> 27) & 0x1F)) + ((B & C) | (B & D) | (C & D)) + E + llList2Integer(W, t) + 0x8F1BBCDC;
           E = D; D = C; C = ((B << 30) | ((B >> 2) & 0x3FFFFFFF)); B = A; A = TEMP;
       }
       for (; t < 80; t++)
       {
           integer TEMP = ((A << 5) | ((A >> 27) & 0x1F)) + (B ^ C ^ D) + E + llList2Integer(W, t) + 0xCA62C1D6;
           E = D; D = C; C = ((B << 30) | ((B >> 2) & 0x3FFFFFFF)); B = A; A = TEMP;
       }
       
       H0 += A;
       H1 += B;
       H2 += C;
       H3 += D;
       H4 += E;
   }
   return [H0, H1, H2, H3, H4];

}

//Caveats: Handling of unicode undefined and no message longer 16M allowed list Sha1DWord(list dw, string message) {

   list sha1blocks = PrepareSha1Blocks(dw, message);
   list digest = ProcessSha1(sha1blocks);
   return digest;

}

list dw_key; list dw_ipad; list dw_opad;

// xor the 64bytes (16 dwords) with value list PreparePad(integer val) {

   list r;
   integer i;
   
   for (i = 0; i < 16; )
   {
       r += [llList2Integer(dw_key, i++) ^ val];
   }
   
   return r;

}

// 2 step design, for simple re-use of key-data HmacInit(string secretkey) {

   if (llStringLength(secretkey) > 64) // sha1 only if blocksize exceeded
       dw_key = Sha1DWord([], secretkey) + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
   else
       dw_key = PrepareShortkey(secretkey);
   
   dw_ipad = PreparePad(0x36363636);
   dw_opad = PreparePad(0x5c5c5c5c);

}

list HmacUpdate(string message) {

   list dw_ihash = Sha1DWord(dw_ipad, message);
   list dw_opad2 = dw_opad + dw_ihash;
   
   return Sha1DWord(dw_opad2, "");

}

integer cacheSize = 25; list tokenCache = [];

string GetAccessToken(key avatarId) {

   string result = "";
   integer index = llListFindList(tokenCache, [avatarId]);
   if(index != -1)
   {
       result = llList2String(tokenCache, index + 1);
   }
   return result;

}

string GetAccessTokenSecret(key avatarId) {

   string result = "";
   integer index = llListFindList(tokenCache, [avatarId]);
   if(index != -1)
   {
       result = llList2String(tokenCache, index + 2);
   }
   return result;

}

SetAccessToken(key avatarId, string accessToken, string accessTokenSecret) {

   integer stride = 3;
   integer maxLength = (cacheSize * stride) - stride;
   if(llGetListLength(tokenCache) > maxLength)
   {
       tokenCache = llDeleteSubList(tokenCache, maxLength, -1);
   }
   tokenCache = [avatarId, accessToken, accessTokenSecret] + tokenCache;

}

string OAuthUrlEncodeString(string s) {

   string result = "";
   integer count = llStringLength(s);
   integer i;
   for (i = 0; i < count; ++i)
   {
       string c = llGetSubString(s, i, i);
       if(c != "-" && c != "_" && c != "." && c != "," && c != "~")
       {
           c = llEscapeURL(c);
       }
       result += c;
   }
   return result;

}

list OAuthUrlEncodeList(list l) {

   list result = [];
   integer c = llGetListLength(l);
   integer i;
   for(i = 0; i < c; ++i)
   {
       result += OAuthUrlEncodeString(llList2String(l, i));
   }
   return result;

}

string OAuthConcatenate(list l) {

   string result = "";
   integer count = llGetListLength(l);
   integer i = 0;
   while (i < count)
   {
       result += llList2String(l, i) + "=" + llList2String(l, i + 1);
       i += 2;
       if (i < count)
       {
           result += "&";
       }
   }
   return result;

}

string Sign(string method, string url, string consumerSecret, string tokenSecret, list parameters) {

   string signatureBase = OAuthUrlEncodeString(method) + "&" + OAuthUrlEncodeString(url) + "&" + 
       OAuthUrlEncodeString(OAuthConcatenate(parameters));
   //llOwnerSay(signatureBase);
   list keyList = [];
   keyList += consumerSecret;
   keyList += tokenSecret;
   string keyString = consumerSecret + "&" + tokenSecret;
   //llOwnerSay(keyString);
   HmacInit(keyString);
   list dwSig = HmacUpdate(signatureBase);
   
   return DwordListToBase64(dwSig) + "=";

}

string OAuthUrl(string method, string url, string consumerKey, string consumerSecret, string token,

                       string tokenSecret, list additionalParams)

{

   integer nonce = (integer)llFrand(1000000);
   list parameters = ["oauth_version","1.0a",
                        "oauth_nonce", (string)nonce, 
                        "oauth_consumer_key", consumerKey, 
                        "oauth_signature_method", "HMAC-SHA1", 
                        "oauth_timestamp", (string)llGetUnixTime()];
                        
   if(token != "")
   {
       parameters += "oauth_token";
       parameters += token;
   }
   parameters += additionalParams;
   parameters = OAuthUrlEncodeList(parameters);
   parameters = llListSort(parameters, 2, 1);
   string sig = Sign(method, url, consumerSecret, tokenSecret, parameters);
   parameters += "oauth_signature";
   parameters += sig;
   url = url + "?" + OAuthConcatenate(parameters);
   //llOwnerSay(url);
   return url;

}

string FindOAuthResponseValue(string name, list tokens) {

   //llOwnerSay("Looking for " + name + " in:" + llList2CSV(tokens));
   integer index = llListFindList(tokens, [name]);
   if (index != -1)
   {
       return llList2String(tokens, index + 1);
   }
   return "";

}

list TokenizeOAuthResponse(string response) {

   return llParseString2List(response, ["=", "&"], []);

}

ResetTimeout() {

   // Time out HTTP requests and input requests after a minute of inactivity.
   llSetTimerEvent(60);

}

Reset() {

   gAvatarKey = NULL_KEY;
   gMessage = "";
   gRequestTokenKey = NULL_KEY;
   gRequestToken = "";
   gRequestTokenSecret = "";
   gAccessTokenKey = NULL_KEY;
   gVerifier = "";
   gRetries = 0;
   llSetTimerEvent(0);

}

RequestAccessToken() {

   list parameters = ["oauth_verifier", gVerifier];
   string url = OAuthUrl("POST", "http://twitter.com/oauth/access_token", 
                           gConsumerKey, gConsumerSecret, gRequestToken, gRequestTokenSecret, parameters);
   ResetTimeout();
   gAccessTokenKey = llHTTPRequest(url, [HTTP_METHOD, "POST"], "");

}

RequestRequestToken() {

   string url = OAuthUrl("GET", "http://twitter.com/oauth/request_token", gConsumerKey, gConsumerSecret, "", "", 
                         ["oauth_callback", gAuthorizeUrl]);
   ResetTimeout();
   gRequestTokenKey = llHTTPRequest(url, [], "");

}

RequestTokens(key avatar, string message) {

   string accessToken = GetAccessToken(avatar);
   string accessTokenSecret = GetAccessTokenSecret(avatar);
   
   if(accessToken != "" && accessTokenSecret != "")
   {
       // Have access token, update immediately.
       UpdateStatus(accessToken, accessTokenSecret, message);
   }
   else
   {
       if(gRequestTokenKey != NULL_KEY || 
          gAccessTokenKey != NULL_KEY)
       {
           // Currently requesting access token, drop this request on the floor.
           // TODO: babbage: queue request for later, handle paralell requests, or signal failure to caller...
           llOwnerSay("OAuth request in progress, please wait.");
           return;
       }
       
       // Access token unknown and no request in progress, request access token.
       Reset();
       gAvatarKey = avatar;
       gMessage = message;
       RequestRequestToken();
   }

}

UpdateStatus(string accessToken, string accessTokenSecret, string message) {

   list parameters = ["status", message];        
   string url = OAuthUrl("POST", "http://twitter.com/statuses/update.xml", 
                           gConsumerKey, gConsumerSecret, accessToken, accessTokenSecret, parameters);
   llHTTPRequest(url, [HTTP_METHOD, "POST"], "");

}

default {

   state_entry()
   {
       llRequestURL();
       Reset();
   }
   
   on_rez(integer param)
   {
       llRequestURL();
       Reset();
   }
   
   changed(integer changes)
   {
       if((changes & CHANGED_REGION_START) != 0)
       {
           llRequestURL();
       }
   }
   
   link_message(integer sender_num, integer num, string message, key avatar)
   {
       if(num == TWITTER_OAUTH_SET_CONSUMER_KEY)
       {
           gConsumerKey = message;
       }
       else if(num == TWITTER_OAUTH_SET_CONSUMER_SECRET)
       {
           gConsumerSecret = message;
       }
       else if(num == TWITTER_OAUTH_SET_MAX_RETRIES)
       {
           gMaxRetries = (integer) message;
       }
       else if(num == TWITTER_OAUTH_UPDATE_STATUS)
       {
           if(gConsumerKey == "")
           {
               llOwnerSay("Consumer key must be set before status update.");
               return;
           }
           if(gConsumerSecret == "")
           {
               llOwnerSay("Consumer secret must be set before status update.");
               return;
           }
           RequestTokens(avatar, message);
       }
   }
   http_response(key id, integer status, list meta, string body)
   {
       //llOwnerSay("status:" + (string)status);
       //llOwnerSay("body:" + body);
       
       if (id == gRequestTokenKey)
       {
           if(status != 200)
           {
               if(gRetries++ < gMaxRetries)
               {
                   llOwnerSay("Failed to obtain oauth request token, retrying...");
                   RequestRequestToken();
               }
               else
               {
                   llOwnerSay("Failed to obtain oauth request token");
                   llOwnerSay("Status:" + (string)status);
                   llOwnerSay("Body:" + body);
                   Reset();
               }
               return;
           }
           
           gRetries = 0;
           list responseTokens = TokenizeOAuthResponse(body);
           gRequestToken = FindOAuthResponseValue("oauth_token", responseTokens);
           gRequestTokenSecret = FindOAuthResponseValue("oauth_token_secret", responseTokens);
           
           string url = "http://twitter.com/oauth/authorize?" + 
               OAuthConcatenate(["oauth_token", gRequestToken]);
           ResetTimeout();
           llLoadURL(gAvatarKey, "Please authorise Twitter access", url);
           
           if(gAuthorizeUrl == "oob")
           {
               llListen(3, "", NULL_KEY, "");
               llOwnerSay("Please chat PIN on channel 3 (eg \"/3 12345678\")");
           }
       }
       else if (id == gAccessTokenKey)
       {
           if(status != 200)
           {
               if(gRetries++ < gMaxRetries)
               {
                   llOwnerSay("Failed to obtain oauth access token, retrying...");
                   RequestAccessToken();
               }
               else
               {
                   llOwnerSay("Failed to obtain oauth access token");
                   llOwnerSay("Status:" + (string)status);
                   llOwnerSay("Body:" + body);
                   Reset();
               }
               return;
           }
           
           gRetries = 0;
           list responseTokens = TokenizeOAuthResponse(body);
           string accessToken = FindOAuthResponseValue("oauth_token", responseTokens);
           string accessTokenSecret = FindOAuthResponseValue("oauth_token_secret", responseTokens);
           string screenName = FindOAuthResponseValue("screen_name", responseTokens);
           SetAccessToken(gAvatarKey, accessToken, accessTokenSecret);
           UpdateStatus(accessToken, accessTokenSecret, gMessage);
           llLoadURL(gAvatarKey, "Show status update?", "http://twitter.com/" + screenName);
           Reset();
       }
       else
       {
           // NOTE: babbage: status update always fail as http out cannot accept XML or JSON
           // TODO: babbage: allow http out to accept XML or JSON, so we can actually check for errors here...
           return;
       }
   }
   
   listen(integer channel, string name, key id, string message)
   {
       if(id == gAvatarKey && channel == 3)
       {
           // PIN based authorization flow.
           gVerifier = message;
           RequestAccessToken();
       }
   }
   
   http_request(key id, string method, string body)
   {
       if(method == "URL_REQUEST_GRANTED")
       {
           // NOTE: babbage: need trailing / path...
           gAuthorizeUrl = body + "/"; 
       }
       else if(method == "URL_REQUEST_DENIED")
       {
           llOwnerSay("No URLs available, using PIN based flow.");
           gAuthorizeUrl = "oob";
       }
       else if(method == "GET")
       {
           list responseTokens = TokenizeOAuthResponse(llGetHTTPHeader(id, "x-query-string"));
           gVerifier = FindOAuthResponseValue("oauth_verifier", responseTokens);
           
           RequestAccessToken();
           
           // TODO: babbage: allow HTML response body, so we can show something more useful than a blank web page here...
           llHTTPResponse(id, 200, "");
       }
   }
   
   timer()
   {
       llOwnerSay("Request timeout, resetting...");
       Reset();
   }

} </lsl>