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
Line 38: Line 38:
//== Based on Reference Implementation by Marine Kelley
//== Based on Reference Implementation by Marine Kelley


integer ALLOW_REMOVE_DETACH = TRUE;
integer DEBUG = FALSE;
integer DEBUG = FALSE;


Line 57: Line 56:


integer lockstatus; //== Has the collar been locked by the RLV plugin?
integer lockstatus; //== Has the collar been locked by the RLV plugin?
string ownerexcept = "@sendim @tplure"; //== List of restrictions owner (not wearer) will always be exempt from


// ---------------------------------------------------
// ---------------------------------------------------
Line 62: Line 63:
// ---------------------------------------------------
// ---------------------------------------------------
   
   
integer RLVRS_PROTOCOL_VERSION = 1020; // version of the protocol, stated on the specification page
integer RLVRS_PROTOCOL_VERSION = 1040; // version of the protocol, stated on the specification page
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";
string RLVRS_IMPL_VERSION = "Felis Darwin's implementation, Amethyst Plugin version";
   
   
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 = 100;    // 100m is llShout distance
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
   
   
Line 77: Line 71:
   
   
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;
integer LOGIN_DELAY_WAIT_FOR_PONG = 20;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;


integer PING_INTERVAL = 60; //== Time between pings
integer PING_INTERVAL = 60; //== Time between pings, and time waiting for force-sit
integer MODE_OFF = 0;
integer MODE_ASK = 1;
integer MODE_AUTO = 2;
   
   
// ---------------------------------------------------
// ---------------------------------------------------
Line 92: Line 81:
   
   
list lRestrictions; // restrictions currently applied (without the "=n" part)
list lRestrictions; // restrictions currently applied (without the "=n" part)
list eRestrictions; //== Really damn long restrictions (e.g. exceptions, etc.)
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 NULL_KEY 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)
Line 99: Line 90:
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)
integer loginWaitingForPong;
integer loginWaitingForPong;
integer loginPendingForceSit;
integer loginPendingForceSit;
integer noping = 0;
   
   
key    lastForceSitDestination;
key    lastForceSitDestination;
integer lastForceSitTime;
integer lastForceSitTime;
integer stop = 0; //== Allows the relay to stop mid-command execution if directed to by another command


// ---------------------------------------------------
// ---------------------------------------------------
Line 124: Line 119:
ack(string cmd_id, key id, string cmd, string ack)
ack(string cmd_id, key id, string cmd, string ack)
{
{
     llShout(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
     if(id != NULL_KEY)
}
        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
string getModeDescription()
string getModeDescription()
{
{
     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)";
     return "RLV Relay is ON (auto-accept)";  
}
// check that this command is for us and not someone else
integer verifyWeAreTarget(string message)
{
    list tokens = llParseString2List(message, [","], []);
    if (llGetListLength(tokens) == 3) // this is a normal command
    {
      if (llList2String(tokens, 1) == llGetOwner()) // talking to me ?
      {
        return TRUE;
      }
    }
    return FALSE;
}
}
   
   
Line 162: Line 139:
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 != NULL_KEY && (kSource == id || ((kSource == NULL_KEY) && (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;
        }
     }
     }
   
   
Line 197: Line 157:
     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;
}
}
   
   
Line 215: Line 176:
     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 != NULL_KEY) //== Is this my owner's stuff?
      || ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?
      || (object_group==parcel_group && object_group != NULL_KEY)      // OR its group is the same as the parcel I'm on
     )
     )
     {
     {
Line 254: Line 217:
     // 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)?
        string param = llGetSubString (cmd, index + 1, -1);
        if (((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) // is it an integer (channel number)?
        {
            return TRUE;
        }
        // removing restriction
        if ((param == "y") || (param == "rem"))
        {
            return TRUE;
        }
    }
   
    // check for a leading ! (meta command)
    if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
    {
        return TRUE;
    }
    // check for @clear
    // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
    // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around
    // a bug in the first relay implementation. You should refuse to use relay versions < 1013
    // instead.)
    if (cmd == "@clear")
     {
     {
         return TRUE;
         return TRUE;
Line 313: Line 251:
     debug("Verifying permission for command "+ message);
     debug("Verifying permission for command "+ message);
      
      
    // is it switched off?
    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]))
     {
     {
        kController = NULL_KEY;
         return FALSE;
         return FALSE;
     }
     }
Line 329: Line 262:
   
   
     // 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, performing");
         debug("simple command or Owner in Whitelist, executing.");
         return TRUE;
         return TRUE;
     }
     }
