Difference between revisions of "LSL Protocol/Restrained Love Relay/Other Implementations/Felis Darwin's Amethyst Plugin"

From Second Life Wiki
Jump to navigation Jump to search
m (<lsl> tag to <source>)
 
(21 intermediate revisions by 3 users not shown)
Line 1: Line 1:
''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.''
This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars.  '''At the moment this plugin may nullify commands issued by the standard Amethyst RLV Plugin.''' However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)


<lsl>
This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet.  It also adds some nice features for Owners set on the collar, such as automatic exceptions to certain restrictions (IM and TP) and the ability to "lock" the plugin's mode so that the wearer cannot turn it off.  It currently complies with Marine's v1.100 spec.
 
Finally, this implementation does ''not'' include support for multiple control objects, due to the overhead necessary for the plugin to interact with the Amethyst collar system.  If you absolutely need this functionality I recommend using one of the many free Relay HUDs.
 
== Current Changelog ==
Current version: v0.6
 
===v0.6===
 
:- Included support in the owner exceptions for the new "secure" IM and teleport restrictions.  When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.
 
:- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec.
:- Changed the release routine so it releases restrictions in reverse order, per the new spec.
:- If the user is seated when they receive an "@unsit" restriction it will assume the issuing object is the sit target, until told otherwise, per the new spec.
:- Relay will now silently reject malformed commands which lack a variable (e.g. "@version" instead of "!version")
 
:- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.
 
:- Removed a few unused variables and irrelevant comments that were lying about.
:- A few other script cleanups that should add a little more working memory to the relay.
 
===v0.5===
 
:- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.
 
===v0.4===
 
:- Implemented changes to bring the relay up to 1040 spec:
::+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel
::+ Made sure the distance check fails when the controlling object has been de-rezzed/removed
::+ Stopped groupless objects from immediately passing the trustworthy check on groupless land
::+ Added support for the "!who" meta-command (tells you who is in control of the device), and updated the user dialog messages accordingly
::+ Added support fo the "!handover" meta-command (allows devices to "hand over" controlled residents)
 
:- Rewrote the rememberForceSit command to use less resources.
:- Removed the "!mode" meta-command I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise
:- Went through the script and combined a number of if statements so as to reduce memory usage and overhead
:- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO).  This has reduced memory usage a bit, and has allowed me to cram in more features.
 
:- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.
 
:- Added in a whitelist/blacklist feature.  When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never".  This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case)  If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.
 
:- Removed the as-yet unused "deny restrictions" list to make room for the above feature.  If I figure out a way to fit it in while keeping the script compilable I'll do it.
 
 
===v0.3===
   
:- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)
   
:- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction.  Currently it has been expanded to include the "Teleport Request"/tplure restriction.
 
:- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode.  The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string.  If it does, the restriction is denied.  (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".
 
:- Removed the "stripping" deny mode, as the above method will handle the same thing.
   
:- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory.  The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).
 
:- Added a means to turn off the "ping" requirement
 
 
===v0.2 and v0.1===
 
No proper changelog exists.  v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.
 
== Current Source Code ==
 
<source lang="lsl2">


//== RestrainedLife Viewer Relay Script
//== RestrainedLife Viewer Relay Script
//==  by Felis Darwin
//==  by Felis Darwin
//== Based on Reference Implementation by Marine Kelley
//== Based on Reference Implementation by Marine Kelley
//== (full credits of that version below)


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 = [];


//~ RestrainedLife Viewer Relay Script example code
key setby = nullkey; //== Who set the RLV Relay status?
//~ By Marine Kelley
//~ 2008-02-03
//~ v1.1
//~ 2008-02-16 with fixes by Maike Short
//~ 2008-02-24 more fixes by Maike Short
//~ 2008-03-03 code cleanup by Maike Short
//~ 2008-03-05 silently ignore commands for removing restrictions if they are not active anyway


//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
integer lockstatus; //== Has the collar been locked by the RLV plugin?
//~ completeness or performance. It may only be distributed in its full source code,
//~ this header and disclaimer and is not to be sold.
//~ * Possible improvements
//~ Do some error checking
//~ Handle more than one object
//~ Periodically check that the in-world objects are still around, when one is missing purge its restrictions
//~ Manage an access list
//~ Reject some commands if not on access list (force remove clothes, force remove attachments...)
//~ and much more...


string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from


// ---------------------------------------------------
// ---------------------------------------------------
Line 35: Line 107:
// ---------------------------------------------------
// ---------------------------------------------------
   
   
integer RLVRS_PROTOCOL_VERSION = 1014; // version of the protocol, stated on the specification page
integer RLVRS_PROTOCOL_VERSION = 1100; // version of the protocol, stated on the specification page
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
   
   
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 = 300; // 300 is 5 minutes
integer PERMISSION_DIALOG_TIMEOUT = 30;
integer PERMISSION_DIALOG_TIMEOUT = 30;
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;


integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
 
integer MODE_OFF = 0;
integer MODE_ASK = 1;
integer MODE_AUTO = 2;
 
 
// ---------------------------------------------------
// ---------------------------------------------------
//                      Variables
//                      Variables
// ---------------------------------------------------
// ---------------------------------------------------
integer nMode;
   
   
integer nMode;
list lRestrictions; // restrictions currently applied (without the "=n" part)
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
key kSource;        // UUID of the object I'm commanded by, always equal to nullkey if lRestrictions is empty, always set if not
key kController;    // UUID of the person controlling the object, if passed to us by the !who command


list WhiteBlack = []; //== A combined white/black list of residents.  Whitelisting exempts them from ask mode.  Blacklisting prevents their objects from even interacting with you.
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)
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)
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)
string sPendingMessage; // message of pending request (first request of a session in mode 1)
integer sPendingTime;
integer sPendingTime;
 
 
// used on login
// used on login
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit)
Line 75: Line 140:
integer loginPendingForceSit;
integer loginPendingForceSit;


key    lastForceSitDestination;
integer noping = 0;
key    lastForceSitDestination = nullkey;
integer lastForceSitTime;
integer lastForceSitTime;
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command


// ---------------------------------------------------
// ---------------------------------------------------
Line 82: Line 151:
// ---------------------------------------------------
// ---------------------------------------------------
   
   
 
debug(string x)
debug(string x)
{
{
//    llOwnerSay("DEBUG: " + x);
    if (DEBUG)
    {
        llOwnerSay("DEBUG: " + x);
    }
}
}
   
   
Line 91: Line 163:
ack(string cmd_id, key id, string cmd, string ack)
ack(string cmd_id, key id, string cmd, string ack)
{
{
     llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
     if(id != nullkey)
}
        llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);
 
// cmd begins with a '@'
sendRLCmd(string cmd)
{
    llOwnerSay(cmd);
}
}


// get current mode as string
// get current mode as string
Line 104: Line 172:
{
{
     if (nMode == 0) return "RLV Relay is OFF";  
     if (nMode == 0) return "RLV Relay is OFF";  
     else if (nMode == 1) return "RLV Relay is ON (permission needed)";  
     if (nMode == 1) return "RLV Relay is ON (permission needed)";
     else return "RLV Relay is ON (auto-accept)";  
     if (nMode == 2) return "RLV Relay is ON (auto-accept)";
    return "RLV Relay is ON (owners + whitelist only)";
}
}
 
