Twitter OAuth Library/HUD

From Second Life Wiki
Jump to navigation Jump to search

Twitter HUD based on Babbage's OAuth Library. More information: Talk:Twitter_OAuth_Library.

Where modified by Opensource Obscure, the following code may cause irritation and sickness in actual, serious scripters and programmers. You've been warned.

Client

//////////////////////////////////////////////////////////////////////////////////////
//    by Opensource Obscure - based on:
//    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 = "xxxxxxxxxxxx"; // (Opensource Obscure's details in the HUD)
string TWITTER_OAUTH_CONSUMER_SECRET = "xxxxxxxxxxxxxxxxxxxxxxxxx"; // (Opensource Obscure's details in the HUD)
 
// 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://maps.secondlife.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(string message)
{
    return message + TwitterOAuthGetSLURL();
}
 
TwitterOAuthUpdateStatus(string message, key avatar)
{
    llMessageLinked(LINK_THIS, TWITTER_OAUTH_UPDATE_STATUS, message, avatar);
}
 

integer listener_handle ;
string message_final;
string message_final_length;
string prompt;

text_box(string prompt)
{
    llListen(99, llDetectedName(0), llDetectedKey(0), "" );
    llTextBox(llDetectedKey(0), message_final_length + prompt,99);    
}
default
{
    state_entry()
    {
        llOwnerSay("Click the HUD to write your message. Current SLURL will be included. 
        Get errors? Detach, reattach, try again. No luck? IM Opensource Obscure.");
        TwitterOAuthInit();
    }
 
    on_rez(integer param)
    {
        TwitterOAuthInit();
    }
 
    touch_start(integer total_number)
    {
        text_box("Update your Twitter status:");
    }
    
    listen(integer channel, string name, key id, string message)
    {
        message_final = TwitterOAuthBuildMessage(message);        
        string message_final_length = (string)llStringLength(message_final);
        if(llStringLength(message_final) > 140)
        {
           text_box(message_final_length + " characters: message is too long, insert a new one:");
        }
        else        
        {
            llOwnerSay("Sending your message (" + message_final_length + " chars)");
            TwitterOAuthUpdateStatus(message_final, id);
        }
    }
}

Library

// see these pages for details:
// https://wiki.secondlife.com/wiki/Twitter_OAuth_Library
// https://wiki.secondlife.com/wiki/Talk:Twitter_OAuth_Library
// 2011.05.28 -- Opensource Obscure
 
//////////////////////////////////////////////////////////////////////////////////////
//
//    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();
    }
}