Line 352: Line 285:
   
   
     // 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 358: Line 291:
         sPendingMessage=message;
         sPendingMessage=message;
         sPendingTime = llGetUnixTime();
         sPendingTime = llGetUnixTime();
       
        list opts = ["Yes", "No"];
          
          
         llSetTimerEvent(2.0);
         llSetTimerEvent(2.0);
       
               
         if(llKey2Name(llGetOwnerKey(id)) != "")
         if(llKey2Name(llGetOwnerKey(id)) != "")
        {
             name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
             name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
            opts += ["Never", "Always"];
        }
           
        if(llKey2Name(kController) != "")
        {
            name = llKey2Name(kController) +", using "+ name +",";
            opts += ["Never"];
        }
          
          
         llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
         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 382: Line 326:
{
{
     integer sentRLV=0;
     integer sentRLV=0;
    stop = 0;
      
      
     list tokens=llParseString2List (message, [","], []);
     list tokens=llParseString2List (message, [","], []);
Line 397: Line 342:
             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);
                     sentRLV=1;
                     sentRLV=1;
                 }
                 }
                 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 424: Line 376:
     string param=llList2String (tokens_command, 1); // 2222
     string param=llList2String (tokens_command, 1); // 2222
     integer ind=llListFindList (lRestrictions, [behav]);
     integer ind=llListFindList (lRestrictions, [behav]);
    if(ind == -1)
        ind = llListFindList(eRestrictions, [behav]);
    debug("param=" + param);
   
   
    debug("behav = "+ behav +"; param=" + param);
   
    //== Stop the public chat exploits.
    if((~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)
         if (ind<0)
         {
         {
             if(llSubStringIndex(behav, ":") > -1)
             if(~llSubStringIndex(behav, ":") && llGetFreeMemory() <= 1024)
             {
             {
                 if(llGetFreeMemory() > 500)
                 llOwnerSay("Relay is running dangerously low on memory; some restrictions will not be processed.");
                    eRestrictions += [behav];
             }
             }
             else
             else
                 lRestrictions += [behav];
                 lRestrictions = (lRestrictions=[]) + lRestrictions + [behav];
               
            if(~llSubStringIndex(ownerexcept,behav)) //== Handle owner exceptions
            {
                if(ownerkey != NULL_KEY)
                    llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");
                if(secaccess || ownerkey == NULL_KEY)
                {
                    integer i;
                    for(i = 0; i < llGetListLength(secowners); i++)
                        llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");
                }
            }
         }
         }


         if(kSource == NULL_KEY && !lockstatus)
         if(kSource == NULL_KEY)
            llOwnerSay("@detach=n");
        {
            llSetTimerEvent(2.0);
            if(!lockstatus)
                llOwnerSay("@detach=n");
        }
                  
                  
         kSource=id; // we know that kSource is either NULL_KEY or id already
         kSource=id; // we know that kSource is either NULL_KEY or id already
Line 450: Line 421:
     {
     {
         if (ind>-1)
         if (ind>-1)
                lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);
               
        //== Unlisted Owner Exceptions are NEVER removed, for safety
        //==  Nor is the public chat exploit fixer
        else if(~llSubStringIndex(behav, ownerkey) || behav == "@a-relay")
         {
         {
            if(llSubStringIndex(behav, ":") > -1)
                eRestrictions=llDeleteSubList ((eRestrictions=[]) + eRestrictions, ind, ind);
            else
                lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind); 
        }
        if (llGetListLength (lRestrictions) == 0 && llGetListLength(eRestrictions) == 0)
        {
            kSource=NULL_KEY;
            if(!lockstatus)
                llOwnerSay("@detach=y");
        }
       
    }
    else if (param == "force" && !ALLOW_REMOVE_DETACH)
    {
        debug("force: " + behav);
        list temp = llParseString2List(behav, [":"], []);
        string commandName = llList2String (temp, 0); // @sit
        if (commandName == "@detach" || commandName == "@remoutfit")
        {
            debug("rejecting remove/detach");
            llWhisper(0, "Not stripping");
             ack(cmd_id, id, command, "ko");
             ack(cmd_id, id, command, "ko");
             return;
             return;
         }
         }
       
        if (llGetListLength (lRestrictions) == 0 && !lockstatus)
            llOwnerSay("@detach=y");
       
     }
     }
   
   
    workaroundForAtClear(command);
     rememberForceSit(command);
     rememberForceSit(command);
     if(llGetListLength(lRestrictions) + llGetListLength(eRestrictions) == 1)
     if(llGetListLength(lRestrictions) == 1)
         sendRLCmd("@this-text-is-automated-and-here-to-stop-relay-exploits=n");
         llOwnerSay("@a-relay=n");
     sendRLCmd(command); // execute command
     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