// 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
//              Permission Handling
// ---------------------------------------------------
// ---------------------------------------------------
 
// are we already under command by this object?
// are we already under command by this object?
integer isObjectKnow(key id)
integer isObjectKnow(key id)
{
{
    // first some error handling
     // are we not under command by any object but were we forced to sit on this object recently?
    if (id == NULL_KEY)
     if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))
    {
        return FALSE;
    }
 
     // are we already under command by this object?
     if (kSource == id)
     {
     {
         return TRUE;
         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;
     return FALSE;
}
}
 
 
// check whether the object is in llSay distance.
// check whether the object is in llSay distance.
// The specification requires llSay instead of llShout or llRegionSay
// The specification requires llSay instead of llShout or llRegionSay
Line 164: Line 202:
     vector myPosition = llGetRootPosition();
     vector myPosition = llGetRootPosition();
     list temp = llGetObjectDetails(id, ([OBJECT_POS]));
     list temp = llGetObjectDetails(id, ([OBJECT_POS]));
     vector objPostition = llList2Vector(temp,0);
     vector objPosition = llList2Vector(temp,0);
     float distance = llVecDist(objPostition, myPosition);
    if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;
     return distance <= MAX_OBJECT_DISTANCE;
     float distance = llVecDist(objPosition, myPosition);
     return distance <= 100;
}
}
 
// do a basic check on the identity of the object trying to issue a command
// do a basic check on the identity of the object trying to issue a command
integer isObjectIdentityTrustworthy(key id)
integer isObjectIdentityTrustworthy(key id)
Line 176: Line 215:
     key object_owner=llGetOwnerKey(id);
     key object_owner=llGetOwnerKey(id);
     key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
     key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);
 
     debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
     debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
     debug("group= " + (string) parcel_group + " / " + (string) object_group);
     debug("group= " + (string) parcel_group + " / " + (string) object_group);
 
     if (object_owner==llGetOwner ()        // IF I am the owner of the object
     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_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
       || (object_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?
      || ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?
      || (object_group==parcel_group && object_group != nullkey)      // OR its group is the same as the parcel I'm on
     )
     )
     {
     {
Line 189: Line 230:
     return FALSE;
     return FALSE;
}
}
 
 
// Is this a simple request for information or a meta command like !release?
// Is this a simple request for information or a meta command like !release?
integer isSimpleRequest(list list_of_commands)  
integer isSimpleRequest(list list_of_commands)  
Line 196: Line 237:
     integer len = llGetListLength(list_of_commands);
     integer len = llGetListLength(list_of_commands);
     integer i;
     integer i;
 
    debug("Checking simplicity of commands...");
     // now check every single atomic command
     // now check every single atomic command
     for (i=0; i < len; ++i)
     for (i=0; i < len; ++i)
Line 203: Line 246:
         if (!isSimpleAtomicCommand(command))
         if (!isSimpleAtomicCommand(command))
         {
         {
          debug("Command "+ command +" fails simplicity check.");
           return FALSE;
           return FALSE;
         }
         }
     }
     }
 
     // all atomic commands passed the test
     // all atomic commands passed the test
     return TRUE;
     return TRUE;
}
}
 
