LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin
This version fixes a few things, like instituting an actual timeout for the ask dialog and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. This is a work in progress. Eventually it will have some of the features suggested by Marine in the original Reference Implementation.
<lsl>
//== RestrainedLife Viewer Relay Script //== by Felis Darwin //== Based on Reference Implementation by Marine Kelley
integer ALLOW_REMOVE_DETACH = TRUE; integer DEBUG = FALSE;
// --------------------------------------------------- // Amethyst Plugin Variables // ---------------------------------------------------
key nullkey = NULL_KEY; string nullstr = "";
integer secaccess=0; //== Do secondary owners have access to the RL functions?
// Internal variables key ownerkey = nullkey; list secowners = [];
key setby = NULL_KEY; //== Who set the RLV Relay status?
integer lockstatus; //== Has the collar been locked by the RLV plugin?
// --------------------------------------------------- // Constants // ---------------------------------------------------
integer RLVRS_PROTOCOL_VERSION = 1014; // version of the protocol, stated on the specification page
string PREFIX_RL_COMMAND = "@"; string PREFIX_METACOMMAND = "!";
integer RLVRS_CHANNEL = -1812221819; // RLVRS in numbers integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
integer MAX_OBJECT_DISTANCE = 20; // 20m is llSay distance integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
integer PERMISSION_DIALOG_TIMEOUT = 30;
integer LOGIN_DELAY_WAIT_FOR_PONG = 10; integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
integer PING_INTERVAL = 60; //== Time between pings
integer MODE_OFF = 0; integer MODE_ASK = 1; integer MODE_AUTO = 2;
// --------------------------------------------------- // Variables // ---------------------------------------------------
integer nMode;
list lRestrictions; // restrictions currently applied (without the "=n" part) key kSource; // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not
string sPendingName; // name of initiator of pending request (first request of a session in mode 1) key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1) string sPendingMessage; // message of pending request (first request of a session in mode 1) integer sPendingTime;
// used on login integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit) integer loginWaitingForPong; integer loginPendingForceSit;
key lastForceSitDestination; integer lastForceSitTime;
integer lastPingTime;
// --------------------------------------------------- // Low Level Communication // ---------------------------------------------------
debug(string x)
{
if (DEBUG)
{
llOwnerSay("DEBUG: " + x);
}
}
// acknowledge or reject ack(string cmd_id, key id, string cmd, string ack) {
llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
}
// cmd begins with a '@' sendRLCmd(string cmd) {
llOwnerSay(cmd);
}
// get current mode as string string getModeDescription() {
if (nMode == 0) return "RLV Relay is OFF"; else if (nMode == 1) return "RLV Relay is ON (permission needed)"; else return "RLV Relay is ON (auto-accept)";
}
// check that this command is for us and not someone else integer verifyWeAreTarget(string message) {
list tokens = llParseString2List(message, [","], []);
if (llGetListLength(tokens) == 3) // this is a normal command
{
if (llList2String(tokens, 1) == llGetOwner()) // talking to me ?
{
return TRUE;
}
}
return FALSE;
}
// --------------------------------------------------- // Permission Handling // ---------------------------------------------------
// are we already under command by this object? integer isObjectKnow(key id) {
// first some error handling
if (id == NULL_KEY)
{
return FALSE;
}
// are we already under command by this object?
if (kSource == id)
{
return TRUE;
}
// are we not under command by any object but were we forced to sit on this object recently?
if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
{
debug("on last force sit target");
if (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime())
{
debug("and recent enough to auto accept");
return TRUE;
}
}
return FALSE;
}
// check whether the object is in llSay distance.
// The specification requires llSay instead of llShout or llRegionSay
// to be used to limit the range. But this has to be checked here again
// because the objects are not trustworthy.
integer isObjectNear(key id)
{
vector myPosition = llGetRootPosition(); list temp = llGetObjectDetails(id, ([OBJECT_POS])); vector objPostition = llList2Vector(temp,0); float distance = llVecDist(objPostition, myPosition); return distance <= MAX_OBJECT_DISTANCE;
}
// do a basic check on the identity of the object trying to issue a command integer isObjectIdentityTrustworthy(key id) {
key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);
key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);
key object_owner=llGetOwnerKey(id);
key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
debug("group= " + (string) parcel_group + " / " + (string) object_group);
if (object_owner==llGetOwner () // IF I am the owner of the object
|| object_owner==parcel_owner // OR its owner is the same as the parcel I'm on
|| object_group==parcel_group // OR its group is the same as the parcel I'm on
)
{
return TRUE;
}
return FALSE;
}
// Is this a simple request for information or a meta command like !release?
integer isSimpleRequest(list list_of_commands)
{
integer len = llGetListLength(list_of_commands);
integer i;
debug("Checking simplicity of commands...");
// now check every single atomic command
for (i=0; i < len; ++i)
{
string command = llList2String(list_of_commands, i);
if (!isSimpleAtomicCommand(command))
{
debug("Command "+ command +" fails simplicity check.");
return FALSE;
}
}
// all atomic commands passed the test
return TRUE;
}
// is this a simple atmar command // (a command which only queries some information or releases restrictions) // (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command) integer isSimpleAtomicCommand(string cmd) {
// check right hand side of the "=" - sign
integer index = llSubStringIndex (cmd, "=");
if (index > -1) // there is a "="
{
// check for a number after the "="
string param = llGetSubString (cmd, index + 1, -1);
if (((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) // is it an integer (channel number)?
{
return TRUE;
}
// removing restriction
if ((param == "y") || (param == "rem"))
{
return TRUE;
}
}
// check for a leading ! (meta command)
if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
{
return TRUE;
}
// check for @clear
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
// (but we need this check here because "!release|@clear" is a BROKEN attempt to work around
// a bug in the first relay implementation. You should refuse to use relay versions < 1013
// instead.)
if (cmd == "@clear")
{
return TRUE;
}
// this one is not "simple".
return FALSE;
}
// If we already have commands from this object pending // because of a permission request dialog, just add the // new commands at the end. // Note: We use a timeout here because the player may // have "ignored" the dialog. integer tryToGluePendingCommands(key id, string commands) {
if ((sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
{
debug("Gluing " + sPendingMessage + " with " + commands);
sPendingMessage = sPendingMessage + "|" + commands;
return TRUE;
}
return FALSE;
}
// verifies the permission. This includes mode // (off, permission, auto) of the relay and the // identity of the object (owned by parcel people). integer verifyPermission(key id, string name, string message) {
debug("Verifying permission for command "+ message);
// is it switched off?
if (nMode == MODE_OFF)
{
return FALSE;
}
// extract the commands-part
list tokens = llParseString2List (message, [","], []);
if (llGetListLength (tokens) < 3)
{
return FALSE;
}
string commands = llList2String(tokens, 2);
list list_of_commands = llParseString2List(commands, ["|"], []);
// accept harmless commands silently
if (isSimpleRequest(list_of_commands))
{
debug("Simple command, performing");
return TRUE;
}
// if we are already having a pending permission-dialog request for THIS object,
// just add the new commands at the end of the pending command list.
if (tryToGluePendingCommands(id, commands))
{
debug("Appending to store of commands pending approval.");
return FALSE; //== Glue the commands and process them later
}
// check whether this object belongs here
integer trustworthy = isObjectIdentityTrustworthy(id);
string warning = "";
if (!trustworthy)
{
warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
}
// ask in permission-request-mode and/OR in case the object identity is suspisous.
if (nMode == MODE_ASK || !trustworthy)
{
sPendingId=id;
sPendingName=name;
sPendingMessage=message;
sPendingTime = llGetUnixTime();
llSetTimerEvent(2.0);
if(llKey2Name(llGetOwnerKey(id)) != "")
name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
debug("Asking for permission");
return FALSE;
}
return TRUE;
}
// ---------------------------------------------------
// Executing of commands
// ---------------------------------------------------
// execute a non-parsed message // this command could be denied here for policy reasons, (if it were implemenetd) // but this time there will be an acknowledgement execute(string name, key id, string message) {
list tokens=llParseString2List (message, [","], []);
if (llGetListLength (tokens)==3) // this is a normal command
{
string cmd_id=llList2String (tokens, 0); // CheckAttach
key target=llList2Key (tokens, 1); // UUID
if (target==llGetOwner ()) // talking to me ?
{
list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);
integer len=llGetListLength (list_of_commands);
integer i;
string command;
string prefix;
for (i=0; i<len; ++i) // execute every command one by one
{
// a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
command=llList2String (list_of_commands, i);
prefix=llGetSubString (command, 0, 0);
if (prefix==PREFIX_RL_COMMAND) // this is a RL command
{
executeRLVCommand(cmd_id, id, command);
}
else if (prefix==PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
{
executeMetaCommand(cmd_id, id, command);
}
}
}
}
}
// executes a command for the restrained life viewer // with some additinal magic like book keeping executeRLVCommand(string cmd_id, string id, string command) {
// we need to know whether whether is a rule or a simple command
list tokens_command=llParseString2List (command, ["="], []);
string behav=llList2String (tokens_command, 0); // @getattach:skull
string param=llList2String (tokens_command, 1); // 2222
integer ind=llListFindList (lRestrictions, [behav]);
debug("param=" + param);
if (param=="n" || param=="add") // add to lRestrictions
{
if (ind<0) lRestrictions+=[behav];
if(kSource == NULL_KEY && !lockstatus)
llOwnerSay("@detach=n");
kSource=id; // we know that kSource is either NULL_KEY or id already
}
else if (param=="y" || param=="rem") // remove from lRestrictions
{
if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
if (llGetListLength (lRestrictions)==0)
{
kSource=NULL_KEY;
if(!lockstatus)
llOwnerSay("@detach=y");
}
}
else if (param == "force" && !ALLOW_REMOVE_DETACH)
{
debug("force: " + behav);
list temp = llParseString2List(behav, [":"], []);
string commandName = llList2String (temp, 0); // @sit
if (commandName == "@detach" || commandName == "@remoutfit")
{
debug("rejecting remove/detach");
llWhisper(0, "Not stripping");
ack(cmd_id, id, command, "ko");
return;
}
}
workaroundForAtClear(command);
rememberForceSit(command);
sendRLCmd(command); // execute command
ack(cmd_id, id, command, "ok"); // acknowledge
}
// check for @clear // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around // a bug in the first relay implementation. You should refuse to use relay versions < 1013 // instead.) workaroundForAtClear(string command) {
if (command == "@clear")
{
releaseRestrictions();
}
}
// remembers the time and object if this command is a force sit rememberForceSit(string command) {
list tokens_command=llParseString2List (command, ["="], []);
string behav=llList2String (tokens_command, 0); // @sit:<uuid>
string param=llList2String (tokens_command, 1); // force
if (param != "force")
{
return;
}
tokens_command=llParseString2List(behav, [":"], []);
behav=llList2String (tokens_command, 0); // @sit
param=llList2String (tokens_command, 1); // <uuid>
debug("'force'-command:" + behav + "/" + param);
if (behav != "@sit")
{
return;
}
lastForceSitDestination = (key) param;
lastForceSitTime = llGetUnixTime();
debug("remembered force sit");
}
// executes a meta command which is handled by the relay itself executeMetaCommand(string cmd_id, string id, string command) {
if (command==PREFIX_METACOMMAND+"version") // checking relay version
{
ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
}
else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
{
releaseRestrictions();
ack(cmd_id, id, command, "ok");
}
}
// lift all the restrictions (called by !release and by turning the relay off) releaseRestrictions () {
kSource=NULL_KEY;
if(!lockstatus)
llOwnerSay("@detach=y");
integer i;
integer len=llGetListLength (lRestrictions);
for (i=0; i<len; ++i)
{
sendRLCmd(llList2String (lRestrictions, i)+"=y");
}
lRestrictions = [];
loginPendingForceSit = FALSE;
}
// ---------------------------------------------------
// initialisation and login handling
// ---------------------------------------------------
init() {
nMode=1; kSource=NULL_KEY; lRestrictions=[]; sPendingId=NULL_KEY; sPendingName=""; sPendingMessage=""; llListen (RLVRS_CHANNEL, "", "", ""); llListen (DIALOG_CHANNEL, "", llGetOwner(), ""); llOwnerSay (getModeDescription());
}
// sends the known restrictions (again) to the RL-viewer // (call this functions on login) reinforceKnownRestrictions() {
integer i;
integer len=llGetListLength(lRestrictions);
string restr;
debug("kSource=" + (string) kSource);
for (i=0; i<len; ++i)
{
restr=llList2String(lRestrictions, i);
debug("restr=" + restr);
sendRLCmd(restr+"=n");
if (restr=="@unsit")
{
loginPendingForceSit = TRUE;
}
}
}
// send a ping request and start a timer pingWorldObjectIfUnderRestrictions() {
loginWaitingForPong = FALSE;
if (kSource != NULL_KEY)
{
ack("ping", kSource, "ping", "ping");
timerTickCounter = 0;
llSetTimerEvent(1.0);
lastPingTime = llGetUnixTime();
loginWaitingForPong = TRUE;
}
}
// Handle commands HandleCommand(string message, key id) {
list templist = llParseString2List(llToLower(message), [" "], []); string cmd = llList2String(templist, 0);
if(cmd == "relay")
{
if(id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1))))
{
integer change = 0;
string second = llList2String(templist, 1);
if(kSource != NULL_KEY && id == llGetOwner())
{
llOwnerSay("You cannot change relay modes while the relay is locked.");
return;
}
if(id == ownerkey && (second == "secondaries" || second == "sec"))
{
string third = llList2String(templist, 2);
if(third == "on" || third == "auto" || (third == "" && !secaccess))
{
secaccess = 1;
llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");
}
else
{
secaccess = 0;
llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");
}
}
else if(second == "on" || second == "auto")
{
nMode = MODE_AUTO;
change = 1;
}
else if(second == "off")
{
nMode = MODE_OFF;
change = 1;
}
else if(second == "ask")
{
nMode = MODE_ASK;
change = 1;
}
else if(second == "" || second == "mode")
{
nMode++;
if(nMode > 2) nMode = 0;
change = 1;
}
if(change)
{
setby = NULL_KEY;
if (nMode == MODE_OFF)
{
llSetTimerEvent(0.0);
releaseRestrictions();
}
else
{
llSetTimerEvent((float)PING_INTERVAL);
if(nMode == MODE_AUTO) setby = id;
}
if(id == llGetOwner())
llOwnerSay(getModeDescription());
else
llSay(0, getModeDescription());
llMessageLinked(LINK_THIS, 63, nullstr, nullstr);
}
}
else if(id == llGetOwner())
{
llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it.");
}
}
}
default {
state_entry()
{
// Request owner list from the collar
llMessageLinked(LINK_THIS, 47, nullstr, nullstr);
// Reset the plugin list
llMessageLinked(LINK_THIS, 62, nullstr, nullstr);
init();
}
// Handle messages from the collar script
link_message(integer sender, integer num, string str, key id)
{
if(num == 47)
{
list templist = llParseString2List(str, [","], []);
integer x;
integer count = llGetListLength(templist);
// Handle owner list reply
ownerkey = id;
secowners = [];
for(x=0;x<count;x++)
{
secowners = secowners + [ (key)llList2String(templist, x) ];
}
}
// Prefixless commands
else if(num == 48)
{
// Handle Commands on the public or alternate channel
HandleCommand(str, id);
}
else if(num == 33 && id != nullkey)
{
// Collar script is giving us an owner
ownerkey = id;
}
else if(num == 34 && id != nullkey)
{
// Collar script is giving us a secondary owner
secowners = secowners + [ id ];
}
else if(num == 35)
{
// Collar script is clearing owners
ownerkey = nullkey;
secowners = [];
}
else if(num == 36)
{
// Collar script is clearing secondary owners
secowners = [];
}
// Handle plugin update
else if(num == 62)
{
string buttons = "Relay Mode";
if(str == nullstr && (id == nullstr || id == nullkey))
{
// Add for owner and owners (key)
llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);
// Add for sub and unowned sub (key)
llMessageLinked(LINK_SET, 63, buttons, nullstr);
}
}
else if(num == 65)
{
if((integer)str == TRUE)
lockstatus = 1;
else
lockstatus = 0;
}
else if(num == 66) //== Safeword, unlock
{
nMode = MODE_OFF;
releaseRestrictions();
llOwnerSay(getModeDescription());
}
}
attach(key id)
{
if(id == NULL_KEY)
llOwnerSay("@clear");
}
on_rez(integer start_param)
{
// relogging, we must refresh the viewer and ping the object if any
// if mode is not OFF, fire all the stored restrictions
if (nMode)
{
reinforceKnownRestrictions();
pingWorldObjectIfUnderRestrictions();
llSetTimerEvent((float)PING_INTERVAL);
}
// remind the current mode to the user
llOwnerSay(getModeDescription());
}
timer()
{
timerTickCounter++;
debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG || lastPingTime + PING_INTERVAL <= llGetUnixTime()))
{
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
loginWaitingForPong = FALSE;
loginPendingForceSit = FALSE;
releaseRestrictions();
}
if (loginPendingForceSit)
{
integer agentInfo = llGetAgentInfo(llGetOwner());
if (agentInfo & AGENT_SITTING)
{
loginPendingForceSit = FALSE;
debug("is sitting now");
}
else if (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
{
llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
loginPendingForceSit = FALSE;
releaseRestrictions();
}
else
{
sendRLCmd ("@sit:"+(string)kSource+"=force");
}
}
if(sPendingId != NULL_KEY)
{
if(sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
{
llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], DIALOG_CHANNEL);
sPendingId = NULL_KEY;
sPendingName = "";
sPendingMessage = "";
}
}
if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
{
pingWorldObjectIfUnderRestrictions();
llSetTimerEvent((float)PING_INTERVAL);
}
}
listen(integer channel, string name, key id, string message)
{
if (channel==RLVRS_CHANNEL)
{
debug("LISTEN: " + message);
if (!verifyWeAreTarget(message))
{
return;
}
if (nMode== MODE_OFF)
{
debug("deactivated - ignoring commands");
return; // mode is 0 (off) => reject
}
if (!isObjectNear(id)) return;
debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);
if (kSource != NULL_KEY && kSource != id)
{
debug("already used by another object => reject");
return;
}
loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request
if (!isObjectKnow(id))
{
debug("asking for permission because kSource is NULL_KEY");
if (!verifyPermission(id, name, message))
{
return;
}
}
debug("Executing: " + (string) kSource);
execute(name, id, message);
}
else if (channel==DIALOG_CHANNEL)
{
if (id != llGetOwner())
{
return; // only accept dialog responses from the owner
}
if (sPendingId!=NULL_KEY)
{
if (message=="Yes") // pending request authorized => process it
{
execute(sPendingName, sPendingId, sPendingMessage);
}
else if(kSource == sPendingId)
releaseRestrictions();
// clear pending request
sPendingName="";
sPendingId=NULL_KEY;
sPendingMessage="";
}
}
}
changed(integer change)
{
if (change & CHANGED_OWNER)
{
llResetScript();
}
}
}
</lsl>