rememberForceSit(string command)
rememberForceSit(string command)
{
{
     list tokens_command=llParseString2List (command, ["="], []);
      
    string behav=llList2String (tokens_command, 0); // @sit:<uuid>
//    list tokens_command=llParseString2List (command, ["="], []);
    string param=llList2String (tokens_command, 1); // force
//    string behav=llList2String (tokens_command, 0); // @sit:<uuid>
     if (param != "force")
//    string param=llList2String (tokens_command, 1); // force
     {
 
    command = llStringTrim(command, STRING_TRIM);
     string param = llGetSubString(command, -6, -1);
 
//    if (param != "force")
     if (param != "=force")
         return;
         return;
    }
   
   
    tokens_command=llParseString2List(behav, [":"], []);
//    tokens_command=llParseString2List(behav, [":"], []);
    behav=llList2String (tokens_command, 0); // @sit
//    behav=llList2String (tokens_command, 0); // @sit
    param=llList2String (tokens_command, 1); // <uuid>
//    param=llList2String (tokens_command, 1); // <uuid>
 
    string behav = llGetSubString(command, 0, 4);
    param = llGetSubString(command, 5, 40);
 
     debug("'force'-command:" + behav + "/" + param);
     debug("'force'-command:" + behav + "/" + param);
     if (behav != "@sit")
      
     {
//    if (behav != "@sit")
     if(behav != "@sit:")
         return;
         return;
     }
      
   
     lastForceSitDestination = (key) param;
     lastForceSitDestination = (key) param;
     lastForceSitTime = llGetUnixTime();
     lastForceSitTime = llGetUnixTime();
Line 526: Line 481:
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 + "implversion") // checking relay version
     else if (command == "!implversion") // checking relay version
     {
     {
         ack(cmd_id, id, command, RLVRS_IMPL_VERSION);
         ack(cmd_id, id, command, RLVRS_IMPL_VERSION);
     }
     }
     else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
     else if (command=="!release") // release all the restrictions (end session)
     {
     {
        ack(cmd_id, id, command, "ok");
         kSource = NULL_KEY; //== So only one release message is sent
         kSource = NULL_KEY; //== So only one release message is sent
         releaseRestrictions();
         releaseRestrictions();
        ack(cmd_id, id, command, "ok");
     }
     }
     else if (command == PREFIX_METACOMMAND + "pong")
 
     //== We don't need to do this because any sent restriction automatically does the same thing   
//    else if (command == "!pong")
//        loginWaitingForPong = FALSE;
   
    if (llGetSubString(command,0,4) == "!who/")
     {
     {
         loginWaitingForPong = FALSE;
         kController = (key)llGetSubString(command, 5, -1);
     }
     }
     else if (command==PREFIX_METACOMMAND+"mode") //== Tell the querying object what mode the relay is in
     if (llGetSubString(command,0,9) == "!handover/")
     {
     {
         integer tmode = nMode;
         list tokens = llParseString2List(command, ["/"], []);
         if(tmode == 2 && !isObjectIdentityTrustworthy(id)) // Report "ask" mode if the object is untrustworthy
         if(!llList2Integer(tokens, 2))
             tmode = 1;
             releaseRestrictions();
 
        kSource = llList2Key(tokens,1);
         ack(cmd_id, id, command, (string)tmode);
         pingWorldObjectIfUnderRestrictions();
     }
     }
}
}
Line 557: Line 517:
releaseRestrictions ()
releaseRestrictions ()
{
{
     if(kSource != NULL_KEY)
     ack("Relay Release Notification", kSource, "!release", "ok");
        ack("Relay Release Notification", kSource, "!release", "ok");


     kSource=NULL_KEY;
     kSource=NULL_KEY;
Line 567: Line 526:
     for (i=0; i<len; ++i)
     for (i=0; i<len; ++i)
     {
     {
         sendRLCmd(llList2String (lRestrictions, i)+"=y");
         llOwnerSay(llList2String (lRestrictions, i)+"=y");
       
        if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))
            llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));
     }
     }
     lRestrictions = [];
     lRestrictions = [];
    len = llGetListLength (eRestrictions);
 
    for (i=0; i<len; ++i)
    {
        sendRLCmd(llList2String (eRestrictions, i)+"=y");
    }
    eRestrictions = [];
     loginPendingForceSit = FALSE;
     loginPendingForceSit = FALSE;
     if(sPendingId != NULL_KEY)
     loginWaitingForPong = FALSE;
        ack("Relay Release Notification", sPendingId, "!release", "ok");
    llSetTimerEvent(0.0);
          
    ack("Relay Release Notification", sPendingId, "!release", "ok");
 
    llMessageLinked(LINK_SET, 356, nullstr, NULL_KEY);
 
    if(kController != NULL_KEY && sPendingId != NULL_KEY)
         llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);
 
     sPendingId = NULL_KEY;
     sPendingId = NULL_KEY;
     sPendingName = "";
     sPendingName = "";
     sPendingMessage = "";
     sPendingMessage = "";
     llMessageLinked(LINK_SET, 356, nullstr, NULL_KEY);
     kController = NULL_KEY;
}
}
   
   
Line 592: Line 555:
   
   
init() {
init() {
    debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());
     nMode=1;
     nMode=1;
     kSource=NULL_KEY;
     kSource=NULL_KEY;
     lRestrictions=[];
     lRestrictions=[];
    eRestrictions=[];
     sPendingId=NULL_KEY;
     sPendingId=NULL_KEY;
     sPendingName="";
     sPendingName="";
     sPendingMessage="";
     sPendingMessage="";
     llListen (RLVRS_CHANNEL, "", "", "");
     llListen (-1812221819, "", "", "");
     llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
     llListen (-1812220409, "", llGetOwner(), "");
     llOwnerSay (getModeDescription());
     llOwnerSay (getModeDescription());
}
}
Line 611: Line 574:
     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 616: Line 586:
         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")
         {
         {
Line 622: Line 592:
         }
         }
     }
     }
   
    integer len2=llGetListLength(eRestrictions);
    for (i=0; i<len2; ++i)
    {
        restr=llList2String(eRestrictions, i);
        debug("restr=" + restr);
        sendRLCmd(restr+"=n");
    }
   
    if(len + len2 > 0)
        llOwnerSay("@detach=n");
}
}
   
   
Line 654: Line 613:
     string cmd = llList2String(templist, 0);
     string cmd = llList2String(templist, 0);


     if(cmd == "relay")
     if(cmd == "relay" && (id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))
     {
     {
         if(id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1))))
         integer change = 0;
       
        string second = llList2String(templist, 1);
        string third = llList2String(templist, 2);
       
        if(kSource != NULL_KEY && id == llGetOwner())
         {
         {
             integer change = 0;
             llOwnerSay("You cannot change relay modes while the relay is locked.");
              
             return; 
            string second = llList2String(templist, 1);
        }
           
       
             if(kSource != NULL_KEY && id == llGetOwner())
        if(id == ownerkey && (second == "secondaries" || second == "sec"))
        {
             if(third == "on" || third == "auto" || (third == "" && !secaccess))
             {
             {
                 llOwnerSay("You cannot change relay modes while the relay is locked.");
                 secaccess = 1;
                return; 
                llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");
             }
             }
              
             else
            if(id == ownerkey && (second == "secondaries" || second == "sec"))
             {
             {
                 string third = llList2String(templist, 2);
                 secaccess = 0;
 
                 llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");
                if(third == "on" || third == "auto" || (third == "" && !secaccess))
                {
                    secaccess = 1;
                    llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");
                 }
                else
                {
                    secaccess = 0;
                    llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");
                }
             }
             }
           
        }
            else if(second == "on" || second == "auto")
       
        else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == NULL_KEY)) && second == "ping")
        {
            if(third == "off" || (third == "" && !noping))
             {
             {
                 nMode = MODE_AUTO;
                 noping = 1;
                 change = 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 that instance will continue to enforce the last known restrictions until the wearer logs off.");
             }
             }
             else if(second == "off")
             else
             {
             {
                 nMode = MODE_OFF;
                 noping = 0;
                change = 1;   
                llWhisper(0,"Restrained Life Relay now requires regular object communication.");
             }
            } 
             else if(second == "ask")
        }
       
        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 == "" || second == "mode")
        {
            nMode++;
            if(nMode > 2) nMode = 0;
             change = 1; 
        }
       
        if(second == "wbclear")
        {
            WhiteBlack = [];
            llWhisper(0,"Relay Whitelist and Blacklist cleared.");
        }
       
        if(change)
        {
            setby = NULL_KEY;
            if (nMode == 0)
             {
             {
                 nMode = MODE_ASK;
                 llSetTimerEvent(0.0);
                 change = 1;  
                 releaseRestrictions();
             }
             }
           
             else
             else if(second == "" || second == "mode")
             {
             {
                 nMode++;
                 llSetTimerEvent((float)PING_INTERVAL);
                if(nMode > 2) nMode = 0;
                if(nMode == 2) setby = id;
                change = 1; 
            }
           
            if(change)
            {
                setby = NULL_KEY;
                if (nMode == MODE_OFF)
                {
                    llSetTimerEvent(0.0);
                    releaseRestrictions();
                }
                else
                {
                    llSetTimerEvent((float)PING_INTERVAL);
                    if(nMode == MODE_AUTO) setby = id;
                }
                if(id == llGetOwner())
                    llOwnerSay(getModeDescription()); 
                else
                    llSay(0, getModeDescription());
                   
                llMessageLinked(LINK_THIS, 63, nullstr, nullstr);
             }
             }
            if(id == llGetOwner())
                llOwnerSay(getModeDescription()); 
            else
                llSay(0, getModeDescription());
               
            llMessageLinked(LINK_THIS, 63, nullstr, nullstr);
         }
         }
        else if(id == llGetOwner())
    }
        {
    else if(cmd == "relay" && id == llGetOwner())
            llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it.");   
    {
        }
        llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it.");   
     }
     }
}
}
Line 807: Line 782:
         else if(num == 65)
         else if(num == 65)
         {
         {
             if((integer)str == TRUE)
             lockstatus = (integer)str;
                lockstatus = 1;
            else
                lockstatus = 0;  
         }
         }
         else if(num == 66) //== Safeword, unlock
         else if(num == 66) //== Safeword, unlock
         {
         {
            nMode = MODE_OFF;
             releaseRestrictions();
             releaseRestrictions();
            nMode = 0;
             llOwnerSay(getModeDescription());
             llOwnerSay(getModeDescription());
         }
         }