// is this a simple atmar command
// is this a simple atmar command
// (a command which only queries some information or releases restrictions)
// (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)
// (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command)
integer isSimpleAtomicCommand(string cmd)
integer isSimpleAtomicCommand(string cmd)
{
{  
     // check right hand side of the "=" - sign
     // check right hand side of the "=" - sign
     integer index = llSubStringIndex (cmd, "=");
     integer index = llSubStringIndex (cmd, "=");
     if (index > -1) // there is a "="
     // check for a number after the "="
    {
    string param = llGetSubString (cmd, index + 1, -1);
        // check for a number after the "="
    if ((((((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) || param == "y" || param == "rem") || index == -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number) or empty?
        string param = llGetSubString (cmd, index + 1, -1);
        if ((integer)param!=0 || param=="0") // 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;
         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".
     // this one is not "simple".
     return FALSE;
     return FALSE;
}
}
 
// If we already have commands from this object pending
// If we already have commands from this object pending
// because of a permission request dialog, just add the
// because of a permission request dialog, just add the
Line 261: Line 280:
integer tryToGluePendingCommands(key id, string commands)
integer tryToGluePendingCommands(key id, string commands)
{
{
     if ((sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
     if (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)
     {
     {
         debug("Gluing " + sPendingMessage + " with " + commands);
         debug("Gluing " + sPendingMessage + " with " + commands);
         sPendingMessage = sPendingMessage + "|" + commands;
         sPendingMessage = (sPendingMessage=nullstr) + sPendingMessage + "|" + commands;
         return TRUE;
         return TRUE;
     }
     }
     return FALSE;
     return FALSE;
}
}
 
// verifies the permission. This includes mode  
// verifies the permission. This includes mode  
// (off, permission, auto) of the relay and the
// (off, permission, auto) of the relay and the
Line 275: Line 294:
integer verifyPermission(key id, string name, string message)
integer verifyPermission(key id, string name, string message)
{
{
     // is it switched off?
     debug("Verifying permission for command "+ message);
    if (nMode == MODE_OFF)
      
    {
        return FALSE;
     }
 
     // extract the commands-part
     // extract the commands-part
     list tokens = llParseString2List (message, [","], []);
     list tokens = llParseString2List (message, [","], []);
     if (llGetListLength (tokens) < 3)
     if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))
     {
     {
        kController = nullkey;
         return FALSE;
         return FALSE;
     }
     }
     string commands = llList2String(tokens, 2);
     string commands = llList2String(tokens, 2);
     list list_of_commands = llParseString2List(commands, ["|"], []);
     list list_of_commands = llParseString2List(commands, ["|"], []);
 
     // accept harmless commands silently
     // accept harmless commands silently
     if (isSimpleRequest(list_of_commands))
     if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))
     {
     {
        debug("simple command or Owner in Whitelist, executing.");
         return TRUE;
         return TRUE;
     }
     }
 
     // if we are already having a pending permission-dialog request for THIS object,
     // 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.
     // just add the new commands at the end of the pending command list.
     if (tryToGluePendingCommands(id, commands))
     if (tryToGluePendingCommands(id, commands))
     {
     {
         return FALSE; //== We have to return false here, otherwise the commands are executed
        debug("Appending to store of commands pending approval.");
         return FALSE; //== Glue the commands and process them later
     }
     }
 
     // check whether this object belongs here
     // check whether this object belongs here
     integer trustworthy = isObjectIdentityTrustworthy(id);
     integer trustworthy = isObjectIdentityTrustworthy(id);
     string warning = "";
     string warning = nullstr;
     if (!trustworthy)
     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.";
         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.
     // ask in permission-request-mode and/OR in case the object identity is suspisous.
     if (nMode == MODE_ASK || !trustworthy)
     if ((nMode == 1 || !trustworthy))
     {
     {
         sPendingId=id;
         sPendingId=id;
Line 318: Line 336:
         sPendingMessage=message;
         sPendingMessage=message;
         sPendingTime = llGetUnixTime();
         sPendingTime = llGetUnixTime();
 
       
         if(llKey2Name(llGetOwnerKey(id)) != "") //== Sometimes the name isn't very revealing, so we should have owner info if possible
        list opts = ["Yes", "No"];
             name += " (owned by "+ llKey2Name(llGetOwnerKey(id)) +")";
       
 
        llSetTimerEvent(2.0);
         llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
               
         if(llKey2Name(llGetOwnerKey(id)) != nullstr)
        {
             name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
            opts += ["Never", "Always"];
        }
           
        if(llKey2Name(kController) != nullstr)
        {
            name = llKey2Name(kController) +", using "+ name +",";
            opts += ["Never"];
        }
       
         llDialog (llGetOwner(), name + " would like control your viewer." + warning + "\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);
         debug("Asking for permission");
         debug("Asking for permission");
         return FALSE;
         return FALSE;
Line 328: Line 359:
     return TRUE;
     return TRUE;
}
}
 
 
// ---------------------------------------------------
// ---------------------------------------------------
//              Executing of commands
//              Executing of commands
// ---------------------------------------------------
// ---------------------------------------------------
 
// execute a non-parsed message
// execute a non-parsed message
// this command could be denied here for policy reasons, (if it were implemenetd)
// this command could be denied here for policy reasons, (if it were implemenetd)
Line 339: Line 370:
execute(string name, key id, string message)
execute(string name, key id, string message)
{
{
    stop = 0;
   
     list tokens=llParseString2List (message, [","], []);
     list tokens=llParseString2List (message, [","], []);
     if (llGetListLength (tokens)==3) // this is a normal command
     if (llGetListLength (tokens)==3) // this is a normal command
Line 353: Line 386:
             for (i=0; i<len; ++i) // execute every command one by one
             for (i=0; i<len; ++i) // execute every command one by one
             {
             {
                if(stop) return;
               
                 // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
                 // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
                 command=llList2String (list_of_commands, i);
                 command=llList2String (list_of_commands, i);
                 prefix=llGetSubString (command, 0, 0);
                 prefix=llGetSubString (command, 0, 0);
               
                 if (prefix==PREFIX_RL_COMMAND) // this is a RL command
                if(command == "@clear")
                {
                    releaseRestrictions();
                    ack(cmd_id, id, command, "ok"); 
                }
                 else if (prefix=="@") // this is a RL command
                 {
                 {
                     executeRLVCommand(cmd_id, id, command);
                     executeRLVCommand(cmd_id, id, command);
                 }
                 }
                 else if (prefix==PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
                 else if (prefix=="!") // this is a metacommand, aimed at the relay itself
                 {
                 {
                     executeMetaCommand(cmd_id, id, command);
                     executeMetaCommand(cmd_id, id, command);
Line 369: Line 409:
     }
     }
}
}
 
// executes a command for the restrained life viewer  
// executes a command for the restrained life viewer  
// with some additinal magic like book keeping
// with some additinal magic like book keeping
Line 379: Line 419:
     string param=llList2String (tokens_command, 1); // 2222
     string param=llList2String (tokens_command, 1); // 2222
     integer ind=llListFindList (lRestrictions, [behav]);
     integer ind=llListFindList (lRestrictions, [behav]);
 
    debug("behav = "+ behav +"; param=" + param);
   
    //== Stop the public chat exploits, and disregard commands issued with no variable (clear is handled elsewhere)
    if(param == nullstr || (~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && ((integer)param <= 0))
    {
        ack(cmd_id, id, command, "ko");
        return; 
    }
     if (param=="n" || param=="add") // add to lRestrictions
     if (param=="n" || param=="add") // add to lRestrictions
     {
     {
         if (ind<0) lRestrictions+=[behav];
         if (ind<0)
        {
            if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)
            {
                llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");
            }
            else
                lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];
               
            if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions
            {
                //== Trim off the _sec part so exceptions can be passed
                if(~llSubStringIndex(behav, "_sec"))
                    behav = llGetSubString(behav, 0, -5);
               
                if(ownerkey != nullkey)
                    llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");
                if(secaccess || ownerkey == nullkey)
                {
                    integer i;
                    for(i = 0; i < llGetListLength(secowners); i++)
                        llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");
                }
            }
        }


         if(kSource == NULL_KEY) //== If this is the first time we're processing commands from this, lock the relay
         if(kSource == nullkey)
             llOwnerSay("@detach=n");
        {
 
            llSetTimerEvent(2.0);
         kSource=id; // we know that kSource is either NULL_KEY or id already
             if(!lockstatus)
                llOwnerSay("@detach=n");
        }
               
         kSource=id; // we know that kSource is either nullkey or id already
     }
     }
     else if (param=="y" || param=="rem") // remove from lRestrictions
     else if (param=="y" || param=="rem") // remove from lRestrictions
     {
     {
         if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
         if (ind>-1)
         if (llGetListLength (lRestrictions)==0)
                lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);
               
        //== Unlisted Owner Exceptions are NEVER removed, for safety
         //==  Nor is the public chat exploit fixer
       
        //== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners
        else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")
         {
         {
             kSource=NULL_KEY;
             ack(cmd_id, id, command, "ko");
            llOwnerSay("@detach=y"); //== Unlock the relay if we are no longer under the control of this device
            return;
         }
         }
       
        if (llGetListLength (lRestrictions) == 0 && !lockstatus)
            llOwnerSay("@detach=y");
       
     }
     }
 
    workaroundForAtClear(command);
     rememberForceSit(command);
     rememberForceSit(command);
     sendRLCmd(command); // execute command
     if(llGetListLength(lRestrictions) == 1)
        llOwnerSay("@a-relay=n");
    llOwnerSay(command); // execute command
     ack(cmd_id, id, command, "ok"); // acknowledge
     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
// remembers the time and object if this command is a force sit
//== Changed to work a little differently
rememberForceSit(string command)
rememberForceSit(string command)
{
{
     list tokens_command=llParseString2List (command, ["="], []);
     command = llStringTrim(command, STRING_TRIM);
     string behav=llList2String (tokens_command, 0); // @sit:<uuid>
     string param = llGetSubString(command, -6, -1);
    string param=llList2String (tokens_command, 1); // force
 
     if (param != "force")
     if (param != "=force")
     {
     {
        //== If we're seated on the kSource and it's restricting us, it's probably the sit target
        if(lastForceSitDestination == nullkey && (command == "@unsit=n" || command == "@unsit=add") && llGetAgentInfo(llGetOwner()) & AGENT_SITTING)
            lastForceSitDestination = kSource;
           
         return;
         return;
     }
     }
    string behav = llGetSubString(command, 0, 4);
    param = llGetSubString(command, 5, 40);


    tokens_command=llParseString2List(behav, [":"], []);
    behav=llList2String (tokens_command, 0); // @sit
    param=llList2String (tokens_command, 1); // <uuid>
     debug("'force'-command:" + behav + "/" + param);
     debug("'force'-command:" + behav + "/" + param);
     if (behav != "@sit")
 
    {
    //== If this somehow isn't a sit command (or there's no valid target) then return
     if(behav != "@sit:" || llStringLength(param) != 36)
         return;
         return;
     }
      
   
     lastForceSitDestination = (key) param;
     lastForceSitDestination = (key) param;
     lastForceSitTime = llGetUnixTime();
     lastForceSitTime = llGetUnixTime();
     debug("remembered force sit");
     debug("remembered force sit");
}
}
 
// executes a meta command which is handled by the relay itself
// executes a meta command which is handled by the relay itself
executeMetaCommand(string cmd_id, string id, string command)
executeMetaCommand(string cmd_id, string id, string command)
{
{
     if (command==PREFIX_METACOMMAND+"version") // checking relay version
     if (command=="!version") // checking relay version
     {
     {
         ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
         ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
     }
     }
     else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
     else if (command == "!implversion") // checking relay version
     {
     {
        ack(cmd_id, id, command, RLVRS_IMPL_VERSION);
    }
    else if (command=="!release") // release all the restrictions (end session)
    {
        ack(cmd_id, id, command, "ok");
        kSource = nullkey; //== So only one release message is sent
         releaseRestrictions();
         releaseRestrictions();
         ack(cmd_id, id, command, "ok");
    }
 
    //== Depreciated but still supported here because it's easy
    if (llGetSubString(command,0,4) == "!who/")
    {
         kController = (key)llGetSubString(command, 5, -1);
    }
    if (llGetSubString(command,0,9) == "!handover/")
    {
        list tokens = llParseString2List(command, ["/"], []);
        if(!llList2Integer(tokens, 2))
            releaseRestrictions();
        kSource = llList2Key(tokens,1);
        pingWorldObjectIfUnderRestrictions();
     }
     }
}
}
Line 459: Line 560:
releaseRestrictions ()
releaseRestrictions ()
{
{
     kSource=NULL_KEY;
    ack("Relay Release Notification", kSource, "!release", "ok");
     llOwnerSay("@detach=y"); //== Allow detach
 
     integer i;
     kSource=nullkey;
     integer len=llGetListLength (lRestrictions);
     if(!lockstatus)
    for (i=0; i<len; ++i)
        llOwnerSay("@detach=y");
 
    //== Do this in reverse order because 1) it saves memory, and 2) the spec says so
     integer len;
     for (len = (llGetListLength(lRestrictions) - 1); len >= 0; len--)
     {
     {
         sendRLCmd(llList2String (lRestrictions, i)+"=y");
         llOwnerSay(llList2String (lRestrictions, len)+"=y");
       
        if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,len)))
            llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, len),1,-1));
     }
     }
     lRestrictions = [];
     lRestrictions = [];
     loginPendingForceSit = FALSE;
     loginPendingForceSit = FALSE;
    lastForceSitDestination = nullkey;
    loginWaitingForPong = FALSE;
    llSetTimerEvent(0.0);
    ack("Relay Release Notification", sPendingId, "!release", "ok");
    llMessageLinked(LINK_SET, 356, nullstr, nullkey);
    if(kController != nullkey && sPendingId != nullkey)
        llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);
    sPendingId = nullkey;
    sPendingName = nullstr;
    sPendingMessage = nullstr;
    kController = nullkey;
}
}
 
   
   
