/////////////////////////////////////////////////////////////////////////// // Copyright (C) Wizardry and Steamworks 2016 - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// // // An eggdrop-like group bot using Corrade. // /////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueGet(string k, string data) { if(llStringLength(data) == 0) return ""; if(llStringLength(k) == 0) return ""; list a = llParseStringKeepNulls(data, ["&", "="], []); integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]); if(i != -1) return llList2String(a, 2*i+1); return ""; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueDelete(string k, string data) { if(llStringLength(data) == 0) return ""; if(llStringLength(k) == 0) return ""; integer i = llListFindList( llList2ListStrided( llParseString2List(data, ["&", "="], []), 0, -1, 2 ), [ k ]); if(i != -1) return llDumpList2String( llDeleteSubList( llParseString2List(data, ["&"], []), i, i), "&"); return data; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2014 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueSet(string k, string v, string data) { if(llStringLength(k) == 0) return ""; if(llStringLength(v) == 0) return ""; if(llStringLength(data) == 0) return k + "=" + v; integer i = llListFindList( llList2ListStrided( llParseString2List(data, ["&", "="], []), 0, -1, 2 ), [ k ]); if(i != -1) return llDumpList2String( llListReplaceList( llParseString2List(data, ["&"], []), [ k + "=" + v ], i, i), "&"); return data + "&" + k + "=" + v; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// string wasKeyValueEncode(list data) { list k = llList2ListStrided(data, 0, -1, 2); list v = llList2ListStrided(llDeleteSubList(data, 0, 0), 0, -1, 2); data = []; do { data += llList2String(k, 0) + "=" + llList2String(v, 0); k = llDeleteSubList(k, 0, 0); v = llDeleteSubList(v, 0, 0); } while(llGetListLength(k) != 0); return llDumpList2String(data, "&"); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// // escapes a string in conformance with RFC1738 string wasURLEscape(string i) { string o = ""; do { string c = llGetSubString(i, 0, 0); i = llDeleteSubString(i, 0, 0); if(c == "") jump continue; if(c == " ") { o += "+"; jump continue; } if(c == "\n") { o += "%0D" + llEscapeURL(c); jump continue; } o += llEscapeURL(c); @continue; } while(i != ""); return o; } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// // unescapes a string in conformance with RFC1738 string wasURLUnescape(string i) { return llUnescapeURL( llDumpList2String( llParseString2List( llDumpList2String( llParseString2List( i, ["+"], [] ), " " ), ["%0D%0A"], [] ), "\n" ) ); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3 // /////////////////////////////////////////////////////////////////////////// string wasListToCSV(list l) { list v = []; do { string a = llDumpList2String( llParseStringKeepNulls( llList2String( l, 0 ), ["\""], [] ), "\"\"" ); if(llParseStringKeepNulls( a, [" ", ",", "\n", "\""], [] ) != (list) a ) a = "\"" + a + "\""; v += a; l = llDeleteSubList(l, 0, 0); } while(l != []); return llDumpList2String(v, ","); } /////////////////////////////////////////////////////////////////////////// // Copyright (C) 2015 Wizardry and Steamworks - License: CC BY 2.0 // /////////////////////////////////////////////////////////////////////////// list wasCSVToList(string csv) { list l = []; list s = []; string m = ""; do { string a = llGetSubString(csv, 0, 0); csv = llDeleteSubString(csv, 0, 0); if(a == ",") { if(llList2String(s, -1) != "\"") { l += m; m = ""; jump continue; } m += a; jump continue; } if(a == "\"" && llGetSubString(csv, 0, 0) == a) { m += a; csv = llDeleteSubString(csv, 0, 0); jump continue; } if(a == "\"") { if(llList2String(s, -1) != a) { s += a; jump continue; } s = llDeleteSubList(s, -1, -1); jump continue; } m += a; @continue; } while(csv != ""); // postcondition: length(s) = 0 return l + m; } // for notecard reading integer line = 0; // key-value data will be read into this list list tuples = []; string configuration = ""; // Corrade's online status. integer online = FALSE; integer compatible = FALSE; string URL = ""; // The notifications to bind to. list notifications = [ "group", "membership", "login", "MQTT" ]; default { state_entry() { if(llGetInventoryType("configuration") != INVENTORY_NOTECARD) { llOwnerSay("[Control] Sorry, could not find a configuration inventory notecard."); return; } // DEBUG llOwnerSay("[Control] Reading configuration file..."); llGetNotecardLine("configuration", line); } dataserver(key id, string data) { if(data == EOF) { // invariant, length(tuples) % 2 == 0 if(llGetListLength(tuples) % 2 != 0) { llOwnerSay("[Control] Error in configuration notecard."); return; } key CORRADE = llList2Key( tuples, llListFindList( tuples, [ "corrade" ] ) +1); if(CORRADE == NULL_KEY) { llOwnerSay("[Control] Error in configuration notecard: corrade"); return; } string GROUP = llList2String( tuples, llListFindList( tuples, [ "group" ] ) +1); if(GROUP == "") { llOwnerSay("[Control] Error in configuration notecard: group"); return; } string PASSWORD = llList2String( tuples, llListFindList( tuples, [ "password" ] ) +1); if(PASSWORD == "") { llOwnerSay("[Control] Error in configuration notecard: password"); return; } string VERSION = llList2String( tuples, llListFindList( tuples, [ "version" ] ) +1); if(VERSION == "") { llOwnerSay("[Control] Error in configuration notecard: version"); return; } // DEBUG llOwnerSay("[Control] Read configuration notecard..."); configuration = wasKeyValueEncode(tuples); // GC tuples = []; state request_url_notifications; } if(data == "") jump continue; // No support for inline comments for this one! Needs parsing! integer i = llSubStringIndex(data, "#"); if(i == 0) data = llDeleteSubString(data, i, -1); list o = llParseString2List(data, ["="], []); // get rid of starting and ending quotes string k = llDumpList2String( llParseString2List( llStringTrim( llList2String( o, 0 ), STRING_TRIM), ["\""], [] ), "\""); string v = llDumpList2String( llParseString2List( llStringTrim( llList2String( o, 1 ), STRING_TRIM), ["\""], [] ), "\""); if(k == "" || v == "") jump continue; tuples += k; tuples += v; @continue; llGetNotecardLine("configuration", ++line); } attach(key id) { llResetScript(); } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START)) { llResetScript(); } } } state request_url_notifications { state_entry() { // DEBUG llOwnerSay("[Control] Requesting URL..."); llRequestURL(); } http_request(key id, string method, string body) { if(method != URL_REQUEST_GRANTED) return; URL = body; // DEBUG llOwnerSay("[Control] Got URL..."); state unbind_notifications; } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START) || (change & CHANGED_OWNER)) { llResetScript(); } } } state unbind_notifications { state_entry() { // DEBUG llOwnerSay("[Control] Releasing notifications..."); llInstantMessage( (key)wasKeyValueGet( "corrade", configuration ), wasKeyValueEncode( [ "command", "notify", "group", wasURLEscape( wasKeyValueGet( "group", configuration ) ), "password", wasURLEscape( wasKeyValueGet( "password", configuration ) ), "action", "remove", "tag", wasURLEscape( wasKeyValueGet( "notification tag", configuration ) ), "callback", wasURLEscape(URL) ] ) ); llSetTimerEvent(60); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); if(wasKeyValueGet("command", body) != "notify" || wasKeyValueGet("success", body) != "True") { // DEBUG llOwnerSay("[Control] Unable to release tag: " + wasURLUnescape( wasKeyValueGet("error", body) ) ); llResetScript(); } state bind_notifications; } timer() { llOwnerSay("[Control] Timeout releasing notifications"); llResetScript(); } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START) || (change & CHANGED_OWNER)) { llResetScript(); } } state_exit() { llSetTimerEvent(0); } } state bind_notifications { state_entry() { // DEBUG llOwnerSay("[Control] Binding to notifications..."); llInstantMessage( wasKeyValueGet( "corrade", configuration ), wasKeyValueEncode( [ "command", "notify", "group", wasURLEscape( wasKeyValueGet( "group", configuration ) ), "password", wasURLEscape( wasKeyValueGet( "password", configuration ) ), "action", "add", "type", wasURLEscape( wasListToCSV( notifications ) ), "URL", wasURLEscape(URL), "tag", wasURLEscape( wasKeyValueGet( "notification tag", configuration ) ), "callback", wasURLEscape(URL) ] ) ); llSetTimerEvent(60); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); if(wasKeyValueGet("command", body) != "notify" || wasKeyValueGet("success", body) != "True") { // DEBUG llOwnerSay("[Control] Unable to bind notifications: " + wasURLUnescape( wasKeyValueGet("error", body) ) ); llResetScript(); } state version_check; } timer() { llOwnerSay("[Control] Timeout binding notifications"); llResetScript(); } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START) || (change & CHANGED_OWNER)) { llResetScript(); } } state_exit() { llSetTimerEvent(0); } } state version_check { state_entry() { // DEBUG llOwnerSay("[Control] Checking version..."); llInstantMessage( wasKeyValueGet( "corrade", configuration ), wasKeyValueEncode( [ "command", "version", "group", wasURLEscape( wasKeyValueGet( "group", configuration ) ), "password", wasURLEscape( wasKeyValueGet( "password", configuration ) ), "callback", wasURLEscape(URL) ] ) ); llSetTimerEvent(60); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); llSetTimerEvent(0); if(wasKeyValueGet("success", body) != "True") { llOwnerSay("[Control] Version check failed..."); return; } list v = llParseString2List( wasKeyValueGet( "data", body ), ["."], [] ); integer receivedVersion = (integer)(llList2String(v, 0) + llList2String(v, 1)); v = llParseString2List( wasKeyValueGet( "version", configuration ), ["."], [] ); integer notecardVersion = (integer)(llList2String(v, 0) + llList2String(v, 1)); if(receivedVersion < notecardVersion) { llOwnerSay("[Control] Version is incompatible! You need a Corrade of at least version: " + wasKeyValueGet( "version", configuration ) + " for this template." ); compatible = FALSE; return; } // Add the URL to the configuration so it can be used for all components. configuration = wasKeyValueSet("URL", URL, configuration); // DEBUG llOwnerSay("[Control] Version is compatible!"); compatible = TRUE; state serve_configuration; } timer() { llOwnerSay("[Control] Timeout checking version..."); llResetScript(); } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START) || (change & CHANGED_OWNER)) { llResetScript(); } } state_exit() { llSetTimerEvent(0); } } state serve_configuration { state_entry() { // DEBUG llOwnerSay("[Control] Serving configuration and passing callbacks..."); } http_request(key id, string method, string body) { llHTTPResponse(id, 200, "OK"); if(wasKeyValueGet("command", body) != "") { llMessageLinked(LINK_THIS, 0, body, "callback"); return; } // Check if this group message is from Corrade and passed through Discord. if(wasKeyValueGet("type", body) == "group" && llToUpper(wasKeyValueGet("agent", body)) == llToUpper(wasKeyValueGet("corrade", configuration)) && llSubStringIndex(wasURLUnescape(wasKeyValueGet("message", body)), "[Discord]:") != -1) { // Split message in Discord discriminator and message body. list messageSplit = llParseString2List( wasURLUnescape( wasKeyValueGet("message", body) ), ["[Discord]:"], [] ); // Retrive the Discord discriminator. string did = llStringTrim( llList2String( messageSplit, 0 ), STRING_TRIM ); // Retrieve a list of Discord discriminator that are allowed to send administrative commands. list admins = wasCSVToList(wasKeyValueGet("discord admin", configuration)); // If the sender is not amongst the administrative Discord discriminators, then strip the agent details. if(llListFindList(admins, [ did ]) == -1) { // DEBUG llOwnerSay("[Control] Non admin, issuing command, strip the agent details from the command."); body = wasKeyValueDelete("firstname", body); body = wasKeyValueDelete("lastname", body); body = wasKeyValueDelete("agent", body); } // Pack the message back without the Discord discriminator identifier. body = wasKeyValueSet( "message", wasURLEscape( llStringTrim( llList2String( messageSplit, 1 ), STRING_TRIM ) ), body ); } llMessageLinked(LINK_THIS, 0, body, "notification"); } link_message(integer sender, integer num, string message, key id) { if(message != "configuration") return; llMessageLinked(LINK_THIS, 0, configuration, "configuration"); } on_rez(integer num) { llResetScript(); } changed(integer change) { if((change & CHANGED_INVENTORY) || (change & CHANGED_REGION_START) || (change & CHANGED_OWNER)) { llResetScript(); } } state_exit() { llSetTimerEvent(0); } }