Line 844: Line 816:
     timer()
     timer()
     {
     {
         timerTickCounter++;
         timerTickCounter++;  
        if(timerTickCounter == 0)
            pingWorldObjectIfUnderRestrictions();       
          
          
         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;
Line 865: Line 835:
                 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 873: Line 843:
             else if(!loginWaitingForPong)
             else if(!loginWaitingForPong)
             {
             {
                 sendRLCmd ("@sit:"+(string)lastForceSitDestination+"=force");
                 llOwnerSay ("@sittp=y,sit:"+(string)lastForceSitDestination+"=force");
             }
             }
         }
         }
          
          
         if(sPendingId != NULL_KEY)
         if(sPendingId != NULL_KEY && 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 = NULL_KEY;
                llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], DIALOG_CHANNEL);
            sPendingName = "";
                sPendingId = NULL_KEY;
            sPendingMessage = "";
                sPendingName = "";
        }
                sPendingMessage = "";  
   
            }   
         if(timerTickCounter == 0 && !noping)
         }
            pingWorldObjectIfUnderRestrictions();
   
   
         if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
         if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
         {
         {
             timerTickCounter = -1;
             timerTickCounter = -1;
             llSetTimerEvent((float)PING_INTERVAL);
             if(!noping)
            {
                llSetTimerEvent((float)PING_INTERVAL);
                return;
            }
            llSetTimerEvent(0.0);
         }
         }
     }
     }