// ---------------------------------------------------
// ---------------------------------------------------
Line 477: Line 600:
   
   
init() {
init() {
    debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());
     nMode=1;
     nMode=1;
     kSource=NULL_KEY;
     kSource=nullkey;
     lRestrictions=[];
     lRestrictions=[];
     sPendingId=NULL_KEY;
     sPendingId=nullkey;
     sPendingName="";
     sPendingName=nullstr;
     sPendingMessage="";
     sPendingMessage=nullstr;
     llListen (RLVRS_CHANNEL, "", "", "");
     llListen (-1812221819, nullstr, nullstr, nullstr);
     llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
     llListen (-1812220409, nullstr, llGetOwner(), nullstr);
     llOwnerSay (getModeDescription());
     llOwnerSay (getModeDescription());
}
}
 
// sends the known restrictions (again) to the RL-viewer
// sends the known restrictions (again) to the RL-viewer
// (call this functions on login)
// (call this functions on login)
Line 495: Line 619:
     integer len=llGetListLength(lRestrictions);
     integer len=llGetListLength(lRestrictions);
     string restr;
     string restr;
   
    if(len > 0)
    {
        llOwnerSay("@a-relay=n");
        llOwnerSay("@detach=n");
    }
   
     debug("kSource=" + (string) kSource);
     debug("kSource=" + (string) kSource);
     for (i=0; i<len; ++i)
     for (i=0; i<len; ++i)
Line 500: Line 631:
         restr=llList2String(lRestrictions, i);
         restr=llList2String(lRestrictions, i);
         debug("restr=" + restr);
         debug("restr=" + restr);
         sendRLCmd(restr+"=n");
         llOwnerSay(restr+"=n");
         if (restr=="@unsit")
         if (restr=="@unsit" && lastForceSitDestination != nullkey)
         {
         {
             loginPendingForceSit = TRUE;
             loginPendingForceSit = TRUE;
Line 507: Line 638:
     }
     }
}
}
 
