Twitter OAuth Library/HUD
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();
}
}