Line 897: Line 872:
     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)
         {
         {
             debug("LISTEN: " + message);
             debug("LISTEN: " + message);
             if (!verifyWeAreTarget(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
             }
             }
            //=== ALWAYS accept a lone "!release" command, no matter the distance
            if (!isObjectNear(id) && llGetSubString(message, -9, -1) != ",!release") return;
   
   
             debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);
             debug("Got message (active world object " + (string) kSource + "): name=" + name+ "; id=" + (string) id + "; message=" + message);
   
   
             if (kSource != NULL_KEY && kSource != id)
             if (kSource != NULL_KEY && kSource != id)
Line 920: Line 896:
                 debug("already used by another object => reject");
                 debug("already used by another object => reject");
                 return;
                 return;
            }
           
            if(!loginPendingForceSit && sPendingId == NULL_KEY)
            {
                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
            llSetTimerEvent(0.0);
//            timerTickCounter = -1;
            llSetTimerEvent((float)PING_INTERVAL);
            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())
            {
                return; // only accept dialog responses from the owner
            }
             if (sPendingId!=NULL_KEY)
             if (sPendingId!=NULL_KEY)
             {
             {              
                 if (message=="Yes") // pending request authorized => process it
                 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);
                     execute(sPendingName, sPendingId, sPendingMessage);
                 }
                 }
                 else if(kSource == sPendingId)
                 else if(kSource == sPendingId)
                     releaseRestrictions();
                     releaseRestrictions();
               
                //== Process Blacklist entry
                if(kController == NULL_KEY) kController = llGetOwnerKey(sPendingId);
                if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];
   
   
                 // clear pending request
                 // clear pending request