// send a ping request and start a timer
// send a ping request and start a timer
pingWorldObjectIfUnderRestrictions()
pingWorldObjectIfUnderRestrictions()
{
{
     loginWaitingForPong = FALSE;
     loginWaitingForPong = FALSE;
     if (kSource != NULL_KEY)
     if (kSource != nullkey)
     {
     {
         ack("ping", kSource, "ping", "ping");
         ack("ping", kSource, "ping", "ping");
Line 518: Line 649:
         llSetTimerEvent(1.0);
         llSetTimerEvent(1.0);
         loginWaitingForPong = TRUE;
         loginWaitingForPong = TRUE;
    }
}
// Handle commands
HandleCommand(string message, key id)
{
    list templist = llParseString2List(llToLower(message), [" "], []);
    string cmd = llList2String(templist, 0);
    if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))
    {
        integer change = 0;
       
        string second = llList2String(templist, 1);
        string third = llList2String(templist, 2);
       
        if(kSource != nullkey && id == llGetOwner())
        {
            llOwnerSay("You cannot change relay modes while the relay is locked.");
            return; 
        }
       
        if(id == ownerkey && (second == "secondaries" || second == "sec"))
        {
            if(third == "on" || third == "auto" || (third == nullstr && !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((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")
        {
            if(third == "off" || (third == nullstr && !noping))
            {
                noping = 1;
                llWhisper(0,"Restrained Life Relay no longer requires regular object communication.  CAUTION: Relay will NOT detect if the control object has crashed or been removed, and in that instance will continue to enforce the last known restrictions until the wearer logs off.");
            }
            else
            {
                noping = 0;
                llWhisper(0,"Restrained Life Relay now requires regular object communication.");
            } 
        }
       
        if(second == "on" || second == "auto")
        {
            nMode = 2;
            change = 1;
        }
        if(second == "off")
        {
            nMode = 0;
            change = 1; 
        }
        if(second == "ask")
        {
            nMode = 1;
            change = 1; 
        }
        if(second == "owner" || second == "wl")
        {
            nMode = 3;
            change = 1; 
        }
       
        if(second == nullstr || second == "mode")
        {
            nMode++;
            if(nMode > 3) nMode = 0;
            change = 1; 
        }
       
        if(second == "wbclear")
        {
            WhiteBlack = [];
            llWhisper(0,"Relay Whitelist and Blacklist cleared.");
        }
       
        if(change)
        {
            setby = nullkey;
            if (nMode == 0)
            {
                llSetTimerEvent(0.0);
                releaseRestrictions();
                setby = nullkey;
            }
            else
            {
                llSetTimerEvent((float)PING_INTERVAL);
                if(nMode >= 2) setby = id;
            }
            if(id == llGetOwner())
                llOwnerSay(getModeDescription()); 
            else
                llSay(0, getModeDescription());
               
            llMessageLinked(LINK_THIS, 63, nullstr, nullstr);
        }
    }
    else if(cmd == "relay" && id == llGetOwner())
    {
        llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); 
     }
     }
}
}
Line 525: Line 765:
     state_entry()
     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();
         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 || num == 828)
        {
            if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply
                id = (key)(llGetSubString(id,0,35)); 
            // 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)
        {
            lockstatus = (integer)str;  
        }
        else if(num == 66) //== Safeword, unlock
        {
            releaseRestrictions();
            setby = nullkey;
            nMode = 0;
            llOwnerSay(getModeDescription());
        }
        else if(num == 355)
            reinforceKnownRestrictions();
    }
   
    attach(key id)
    {
        if(id == nullkey)
            llOwnerSay("@clear"); 
    }
   
     on_rez(integer start_param)
     on_rez(integer start_param)
     {
     {
Line 540: Line 864:
         llOwnerSay(getModeDescription());
         llOwnerSay(getModeDescription());
     }
     }
 
 
     timer()
     timer()
     {
     {
         timerTickCounter++;
         timerTickCounter++;  
       
         debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
         debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
         if (loginWaitingForPong && (timerTickCounter == LOGIN_DELAY_WAIT_FOR_PONG))
         if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG))
         {
         {
             llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
             llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");
             loginWaitingForPong = FALSE;
             loginWaitingForPong = FALSE;
             loginPendingForceSit = FALSE;
             loginPendingForceSit = FALSE;
             releaseRestrictions();
             releaseRestrictions();
         }
         }
 
         if (loginPendingForceSit)
         if (loginPendingForceSit)
         {
         {
Line 562: Line 887:
                 debug("is sitting now");
                 debug("is sitting now");
             }
             }
             else if (timerTickCounter == LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
             else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check
             {
             {
                 llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
                 llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
Line 568: Line 893:
                 releaseRestrictions();
                 releaseRestrictions();
             }
             }
             else
             else if(!loginWaitingForPong)
             {
             {
                 sendRLCmd ("@sit:"+(string)kSource+"=force");
                 llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");
             }
             }
         }
         }
 
       
         if(sPendingId != NULL_KEY) //== Handle dialog timeout
         if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
         {
         {
             if(sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
             llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);
            {
            sPendingId = nullkey;
                //== Pop a dialog up just so the wearer knows what happened
            sPendingName = nullstr;
                llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], DIALOG_CHANNEL);
            sPendingMessage = nullstr;
                sPendingId = NULL_KEY;
        }
                sPendingName = "";
   
                sPendingMessage = "";  
         if(timerTickCounter == 0 && !noping)
            }   
            pingWorldObjectIfUnderRestrictions();
         }
   
   
         if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
         if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)
         {
         {
            timerTickCounter = -1;
            if(!noping)
            {
                llSetTimerEvent((float)PING_INTERVAL);
                return;
            }
             llSetTimerEvent(0.0);
             llSetTimerEvent(0.0);
         }
         }
Line 594: Line 924:
     listen(integer channel, string name, key id, string message)
     listen(integer channel, string name, key id, string message)
     {
     {
         if (channel==RLVRS_CHANNEL)
         if (channel==-1812221819)
         {
         {
             if (!verifyWeAreTarget(message))
            debug("LISTEN: " + message);
           
            //=== ALWAYS accept a lone "!release" command, no matter the distance
            list tokens = llCSV2List(message);
             if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))
             {
             {
               return;
               return;
             }
             }
       
            tokens = [];
             if (nMode== MODE_OFF)
             if (nMode== 0)
             {
             {
                 debug("deactivated - ignoring commands");
                 debug("deactivated - ignoring commands");
                 return; // mode is 0 (off) => reject
                 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("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);
             if (kSource != nullkey && kSource != id)
             {
             {
                 debug("already used by another object => reject");
                 debug("already used by another object => reject");
                 return;
                 return;
             }
             }
 
           
            if(!loginPendingForceSit && sPendingId == nullkey)
            {
                llSetTimerEvent(0.0);
                llSetTimerEvent((float)PING_INTERVAL);
            }
             loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request
             loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request
 
//            timerTickCounter = -1;
             if (!isObjectKnow(id))
             if (!isObjectKnow(id))
            {
                 if(!verifyPermission(id, name, message))
                debug("asking for permission because kSource is NULL_KEY");
                 if (!verifyPermission(id, name, message))
                {
                     return;
                     return;
                }
            }
 
             debug("Executing: " + (string) kSource);
             debug("Executing: " + (string) kSource);
             execute(name, id, message);
             execute(name, id, message);
         }
         }
         else if (channel==DIALOG_CHANNEL)
         else if (channel==-1812220409 && id == llGetOwner())
         {
         {
            if (id != llGetOwner())
             if (sPendingId!=nullkey)
            {
             {              
                return; // only accept dialog responses from the owner
                 if (message=="Yes" || message == "Always") // pending request authorized => process it
            }
             if (sPendingId!=NULL_KEY)
             {
                 if (message=="Yes") // pending request authorized => process it
                 {
                 {
                    //== Process Whitelist entry
                    if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];
                    debug("Got approval of restrictions from wearer");
                     execute(sPendingName, sPendingId, sPendingMessage);
                     execute(sPendingName, sPendingId, sPendingMessage);
                 }
                 }
 
                else if(kSource == sPendingId)
                    releaseRestrictions();
               
                //== Process Blacklist entry
                if(kController == nullkey) kController = llGetOwnerKey(sPendingId);
                if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];
                 // clear pending request
                 // clear pending request
                 sPendingName="";
                 sPendingName=nullstr;
                 sPendingId=NULL_KEY;
                 sPendingId=nullkey;
                 sPendingMessage="";
                 sPendingMessage=nullstr;
             }
             }
         }
         }
     }
     }
      
     touch_start(integer num_detected)
    {
        // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
        key toucher=llDetectedKey(0);
        if (toucher==llGetOwner())
        {
            if (kSource != NULL_KEY)
            {
                llOwnerSay("Sorry, you cannot change the relay mode while it is locked.");
                return;
            }
            ++nMode;
            if (nMode>2) nMode=0;
            if (nMode==MODE_OFF) releaseRestrictions ();
            llOwnerSay (getModeDescription());
        }
    }
 
     changed(integer change)
     changed(integer change)
     {
     {
Line 677: Line 1,000:
     }
     }
}
}
</lsl>
 
</source>
 
[[Category:RestrainedLove|{{SUBPAGENAME}}]]

Latest revision as of 13:26, 25 January 2015

This is an Amethyst Plugin implementation of the Restrained Life Relay, meant for use in Amethyst collars. At the moment this plugin may nullify commands issued by the standard Amethyst RLV Plugin. However, it will instruct the Amethyst RLV plugin to re-issue its restrictions once the relay is no longer active. (Collar v6.7 and above)