Line 970: Line 948:
     }
     }
}
}


</lsl>
</lsl>

Revision as of 23:49, 28 April 2009

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 Restrained Life 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 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 most of the features suggested by Marine in the original Reference Implementation document.

Current Changelog

   v0.2:
   
    - Added support for new Amethyst v7 menu system. (Plugin Menu messages are handled on link-828 now)
    
    - Delayed force-sit on relog until the sit target responds to a ping.
    
    - "!release" now clears any pending restrictions. (e.g. from an unapproved control request in ASK mode)
    
    - Used an LSL workaround to expand the maximum number of pending commands the relay can store when in ASK mode.
    
    - Added crash-prevention code to stop adding pending commands if the script is running low on memory.
    
    - Implemented a version of Maike's "@getstatus" exploit fix.  Read the Wiki if you want to know what that is.
    
    - Added support for a new "!mode" metacommand, which makes the Relay reply with its current mode. (a number)
    
    - The Relay now issues a link message on release which tells the Amethyst RLV plugin to re-issue its restrictions.
    
    - Whenever the relay releases restrictions it will issue the "!release,ok" reply to the current or pending control object.
    
    - Fixed a logic error which could cause the restriction list to grow infinitely, eventually causing a script crash.
    
    - When restrictions are issued or re-issued the relay counts it as a ping and resets the ping check countdown.

Current Source Code

<lsl>

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

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

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

// --------------------------------------------------- // Constants // ---------------------------------------------------

integer RLVRS_PROTOCOL_VERSION = 1040; // 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 NULL_KEY 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; 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 != NULL_KEY)
       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)"; 
   return "RLV Relay is ON (auto-accept)"; 

}

// --------------------------------------------------- // 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 != NULL_KEY && (kSource == id || ((kSource == NULL_KEY) && (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 != NULL_KEY) //== Is this my owner's stuff?
     || ~llListFindList(secowners, [object_owner]) //== ...or my owners' stuff?
     || (object_group==parcel_group && object_group != NULL_KEY)       // 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)?
   {
       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 == NULL_KEY && (sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()) && llGetFreeMemory() > 500)
   {
       debug("Gluing " + sPendingMessage + " with " + commands);
       sPendingMessage = (sPendingMessage="") + sPendingMessage + "|" + commands;
       return TRUE;
   }
   return FALSE;

}