This version adds a few important things to the Reference Implementation, like periodically pinging the restraining object (optional) and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. It also adds some nice features for Owners set on the collar, such as automatic exceptions to certain restrictions (IM and TP) and the ability to "lock" the plugin's mode so that the wearer cannot turn it off. It currently complies with Marine's v1.100 spec.

Finally, this implementation does not include support for multiple control objects, due to the overhead necessary for the plugin to interact with the Amethyst collar system. If you absolutely need this functionality I recommend using one of the many free Relay HUDs.

Current Changelog

Current version: v0.6

v0.6

- Included support in the owner exceptions for the new "secure" IM and teleport restrictions. When the relay sees these commands it will add IM or teleport restrictions, allowing the owner to receive IMs from the wearer or teleport them to safety, respectively.
- Cleaned up the Implementation Version text to be compatible with the new 1.100 spec.
- Changed the release routine so it releases restrictions in reverse order, per the new spec.
- If the user is seated when they receive an "@unsit" restriction it will assume the issuing object is the sit target, until told otherwise, per the new spec.
- Relay will now silently reject malformed commands which lack a variable (e.g. "@version" instead of "!version")
- Changed the "control attempt denied" dialog so it specifies what device the recipient was using.
- Removed a few unused variables and irrelevant comments that were lying about.
- A few other script cleanups that should add a little more working memory to the relay.

v0.5

- Added an "Owner + Whitelist" mode, which automatically allows all commands issued from devices owned by the collar's Owner(s) or those on the Whitelist, while automatically denying all other commands.

v0.4

- Implemented changes to bring the relay up to 1040 spec:
+ Added a more complete fix to an exploit allowing devices to force the user to speak on the public channel
+ Made sure the distance check fails when the controlling object has been de-rezzed/removed
+ Stopped groupless objects from immediately passing the trustworthy check on groupless land
+ Added support for the "!who" meta-command (tells you who is in control of the device), and updated the user dialog messages accordingly
+ Added support fo the "!handover" meta-command (allows devices to "hand over" controlled residents)
- Rewrote the rememberForceSit command to use less resources.
- Removed the "!mode" meta-command I had proposed because 1) nobody used it, and 2) there were compilation errors (due to too many else/ifs) otherwise
- Went through the script and combined a number of if statements so as to reduce memory usage and overhead
- Removed a number of global variables and replaced them with the values they previously held. Examples are the command prefix variables (PREFIX_RL_COMMAND, etc.) and the mode variables (MODE_OFF, MODE_ASK, MODE_AUTO). This has reduced memory usage a bit, and has allowed me to cram in more features.
- Objects owned by your owners (primary or secondary) are now automatically considered "trustworthy", thus bypassing the annoying "this object is not owned by the parcel owner blah blah" security message.
- Added in a whitelist/blacklist feature. When you are presented with a prompt to approve a control request from an object you will notice two additional buttons, "Always" and "Never". This will add the CONTROLLER of the object (who is pushing the buttons, so to speak) to the white/blacklist if that information is known. (It will mention "so and so using X" if this is the case) If the controller ISN'T known then clicking either of these buttons will add the OWNER of the object to the white/blacklist.
- Removed the as-yet unused "deny restrictions" list to make room for the above feature. If I figure out a way to fit it in while keeping the script compilable I'll do it.


v0.3

- Upgraded implementation version to 1.021. (Was already compliant in v0.2, but the version reply is now updated)
- Modified the "Send IM" exception so that it is now generalized and will work for any specified restriction. Currently it has been expanded to include the "Teleport Request"/tplure restriction.
- Added in a means of disallowing certain restrictions, beyond just the "stripping" deny mode. The list (denyRestrictions) takes strings, and checks each restriction to see if it contains that string. If it does, the restriction is denied. (e.g. the entry "tp" would prohibit all tp-related restrictions like accepttp, tploc, tplure, sittp, etc.) Adding "=force" to the end of an entry will limit it to only work on "force" commands, e.g. "detach:skirt=force" or even "detach=force".
- Removed the "stripping" deny mode, as the above method will handle the same thing.
- Got rid of the separate list for restriction exceptions, originally added as a means of saving available memory. The code will now simply ignore exceptions if it runs to low on memory (and will notify the wearer of this).
- Added a means to turn off the "ping" requirement


v0.2 and v0.1

No proper changelog exists. v0.2 was the first version to implement the now-optional "periodic ping" requirement, requiring furniture to respond to ping requests in order to keep the relay active.

Current Source Code

//== RestrainedLife Viewer Relay Script
//==  by Felis Darwin
//== Based on Reference Implementation by Marine Kelley

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 = nullkey; //== Who set the RLV Relay status?

integer lockstatus; //== Has the collar been locked by the RLV plugin?

string ownerexcept = "@sendim @sendim_sec @tplure @tplure_sec"; //== List of restrictions owner (not wearer) will always be exempt from

// ---------------------------------------------------
//                     Constants
// ---------------------------------------------------
 
integer RLVRS_PROTOCOL_VERSION = 1100; // version of the protocol, stated on the specification page
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation (Amethyst Plugin version)";
 
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
 
integer PERMISSION_DIALOG_TIMEOUT = 30;
 
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;

integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit
 
// ---------------------------------------------------
//                      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 nullkey if lRestrictions is empty, always set if not
key kController;    // UUID of the person controlling the object, if passed to us by the !who command

list WhiteBlack = []; //== A combined white/black list of residents.  Whitelisting exempts them from ask mode.  Blacklisting prevents their objects from even interacting with you.
 
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;

integer noping = 0;
 
key     lastForceSitDestination = nullkey;
integer lastForceSitTime;

integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command

// ---------------------------------------------------
//               Low Level Communication
// ---------------------------------------------------
 
 
debug(string x)
{
    if (DEBUG)
    {
        llOwnerSay("DEBUG: " + x);
    }
}
 
// acknowledge or reject
ack(string cmd_id, key id, string cmd, string ack)
{
    if(id != nullkey)
        llShout(-1812221819, cmd_id + "," + (string)id + "," + cmd + "," + ack);
}
 

// get current mode as string
string getModeDescription()
{
    if (nMode == 0) return "RLV Relay is OFF"; 
    if (nMode == 1) return "RLV Relay is ON (permission needed)";
    if (nMode == 2) return "RLV Relay is ON (auto-accept)"; 
    return "RLV Relay is ON (owners + whitelist only)";
}
 
// ---------------------------------------------------
//               Permission Handling
// ---------------------------------------------------
 