// verifies the permission. This includes mode // (off, permission, auto) of the relay and the // identity of the object (owned by parcel people). integer verifyPermission(key id, string name, string message) {

   debug("Verifying permission for command "+ message);
   
   // extract the commands-part
   list tokens = llParseString2List (message, [","], []);
   if (llGetListLength (tokens) < 3 || nMode == 0 || ~llListFindList(WhiteBlack, ["-"+(string)llGetOwnerKey(id)]) || ~llListFindList(WhiteBlack, ["-"+(string)kController]))
   {
       kController = NULL_KEY;
       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 = "";
   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)) != "")
       {
           name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
           opts += ["Never", "Always"];
       }
           
       if(llKey2Name(kController) != "")
       {
           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) {

   integer sentRLV=0;
   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);
                   sentRLV=1;
               }
               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.
   if((~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
           {
               if(ownerkey != NULL_KEY)
                   llOwnerSay("@"+behav+":"+(string)ownerkey+"=add");
               if(secaccess || ownerkey == NULL_KEY)
               {
                   integer i;
                   for(i = 0; i < llGetListLength(secowners); i++)
                       llOwnerSay("@"+behav+":"+llList2String(secowners,i)+"=add");
               }
           }
       }
       if(kSource == NULL_KEY)
       {
           llSetTimerEvent(2.0);
           if(!lockstatus)
               llOwnerSay("@detach=n");
       }
               
       kSource=id; // we know that kSource is either NULL_KEY or id already
   }
   else if (param=="y" || param=="rem") // remove from lRestrictions
   {
       if (ind>-1)
               lRestrictions=llDeleteSubList ((lRestrictions=[]) + lRestrictions, ind, ind);
               
       //== Unlisted Owner Exceptions are NEVER removed, for safety
       //==  Nor is the public chat exploit fixer
       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 rememberForceSit(string command) {

// list tokens_command=llParseString2List (command, ["="], []); // string behav=llList2String (tokens_command, 0); // @sit:<uuid> // string param=llList2String (tokens_command, 1); // force

   command = llStringTrim(command, STRING_TRIM);
   string param = llGetSubString(command, -6, -1);

// if (param != "force")

   if (param != "=force")
       return;

// tokens_command=llParseString2List(behav, [":"], []); // behav=llList2String (tokens_command, 0); // @sit // param=llList2String (tokens_command, 1); // <uuid>

   string behav = llGetSubString(command, 0, 4);
   param = llGetSubString(command, 5, 40);
   debug("'force'-command:" + behav + "/" + param);
   

// if (behav != "@sit")

   if(behav != "@sit:")
       return;
   
   
   lastForceSitDestination = (key) param;
   lastForceSitTime = llGetUnixTime();
   debug("remembered force sit");

}

// executes a meta command which is handled by the relay itself executeMetaCommand(string cmd_id, string id, string command) {

   if (command=="!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 = NULL_KEY; //== So only one release message is sent
       releaseRestrictions();
   }
   //== We don't need to do this because any sent restriction automatically does the same thing    

// else if (command == "!pong") // loginWaitingForPong = FALSE;

   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=NULL_KEY;
   if(!lockstatus)
       llOwnerSay("@detach=y");
   integer i;
   integer len=llGetListLength (lRestrictions);
   for (i=0; i<len; ++i)
   {
       llOwnerSay(llList2String (lRestrictions, i)+"=y");
       
       if(~llSubStringIndex(ownerexcept,llList2String(lRestrictions,i)))
           llOwnerSay("@clear="+llGetSubString(llList2String(lRestrictions, i),1,-1));
   }
   lRestrictions = [];
   loginPendingForceSit = FALSE;
   loginWaitingForPong = FALSE;
   llSetTimerEvent(0.0);
   ack("Relay Release Notification", sPendingId, "!release", "ok");
   llMessageLinked(LINK_SET, 356, nullstr, NULL_KEY);
   if(kController != NULL_KEY && sPendingId != NULL_KEY)
       llDialog(kController, llKey2Name(llGetOwner()) +" has not accepted your attempt to control their viewer via Restrained Life.", [], 99);
   sPendingId = NULL_KEY;
   sPendingName = "";
   sPendingMessage = "";
   kController = NULL_KEY;

}