// are we already under command by this object?
integer isObjectKnow(key id)
{
    // are we not under command by any object but were we forced to sit on this object recently?
    if (id != nullkey && (kSource == id || ((kSource == nullkey) && (id == lastForceSitDestination) && (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime()))))
    {
        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 objPosition = llList2Vector(temp,0);
    if(temp == []) objPosition = <1000.0, 1000.0, -1000.0>;
    float distance = llVecDist(objPosition, myPosition);
    return distance <= 100;
}
 
// 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_owner==ownerkey && ownerkey != nullkey) //== Is this my owner's stuff?
      || ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?
      || (object_group==parcel_group && object_group != nullkey)       // 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, "=");
    // 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) || param == "y" || param == "rem") || index == -1) || llSubStringIndex(cmd, "!") == 0 || cmd == "@clear") // is it an integer (channel number) or empty?
    {
        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 (kSource == nullkey && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)
    {
        debug("Gluing " + sPendingMessage + " with " + commands);
        sPendingMessage = (sPendingMessage=nullstr) + 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);
    
    // extract the commands-part
    list tokens = llParseString2List (message, [","], []);
    if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]) || (nMode == 3 && llGetOwnerKey(id) != ownerkey && !~llListFindList(secowners, [llGetOwnerKey(id)]) && !~llListFindList(WhiteBlack,[llGetOwnerKey(id)])))
    {
        kController = nullkey;
        return FALSE;
    }
    string commands = llList2String(tokens, 2);
    list list_of_commands = llParseString2List(commands, ["|"], []);
 
    // accept harmless commands silently
    if (isSimpleRequest(list_of_commands) || ~llListFindList(WhiteBlack, [llGetOwnerKey(id)]))
    {
        debug("simple command or Owner in Whitelist, executing.");
        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 = nullstr;
    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 == 1 || !trustworthy))
    {
        sPendingId=id;
        sPendingName=name;
        sPendingMessage=message;
        sPendingTime = llGetUnixTime();
        
        list opts = ["Yes", "No"];
        
        llSetTimerEvent(2.0);
                
        if(llKey2Name(llGetOwnerKey(id)) != nullstr)
        {
            name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
            opts += ["Never", "Always"];
        }
            
        if(llKey2Name(kController) != nullstr)
        {
            name = llKey2Name(kController) +", using "+ name +",";
            opts += ["Never"];
        }
        
        llDialog (llGetOwner(), name + " would like control your viewer." + warning + "\n\nDo you accept ?", llList2List(opts,0,3), -1812220409);
        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)
{
    stop = 0;
    
    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
            {
                if(stop) return;
                
                // 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(command == "@clear")
                {
                    releaseRestrictions();
                    ack(cmd_id, id, command, "ok");   
                }
                else if (prefix=="@") // this is a RL command
                {
                    executeRLVCommand(cmd_id, id, command);
                }
                else if (prefix=="!") // 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("behav = "+ behav +"; param=" + param);
    
    //== Stop the public chat exploits, and disregard commands issued with no variable (clear is handled elsewhere)
    if(param == nullstr || (~llSubStringIndex(behav, "@get") || ~llSubStringIndex(behav, "@findfolder") || ~llSubStringIndex(behav, "@version")) && ((integer)param <= 0))
    {
        ack(cmd_id, id, command, "ko");
        return;   
    } 
    if (param=="n" || param=="add") // add to lRestrictions
    {
        if (ind<0)
        {
            if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)
            {
                llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");
            }
            else
                lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];
                
            if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions
            {
                //== Trim off the _sec part so exceptions can be passed
                if(~llSubStringIndex(behav, "_sec"))
                    behav = llGetSubString(behav, 0, -5);
                
                if(ownerkey != nullkey)
                    llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");
                if(secaccess || ownerkey == nullkey)
                {
                    integer i;
                    for(i = 0; i < llGetListLength(secowners); i++)
                        llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");
                }
            }
        }

        if(kSource == nullkey)
        {
            llSetTimerEvent(2.0);
            if(!lockstatus)
                llOwnerSay("@detach=n");
        }
                
        kSource=id; // we know that kSource is either nullkey or id already
    }
    else if (param=="y" || param=="rem") // remove from lRestrictions
    {
        if (ind>-1)
                lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);
                
        //== Unlisted Owner Exceptions are NEVER removed, for safety
        //==  Nor is the public chat exploit fixer
        
        //== NOTE TO SELF: Find a memory-efficient way to add this protection for secowners
        else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")
        {
            ack(cmd_id, id, command, "ko");
            return;
        }
        
        if (llGetListLength (lRestrictions) == 0 && !lockstatus)
            llOwnerSay("@detach=y");
        
    }
 
    rememberForceSit(command);
    if(llGetListLength(lRestrictions) == 1)
        llOwnerSay("@a-relay=n");
    llOwnerSay(command); // execute command
    ack(cmd_id, id, command, "ok"); // acknowledge
}
 
 
// remembers the time and object if this command is a force sit
//== Changed to work a little differently
rememberForceSit(string command)
{
    command = llStringTrim(command, STRING_TRIM);
    string param = llGetSubString(command, -6, -1);

    if (param != "=force")
    {
        //== If we're seated on the kSource and it's restricting us, it's probably the sit target
        if(lastForceSitDestination == nullkey && (command == "@unsit=n" || command == "@unsit=add") && llGetAgentInfo(llGetOwner()) & AGENT_SITTING)
            lastForceSitDestination = kSource;
            
        return;
    }
 
    string behav = llGetSubString(command, 0, 4);
    param = llGetSubString(command, 5, 40);

    debug("'force'-command:" + behav + "/" + param);

    //== If this somehow isn't a sit command (or there's no valid target) then return
    if(behav != "@sit:" || llStringLength(param) != 36)
        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=="!version") // checking relay version
    {
        ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
    }
    else if (command == "!implversion") // checking relay version
    {
        ack(cmd_id, id, command, RLVRS_IMPL_VERSION);
    }
    else if (command=="!release") // release all the restrictions (end session)
    {
        ack(cmd_id, id, command, "ok");
        kSource = nullkey; //== So only one release message is sent
        releaseRestrictions();
    }

    //== Depreciated but still supported here because it's easy
    if (llGetSubString(command,0,4) == "!who/")
    {
        kController = (key)llGetSubString(command, 5, -1);
    }
    if (llGetSubString(command,0,9) == "!handover/")
    {
        list tokens = llParseString2List(command, ["/"], []);
        if(!llList2Integer(tokens, 2))
            releaseRestrictions();
        kSource = llList2Key(tokens,1);
        pingWorldObjectIfUnderRestrictions();
    }
}
 
// lift all the restrictions (called by !release and by turning the relay off)
releaseRestrictions ()
{
    ack("Relay Release Notification", kSource, "!release", "ok");

    kSource=nullkey;
    if(!lockstatus)
        llOwnerSay("@detach=y");

    //== Do this in reverse order because 1) it saves memory, and 2) the spec says so
    integer len;
    for (len = (llGetListLength(lRestrictions) - 1); len >= 0; len--)
    {
        llOwnerSay(llList2String (lRestrictions, len)+"=y");
        
        if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,len)))
            llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, len),1,-1));
    }
    lRestrictions = [];

    loginPendingForceSit = FALSE;
    lastForceSitDestination = nullkey;
    loginWaitingForPong = FALSE;
    llSetTimerEvent(0.0);
    ack("Relay Release Notification", sPendingId, "!release", "ok");

    llMessageLinked(LINK_SET, 356, nullstr, nullkey);

    if(kController != nullkey && sPendingId != nullkey)
        llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via " + sPendingName +".", [], 99);

    sPendingId = nullkey;
    sPendingName = nullstr;
    sPendingMessage = nullstr;
    kController = nullkey;
}
 
 
// ---------------------------------------------------
//            initialisation and login handling
// ---------------------------------------------------
 
init() {
    debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());
    nMode=1;
    kSource=nullkey;
    lRestrictions=[];
    sPendingId=nullkey;
    sPendingName=nullstr;
    sPendingMessage=nullstr;
    llListen (-1812221819, nullstr, nullstr, nullstr);
    llListen (-1812220409, nullstr, llGetOwner(), nullstr);
    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;
    
    if(len > 0)
    {
        llOwnerSay("@a-relay=n");
        llOwnerSay("@detach=n");
    }
    
    debug("kSource=" + (string) kSource);
    for (i=0; i<len; ++i)
    {
        restr=llList2String(lRestrictions, i);
        debug("restr=" + restr);
        llOwnerSay(restr+"=n");
        if (restr=="@unsit" && lastForceSitDestination != nullkey)
        {
            loginPendingForceSit = TRUE;
        }
    }
}
 
// send a ping request and start a timer
pingWorldObjectIfUnderRestrictions()
{
    loginWaitingForPong = FALSE;
    if (kSource != nullkey)
    {
        ack("ping", kSource, "ping", "ping");
        timerTickCounter = 0;
        llSetTimerEvent(1.0);
        loginWaitingForPong = TRUE;
    }
}

// Handle commands
HandleCommand(string message, key id)
{
    list templist = llParseString2List(llToLower(message), [" "], []);
    string cmd = llList2String(templist, 0);

    if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == nullkey || secaccess)) || (id == llGetOwner() && (setby == nullkey || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))
    {
        integer change = 0;
        
        string second = llList2String(templist, 1);
        string third = llList2String(templist, 2);
        
        if(kSource != nullkey && id == llGetOwner())
        {
            llOwnerSay("You cannot change relay modes while the relay is locked.");
            return;   
        }
        
        if(id == ownerkey && (second == "secondaries" || second == "sec"))
        {
            if(third == "on" || third == "auto" || (third == nullstr && !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((secaccess || id == ownerkey || (id == llGetOwner() && kSource == nullkey)) && second == "ping")
        {
            if(third == "off" || (third == nullstr && !noping))
            {
                noping = 1;
                llWhisper(0,"Restrained Life Relay no longer requires regular object communication.  CAUTION: Relay will NOT detect if the control object has crashed or been removed, and in that instance will continue to enforce the last known restrictions until the wearer logs off.");
            }
            else
            {
                noping = 0;
                llWhisper(0,"Restrained Life Relay now requires regular object communication."); 
            }  
        }
        
        if(second == "on" || second == "auto")
        {
            nMode = 2;
            change = 1;
        }
        if(second == "off")
        {
            nMode = 0;
            change = 1;   
        }
        if(second == "ask")
        {
            nMode = 1;
            change = 1;   
        }
        if(second == "owner" || second == "wl")
        {
            nMode = 3;
            change = 1;   
        }
        
        if(second == nullstr || second == "mode")
        {
            nMode++;
            if(nMode > 3) nMode = 0;
            change = 1;  
        }
        
        if(second == "wbclear")
        {
            WhiteBlack = [];
            llWhisper(0,"Relay Whitelist and Blacklist cleared.");
        }
        
        if(change)
        {
            setby = nullkey;
            if (nMode == 0)
            {
                llSetTimerEvent(0.0);
                releaseRestrictions();
                setby = nullkey;
            }
            else
            {
                llSetTimerEvent((float)PING_INTERVAL);
                if(nMode >= 2) setby = id;
            }
            if(id == llGetOwner())
                llOwnerSay(getModeDescription());   
            else
                llSay(0, getModeDescription());
                
            llMessageLinked(LINK_THIS, 63, nullstr, nullstr);
        }
    }
    else if(cmd == "relay" && 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 || num == 828)
        {
            if(llSubStringIndex(id,"|") != -1) //== Strip out the combo info from the 828 reply
                id = (key)(llGetSubString(id,0,35));   
            // 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)
        {
            lockstatus = (integer)str;  
        }
        else if(num == 66) //== Safeword, unlock
        {
            releaseRestrictions();
            setby = nullkey;
            nMode = 0;
            llOwnerSay(getModeDescription());
        }
        else if(num == 355)
            reinforceKnownRestrictions();
    }
    
    attach(key id)
    {
        if(id == nullkey)
            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();
        }
        // 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))
        {
            llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available or is not responding to pings.");
            loginWaitingForPong = FALSE;
            loginPendingForceSit = FALSE;
            releaseRestrictions();
        }
 
        if (loginPendingForceSit)
        {
            integer agentInfo = llGetAgentInfo(llGetOwner());
            if (agentInfo & AGENT_SITTING)
            {
                loginPendingForceSit = FALSE;
                debug("is sitting now");
            }
            else if (timerTickCounter >= PING_INTERVAL) //== Force Sit check
            {
                llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
                loginPendingForceSit = FALSE;
                releaseRestrictions();
            }
            else if(!loginWaitingForPong)
            {
                 llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");
            }
        }
        
        if(sPendingId != nullkey && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
        {
            llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);
            sPendingId = nullkey;
            sPendingName = nullstr;
            sPendingMessage = nullstr;
        }  
 
        if(timerTickCounter == 0 && !noping)
            pingWorldObjectIfUnderRestrictions(); 
 
        if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == nullkey)
        {
            timerTickCounter = -1;
            if(!noping)
            {
                llSetTimerEvent((float)PING_INTERVAL);
                return;
            }
            llSetTimerEvent(0.0);
        }
    }
 
    listen(integer channel, string name, key id, string message)
    {
        if (channel==-1812221819)
        {
            debug("LISTEN: " + message);
            
            //=== ALWAYS accept a lone "!release" command, no matter the distance
            list tokens = llCSV2List(message);
            if (!(llGetListLength(tokens) == 3 && llList2String(tokens, 1) == llGetOwner()) || (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release"))
            {
               return;
            }
            tokens = [];
 
            if (nMode== 0)
            {
                debug("deactivated - ignoring commands");
                return; // mode is 0 (off) => reject
            }
 
            debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);
 
            if (kSource != nullkey && kSource != id)
            {
                debug("already used by another object => reject");
                return;
            }
            
            if(!loginPendingForceSit && sPendingId == nullkey)
            {
                llSetTimerEvent(0.0);
                llSetTimerEvent((float)PING_INTERVAL);
            }
 
            loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request
//            timerTickCounter = -1;
 
            if (!isObjectKnow(id))
                if(!verifyPermission(id, name, message))
                    return;
 
            debug("Executing: " + (string) kSource);
            execute(name, id, message);
        }
        else if (channel==-1812220409 && id == llGetOwner())
        {
            if (sPendingId!=nullkey)
            {                
                if (message=="Yes" || message == "Always") // pending request authorized => process it
                {
                    //== Process Whitelist entry
                    if(message == "Always") WhiteBlack += [llGetOwnerKey(sPendingId)];
                    debug("Got approval of restrictions from wearer");
                    execute(sPendingName, sPendingId, sPendingMessage);
                }
                else if(kSource == sPendingId)
                    releaseRestrictions();
                
                //== Process Blacklist entry
                if(kController == nullkey) kController = llGetOwnerKey(sPendingId);
                if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];
 
                // clear pending request
                sPendingName=nullstr;
                sPendingId=nullkey;
                sPendingMessage=nullstr;
            }
        }
    }
    
    changed(integer change)
    {
        if (change & CHANGED_OWNER) 
        {
             llResetScript();
        }
    }
}