// --------------------------------------------------- // initialisation and login handling // ---------------------------------------------------

init() {

   debug("RLV Plugin Free Memory at "+ (string)llGetFreeMemory());
   nMode=1;
   kSource=NULL_KEY;
   lRestrictions=[];
   sPendingId=NULL_KEY;
   sPendingName="";
   sPendingMessage="";
   llListen (-1812221819, "", "", "");
   llListen (-1812220409, "", llGetOwner(), "");
   llOwnerSay (getModeDescription());

}

// sends the known restrictions (again) to the RL-viewer // (call this functions on login) reinforceKnownRestrictions() {

   integer i;
   integer len=llGetListLength(lRestrictions);
   string restr;
   
   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")
       {
           loginPendingForceSit = TRUE;
       }
   }

}

// send a ping request and start a timer pingWorldObjectIfUnderRestrictions() {

   loginWaitingForPong = FALSE;
   if (kSource != NULL_KEY)
   {
       ack("ping", kSource, "ping", "ping");
       timerTickCounter = 0;
       llSetTimerEvent(1.0);
       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 == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1)))))
   {
       integer change = 0;
       
       string second = llList2String(templist, 1);
       string third = llList2String(templist, 2);
       
       if(kSource != NULL_KEY && id == llGetOwner())
       {
           llOwnerSay("You cannot change relay modes while the relay is locked.");
           return;   
       }
       
       if(id == ownerkey && (second == "secondaries" || second == "sec"))
       {
           if(third == "on" || third == "auto" || (third == "" && !secaccess))
           {
               secaccess = 1;
               llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");
           }
           else
           {
               secaccess = 0;
               llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");
           }
       }
       
       else if((secaccess || id == ownerkey || (id == llGetOwner() && kSource == NULL_KEY)) && second == "ping")
       {
           if(third == "off" || (third == "" && !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 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 == "" || second == "mode")
       {
           nMode++;
           if(nMode > 2) nMode = 0;
           change = 1;   
       }
       
       if(second == "wbclear")
       {
           WhiteBlack = [];
           llWhisper(0,"Relay Whitelist and Blacklist cleared.");
       }
       
       if(change)
       {
           setby = NULL_KEY;
           if (nMode == 0)
           {
               llSetTimerEvent(0.0);
               releaseRestrictions();
           }
           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();
           nMode = 0;
           llOwnerSay(getModeDescription());
       }
       else if(num == 355)
           reinforceKnownRestrictions();
   }
   
   attach(key id)
   {
       if(id == NULL_KEY)
           llOwnerSay("@clear");   
   }
   
   on_rez(integer start_param)
   {
       // relogging, we must refresh the viewer and ping the object if any
       // if mode is not OFF, fire all the stored restrictions
       if (nMode)
       {
           reinforceKnownRestrictions();
           pingWorldObjectIfUnderRestrictions();
       }
       // 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 != NULL_KEY && sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
       {
           llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], -1812220409);
           sPendingId = NULL_KEY;
           sPendingName = "";
           sPendingMessage = "";
       }  

       if(timerTickCounter == 0 && !noping)
           pingWorldObjectIfUnderRestrictions(); 

       if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
       {
           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 != NULL_KEY && kSource != id)
           {
               debug("already used by another object => reject");
               return;
           }
           
           if(!loginPendingForceSit && sPendingId == NULL_KEY)
           {
               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!=NULL_KEY)
           {                
               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 == NULL_KEY) kController = llGetOwnerKey(sPendingId);
               if(message == "Never") WhiteBlack += ["-"+(string)llGetOwnerKey(kController)];

               // clear pending request
               sPendingName="";
               sPendingId=NULL_KEY;
               sPendingMessage="";
           }
       }
   }
   
   changed(integer change)
   {
       if (change & CHANGED_OWNER) 
       {
            llResetScript();
       }
   }

}

</lsl>