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 6: Line 6:
//==  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 ALLOW_REMOVE_DETACH = TRUE;
integer DEBUG = FALSE;


//~ RestrainedLife Viewer Relay Script example code
// ---------------------------------------------------
//~ By Marine Kelley
//             Amethyst Plugin Variables
//~ 2008-02-03
// ---------------------------------------------------
//~ v1.1
 
//~ 2008-02-16 with fixes by Maike Short
key nullkey = NULL_KEY;
//~ 2008-02-24 more fixes by Maike Short
string nullstr = "";
//~ 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 secaccess=0; //== Do secondary owners have access to the RL functions?
//~ completeness or performance. It may only be distributed in its full source code,
 
//~ this header and disclaimer and is not to be sold.
// Internal variables
key ownerkey = nullkey;
//~ * Possible improvements
list secowners = [];
//~ Do some error checking
 
//~ Handle more than one object
key setby = NULL_KEY; //== Who set the RLV Relay status?
//~ 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...


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


// ---------------------------------------------------
// ---------------------------------------------------
Line 39: Line 35:
string PREFIX_RL_COMMAND = "@";
string PREFIX_RL_COMMAND = "@";
string PREFIX_METACOMMAND = "!";
string PREFIX_METACOMMAND = "!";
 
integer RLVRS_CHANNEL = -1812221819;  // RLVRS in numbers
integer RLVRS_CHANNEL = -1812221819;  // RLVRS in numbers
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
 
integer MAX_OBJECT_DISTANCE = 20;    // 20m is llSay distance
integer MAX_OBJECT_DISTANCE = 20;    // 20m is llSay distance
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 300; // 300 is 5 minutes
integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds
 
integer PERMISSION_DIALOG_TIMEOUT = 30;
integer PERMISSION_DIALOG_TIMEOUT = 30;
 
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;
integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;


integer PING_INTERVAL = 60; //== Time between pings
integer MODE_OFF = 0;
integer MODE_OFF = 0;
integer MODE_ASK = 1;
integer MODE_ASK = 1;
integer MODE_AUTO = 2;
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 NULL_KEY if lRestrictions is empty, always set if not
 
string sPendingName; // name of initiator of pending request (first request of a session in mode 1)
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)
integer loginWaitingForPong;
integer loginWaitingForPong;
integer loginPendingForceSit;
integer loginPendingForceSit;
 
key    lastForceSitDestination;
key    lastForceSitDestination;
integer lastForceSitTime;
integer lastForceSitTime;
integer lastPingTime;


// ---------------------------------------------------
// ---------------------------------------------------
Line 82: Line 81:
// ---------------------------------------------------
// ---------------------------------------------------
   
   
 
debug(string x)
debug(string x)
{
{
//    llOwnerSay("DEBUG: " + x);
    if (DEBUG)
    {
        llOwnerSay("DEBUG: " + x);
    }
}
}
   
   
Line 93: Line 95:
     llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
     llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
}
}
 
// cmd begins with a '@'  
// cmd begins with a '@'  
sendRLCmd(string cmd)
sendRLCmd(string cmd)
Line 99: Line 101:
     llOwnerSay(cmd);
     llOwnerSay(cmd);
}
}
 
// get current mode as string
// get current mode as string
string getModeDescription()
string getModeDescription()
Line 107: Line 109:
     else return "RLV Relay is ON (auto-accept)";  
     else return "RLV Relay is ON (auto-accept)";  
}
}
 
// check that this command is for us and not someone else
// check that this command is for us and not someone else
integer verifyWeAreTarget(string message)
integer verifyWeAreTarget(string message)
Line 121: Line 123:
     return FALSE;
     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)
Line 134: Line 136:
         return FALSE;
         return FALSE;
     }
     }
 
     // are we already under command by this object?
     // are we already under command by this object?
     if (kSource == id)
     if (kSource == id)
Line 140: Line 142:
         return TRUE;
         return TRUE;
     }
     }
 
     // are we not under command by any object but were we forced to sit on this object recently?
     // are we not under command by any object but were we forced to sit on this object recently?
     if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
     if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
Line 151: Line 153:
         }
         }
     }
     }
 
     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 168: Line 170:
     return distance <= MAX_OBJECT_DISTANCE;
     return distance <= MAX_OBJECT_DISTANCE;
}
}
 
// 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 178:
     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
Line 189: Line 191:
     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 198:
     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 207:
         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, "=");
Line 222: Line 227:
         // check for a number after the "="
         // check for a number after the "="
         string param = llGetSubString (cmd, index + 1, -1);
         string param = llGetSubString (cmd, index + 1, -1);
         if ((integer)param!=0 || param=="0") // is it an integer (channel number)?
         if (((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) // is it an integer (channel number)?
         {
         {
             return TRUE;
             return TRUE;
         }
         }
 
         // removing restriction
         // removing restriction
         if ((param == "y") || (param == "rem"))
         if ((param == "y") || (param == "rem"))
Line 233: Line 238:
         }
         }
     }
     }
 
     // check for a leading ! (meta command)
     // check for a leading ! (meta command)
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
Line 239: Line 244:
         return TRUE;
         return TRUE;
     }
     }
 
     // check for @clear
     // check for @clear
     // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
     // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
Line 249: Line 254:
         return TRUE;
         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 269: Line 274:
     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 280:
integer verifyPermission(key id, string name, string message)
integer verifyPermission(key id, string name, string message)
{
{
    debug("Verifying permission for command "+ message);
   
     // is it switched off?
     // is it switched off?
     if (nMode == MODE_OFF)
     if (nMode == MODE_OFF)
Line 280: Line 287:
         return FALSE;
         return FALSE;
     }
     }
 
     // extract the commands-part
     // extract the commands-part
     list tokens = llParseString2List (message, [","], []);
     list tokens = llParseString2List (message, [","], []);
Line 289: Line 296:
     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))
     {
     {
        debug("Simple command, performing");
         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);
Line 310: Line 319:
         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 == MODE_ASK || !trustworthy)
Line 318: Line 327:
         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
        llSetTimerEvent(2.0);
             name += " (owned by "+ llKey2Name(llGetOwnerKey(id)) +")";
       
 
         if(llKey2Name(llGetOwnerKey(id)) != "")
             name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
       
         llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
         llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
         debug("Asking for permission");
         debug("Asking for permission");
Line 328: Line 339:
     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 369: Line 380:
     }
     }
}
}
 
// 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 390:
     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("param=" + param);
     if (param=="n" || param=="add") // add to lRestrictions
     if (param=="n" || param=="add") // add to lRestrictions
     {
     {
         if (ind<0) lRestrictions+=[behav];
         if (ind<0) lRestrictions+=[behav];


         if(kSource == NULL_KEY) //== If this is the first time we're processing commands from this, lock the relay
         if(kSource == NULL_KEY && !lockstatus)
             llOwnerSay("@detach=n");
             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 395: Line 408:
         {
         {
             kSource=NULL_KEY;
             kSource=NULL_KEY;
             llOwnerSay("@detach=y"); //== Unlock the relay if we are no longer under the control of this device
             if(!lockstatus)
                llOwnerSay("@detach=y");
         }
         }
       
     }
     }
 
    else if (param == "force" && !ALLOW_REMOVE_DETACH)
    {
        debug("force: " + behav);
        list temp = llParseString2List(behav, [":"], []);
        string commandName = llList2String (temp, 0); // @sit
        if (commandName == "@detach" || commandName == "@remoutfit")
        {
            debug("rejecting remove/detach");
            llWhisper(0, "Not stripping");
            ack(cmd_id, id, command, "ko");
            return;
        }
    }
     workaroundForAtClear(command);
     workaroundForAtClear(command);
     rememberForceSit(command);
     rememberForceSit(command);
Line 404: Line 432:
     ack(cmd_id, id, command, "ok"); // acknowledge
     ack(cmd_id, id, command, "ok"); // acknowledge
}
}
 
// check for @clear
// check for @clear
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
// Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
Line 417: Line 445:
     }
     }
}
}
 
// 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)
Line 428: Line 456:
         return;
         return;
     }
     }
 
     tokens_command=llParseString2List(behav, [":"], []);
     tokens_command=llParseString2List(behav, [":"], []);
     behav=llList2String (tokens_command, 0); // @sit
     behav=llList2String (tokens_command, 0); // @sit
Line 441: Line 469:
     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)
Line 460: Line 488:
{
{
     kSource=NULL_KEY;
     kSource=NULL_KEY;
     llOwnerSay("@detach=y"); //== Allow detach
     if(!lockstatus)
        llOwnerSay("@detach=y");
     integer i;
     integer i;
     integer len=llGetListLength (lRestrictions);
     integer len=llGetListLength (lRestrictions);
Line 470: Line 499:
     loginPendingForceSit = FALSE;
     loginPendingForceSit = FALSE;
}
}
 
   
   
// ---------------------------------------------------
// ---------------------------------------------------
Line 487: Line 516:
     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 507: Line 536:
     }
     }
}
}
 
// send a ping request and start a timer
// send a ping request and start a timer
pingWorldObjectIfUnderRestrictions()
pingWorldObjectIfUnderRestrictions()
Line 517: Line 546:
         timerTickCounter = 0;
         timerTickCounter = 0;
         llSetTimerEvent(1.0);
         llSetTimerEvent(1.0);
        lastPingTime = llGetUnixTime();
         loginWaitingForPong = TRUE;
         loginWaitingForPong = TRUE;
    }
}
// Handle commands
HandleCommand(string message, key id)
{
    list templist = llParseString2List(llToLower(message), [" "], []);
    string cmd = llList2String(templist, 0);
    if(cmd == "relay")
    {
        if(id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1))))
        {
            integer change = 0;
           
            string second = llList2String(templist, 1);
           
            if(kSource != NULL_KEY && id == llGetOwner())
            {
                llOwnerSay("You cannot change relay modes while the relay is locked.");
                return; 
            }
           
            if(id == ownerkey && (second == "secondaries" || second == "sec"))
            {
                string third = llList2String(templist, 2);
                if(third == "on" || third == "auto" || (third == "" && !secaccess))
                {
                    secaccess = 1;
                    llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");
                }
                else
                {
                    secaccess = 0;
                    llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");
                }
            }
           
            else if(second == "on" || second == "auto")
            {
                nMode = MODE_AUTO;
                change = 1;
            }
            else if(second == "off")
            {
                nMode = MODE_OFF;
                change = 1; 
            }
            else if(second == "ask")
            {
                nMode = MODE_ASK;
                change = 1; 
            }
           
            else if(second == "" || second == "mode")
            {
                nMode++;
                if(nMode > 2) nMode = 0;
                change = 1; 
            }
           
            if(change)
            {
                setby = NULL_KEY;
                if (nMode == MODE_OFF)
                {
                    llSetTimerEvent(0.0);
                    releaseRestrictions();
                }
                else
                {
                    llSetTimerEvent((float)PING_INTERVAL);
                    if(nMode == MODE_AUTO) setby = id;
                }
                if(id == llGetOwner())
                    llOwnerSay(getModeDescription()); 
                else
                    llSay(0, getModeDescription());
                   
                llMessageLinked(LINK_THIS, 63, nullstr, nullstr);
            }
        }
        else if(id == llGetOwner())
        {
            llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it."); 
        }
     }
     }
}
}
Line 525: Line 642:
     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)
        {
            // Handle Commands on the public or alternate channel
            HandleCommand(str, id);
        }
        else if(num == 33 && id != nullkey)
        {
            // Collar script is giving us an owner
            ownerkey = id;
        }
        else if(num == 34 && id != nullkey)
        {
            // Collar script is giving us a secondary owner
            secowners = secowners + [ id ];
        }
        else if(num == 35)
        {
            // Collar script is clearing owners
            ownerkey = nullkey;
            secowners = [];
        }
        else if(num == 36)
        {
            // Collar script is clearing secondary owners
            secowners = [];
        }
        // Handle plugin update
        else if(num == 62)
        {
            string buttons = "Relay Mode";
           
            if(str == nullstr && (id == nullstr || id == nullkey))
            {
                // Add for owner and owners (key)
                llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);
                // Add for sub and unowned sub (key)
                llMessageLinked(LINK_SET, 63, buttons, nullstr);
            }
        }
        else if(num == 65)
        {
            if((integer)str == TRUE)
                lockstatus = 1;
            else
                lockstatus = 0; 
        }
        else if(num == 66) //== Safeword, unlock
        {
            nMode = MODE_OFF;
            releaseRestrictions();
            llOwnerSay(getModeDescription());
        }
    }
   
    attach(key id)
    {
        if(id == NULL_KEY)
            llOwnerSay("@clear"); 
    }
   
     on_rez(integer start_param)
     on_rez(integer start_param)
     {
     {
Line 536: Line 735:
             reinforceKnownRestrictions();
             reinforceKnownRestrictions();
             pingWorldObjectIfUnderRestrictions();
             pingWorldObjectIfUnderRestrictions();
            llSetTimerEvent((float)PING_INTERVAL);
         }
         }
         // remind the current mode to the user
         // remind the current mode to the user
         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 || lastPingTime + PING_INTERVAL <= llGetUnixTime()))
         {
         {
             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.");
Line 553: Line 753:
             releaseRestrictions();
             releaseRestrictions();
         }
         }
 
         if (loginPendingForceSit)
         if (loginPendingForceSit)
         {
         {
Line 562: Line 762:
                 debug("is sitting now");
                 debug("is sitting now");
             }
             }
             else if (timerTickCounter == LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
             else if (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
             {
             {
                 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 573: Line 773:
             }
             }
         }
         }
 
       
         if(sPendingId != NULL_KEY) //== Handle dialog timeout
         if(sPendingId != NULL_KEY)
         {
         {
             if(sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
             if(sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
             {
             {
                //== Pop a dialog up just so the wearer knows what happened
                 llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], DIALOG_CHANNEL);
                 llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], DIALOG_CHANNEL);
                 sPendingId = NULL_KEY;
                 sPendingId = NULL_KEY;
Line 588: Line 787:
         if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
         if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
         {
         {
             llSetTimerEvent(0.0);
            pingWorldObjectIfUnderRestrictions();
             llSetTimerEvent((float)PING_INTERVAL);
         }
         }
     }
     }
Line 596: Line 796:
         if (channel==RLVRS_CHANNEL)
         if (channel==RLVRS_CHANNEL)
         {
         {
            debug("LISTEN: " + message);
             if (!verifyWeAreTarget(message))
             if (!verifyWeAreTarget(message))
             {
             {
               return;
               return;
             }
             }
       
             if (nMode== MODE_OFF)
             if (nMode== MODE_OFF)
             {
             {
Line 607: Line 808:
             }
             }
             if (!isObjectNear(id)) return;
             if (!isObjectNear(id)) 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);
   
   
Line 615: Line 816:
                 return;
                 return;
             }
             }
 
             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
 
             if (!isObjectKnow(id))
             if (!isObjectKnow(id))
             {
             {
Line 626: Line 827:
                 }
                 }
             }
             }
 
             debug("Executing: " + (string) kSource);
             debug("Executing: " + (string) kSource);
             execute(name, id, message);
             execute(name, id, message);
Line 642: Line 843:
                     execute(sPendingName, sPendingId, sPendingMessage);
                     execute(sPendingName, sPendingId, sPendingMessage);
                 }
                 }
 
                else if(kSource == sPendingId)
                    releaseRestrictions();
                 // clear pending request
                 // clear pending request
                 sPendingName="";
                 sPendingName="";
Line 650: Line 853:
         }
         }
     }
     }
      
     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 862:
     }
     }
}
}
</lsl>
</lsl>

Revision as of 17:30, 24 June 2008

This version fixes a few things, like instituting an actual timeout for the ask dialog and fixing a loophole in the "ask" mode that could allow items to restrict you even if you hadn't approved them yet. This is a work in progress. Eventually it will have some of the features suggested by Marine in the original Reference Implementation.

<lsl>

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

integer ALLOW_REMOVE_DETACH = TRUE; integer DEBUG = FALSE;

// --------------------------------------------------- // Amethyst Plugin Variables // ---------------------------------------------------

key nullkey = NULL_KEY; string nullstr = "";

integer secaccess=0; //== Do secondary owners have access to the RL functions?

// Internal variables key ownerkey = nullkey; list secowners = [];

key setby = NULL_KEY; //== Who set the RLV Relay status?

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

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

integer RLVRS_PROTOCOL_VERSION = 1014; // version of the protocol, stated on the specification page

string PREFIX_RL_COMMAND = "@"; string PREFIX_METACOMMAND = "!";

integer RLVRS_CHANNEL = -1812221819; // RLVRS in numbers integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers

integer MAX_OBJECT_DISTANCE = 20; // 20m is llSay distance integer MAX_TIME_AUTOACCEPT_AFTER_FORCESIT = 60; // seconds

integer PERMISSION_DIALOG_TIMEOUT = 30;

integer LOGIN_DELAY_WAIT_FOR_PONG = 10; integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;

integer PING_INTERVAL = 60; //== Time between pings

integer MODE_OFF = 0; integer MODE_ASK = 1; integer MODE_AUTO = 2;

// --------------------------------------------------- // Variables // ---------------------------------------------------

integer nMode;

list lRestrictions; // restrictions currently applied (without the "=n" part) key kSource; // UUID of the object I'm commanded by, always equal to NULL_KEY if lRestrictions is empty, always set if not

string sPendingName; // name of initiator of pending request (first request of a session in mode 1) key sPendingId; // UUID of initiator of pending request (first request of a session in mode 1) string sPendingMessage; // message of pending request (first request of a session in mode 1) integer sPendingTime;

// used on login integer timerTickCounter; // count the number of time events on login (forceSit has to be delayed a bit) integer loginWaitingForPong; integer loginPendingForceSit;

key lastForceSitDestination; integer lastForceSitTime;

integer lastPingTime;

// --------------------------------------------------- // Low Level Communication // ---------------------------------------------------


debug(string x) {

   if (DEBUG)
   {
       llOwnerSay("DEBUG: " + x);
   }

}

// acknowledge or reject ack(string cmd_id, key id, string cmd, string ack) {

   llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);

}

// cmd begins with a '@' sendRLCmd(string cmd) {

   llOwnerSay(cmd);

}

// get current mode as string string getModeDescription() {

   if (nMode == 0) return "RLV Relay is OFF"; 
   else if (nMode == 1) return "RLV Relay is ON (permission needed)"; 
   else return "RLV Relay is ON (auto-accept)"; 

}

// check that this command is for us and not someone else integer verifyWeAreTarget(string message) {

   list tokens = llParseString2List(message, [","], []);
   if (llGetListLength(tokens) == 3) // this is a normal command
   {
     if (llList2String(tokens, 1) == llGetOwner()) // talking to me ?
     {
        return TRUE;
     }
   }
   return FALSE;

}

// --------------------------------------------------- // Permission Handling // ---------------------------------------------------

// are we already under command by this object? integer isObjectKnow(key id) {

   // first some error handling
   if (id == NULL_KEY)
   {
       return FALSE;
   }

   // are we already under command by this object?
   if (kSource == id)
   {
       return TRUE;
   }

   // are we not under command by any object but were we forced to sit on this object recently?
   if ((kSource == NULL_KEY) && (id == lastForceSitDestination))
   {
       debug("on last force sit target");
       if (lastForceSitTime + MAX_TIME_AUTOACCEPT_AFTER_FORCESIT > llGetUnixTime())
       {
           debug("and recent enough to auto accept");
           return TRUE;
       }
   }

   return FALSE;

}


// check whether the object is in llSay distance. // The specification requires llSay instead of llShout or llRegionSay // to be used to limit the range. But this has to be checked here again // because the objects are not trustworthy. integer isObjectNear(key id) {

   vector myPosition = llGetRootPosition();
   list temp = llGetObjectDetails(id, ([OBJECT_POS]));
   vector objPostition = llList2Vector(temp,0);
   float distance = llVecDist(objPostition, myPosition);
   return distance <= MAX_OBJECT_DISTANCE;

}

// do a basic check on the identity of the object trying to issue a command integer isObjectIdentityTrustworthy(key id) {

   key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);
   key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);
   key object_owner=llGetOwnerKey(id);
   key object_group=llList2Key (llGetObjectDetails (id, [OBJECT_GROUP]), 0);

   debug("owner= " + (string) parcel_owner + " / " + (string) object_owner);
   debug("group= " + (string) parcel_group + " / " + (string) object_group);

   if (object_owner==llGetOwner ()        // IF I am the owner of the object
     || object_owner==parcel_owner        // OR its owner is the same as the parcel I'm on
     || object_group==parcel_group        // OR its group is the same as the parcel I'm on
   )
   {
       return TRUE;
   }
   return FALSE;

}


// Is this a simple request for information or a meta command like !release? integer isSimpleRequest(list list_of_commands) {

   integer len = llGetListLength(list_of_commands);
   integer i;

   debug("Checking simplicity of commands...");

   // now check every single atomic command
   for (i=0; i < len; ++i)
   {
       string command = llList2String(list_of_commands, i);
       if (!isSimpleAtomicCommand(command))
       {
          debug("Command "+ command +" fails simplicity check.");
          return FALSE;
       }
   }

   // all atomic commands passed the test
   return TRUE;

}

// is this a simple atmar command // (a command which only queries some information or releases restrictions) // (e. g.: cmd ends with "=" and a number (@version, @getoutfit, @getattach) or is a !-meta-command) integer isSimpleAtomicCommand(string cmd) {

   // check right hand side of the "=" - sign
   integer index = llSubStringIndex (cmd, "=");
   if (index > -1) // there is a "=" 
   {
       // check for a number after the "="
       string param = llGetSubString (cmd, index + 1, -1);
       if (((integer)param!=0 || param=="0") && llSubStringIndex(param, "n") <= -1 && llSubStringIndex(param, "add")<= -1) // is it an integer (channel number)?
       {
           return TRUE;
       }

       // removing restriction
       if ((param == "y") || (param == "rem"))
       {
           return TRUE;
       }
   }

   // check for a leading ! (meta command)
   if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
   {
       return TRUE;
   }

   // check for @clear
   // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
   // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around
   // a bug in the first relay implementation. You should refuse to use relay versions < 1013
   // instead.)
   if (cmd == "@clear")
   {
       return TRUE;
   }

   // this one is not "simple".
   return FALSE;

}

// If we already have commands from this object pending // because of a permission request dialog, just add the // new commands at the end. // Note: We use a timeout here because the player may // have "ignored" the dialog. integer tryToGluePendingCommands(key id, string commands) {

   if ((sPendingId == id) && (sPendingTime + PERMISSION_DIALOG_TIMEOUT > llGetUnixTime()))
   {
       debug("Gluing " + sPendingMessage + " with " + commands);
       sPendingMessage = sPendingMessage + "|" + commands;
       return TRUE;
   }
   return FALSE;

}

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

   debug("Verifying permission for command "+ message);
   
   // is it switched off?
   if (nMode == MODE_OFF)
   {
       return FALSE;
   }

   // extract the commands-part
   list tokens = llParseString2List (message, [","], []);
   if (llGetListLength (tokens) < 3)
   {
       return FALSE;
   }
   string commands = llList2String(tokens, 2);
   list list_of_commands = llParseString2List(commands, ["|"], []);

   // accept harmless commands silently
   if (isSimpleRequest(list_of_commands))
   {
       debug("Simple command, performing");
       return TRUE;
   }

   // if we are already having a pending permission-dialog request for THIS object,
   // just add the new commands at the end of the pending command list.
   if (tryToGluePendingCommands(id, commands))
   {
       debug("Appending to store of commands pending approval.");
       return FALSE; //== Glue the commands and process them later
   }

   // check whether this object belongs here
   integer trustworthy = isObjectIdentityTrustworthy(id);
   string warning = "";
   if (!trustworthy)
   {
       warning = "\n\nWARNING: This object is not owned by the people owning this parcel. Unless you know the owner, you should deny this request.";
   }

   // ask in permission-request-mode and/OR in case the object identity is suspisous.
   if (nMode == MODE_ASK || !trustworthy)
   {
       sPendingId=id;
       sPendingName=name;
       sPendingMessage=message;
       sPendingTime = llGetUnixTime();
       
       llSetTimerEvent(2.0);
       
       if(llKey2Name(llGetOwnerKey(id)) != "")
           name += " (owned by "+llKey2Name(llGetOwnerKey(id))+")";
       
       llDialog (llGetOwner(), name + " would like control your viewer." + warning + ".\n\nDo you accept ?", ["Yes", "No"], DIALOG_CHANNEL);
       debug("Asking for permission");
       return FALSE;
   }
   return TRUE;

}


// --------------------------------------------------- // Executing of commands // ---------------------------------------------------

// execute a non-parsed message // this command could be denied here for policy reasons, (if it were implemenetd) // but this time there will be an acknowledgement execute(string name, key id, string message) {

   list tokens=llParseString2List (message, [","], []);
   if (llGetListLength (tokens)==3) // this is a normal command
   {
       string cmd_id=llList2String (tokens, 0); // CheckAttach
       key target=llList2Key (tokens, 1); // UUID
       if (target==llGetOwner ()) // talking to me ?
       {
           list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);
           integer len=llGetListLength (list_of_commands);
           integer i;
           string command;
           string prefix;
           for (i=0; i<len; ++i) // execute every command one by one
           {
               // a command is a RL command if it starts with '@' or a metacommand if it starts with '!'
               command=llList2String (list_of_commands, i);
               prefix=llGetSubString (command, 0, 0);

               if (prefix==PREFIX_RL_COMMAND) // this is a RL command
               {
                   executeRLVCommand(cmd_id, id, command);
               }
               else if (prefix==PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
               {
                   executeMetaCommand(cmd_id, id, command);
               }
           }
       }
   }

}

// executes a command for the restrained life viewer // with some additinal magic like book keeping executeRLVCommand(string cmd_id, string id, string command) {

   // we need to know whether whether is a rule or a simple command
   list tokens_command=llParseString2List (command, ["="], []);
   string behav=llList2String (tokens_command, 0); // @getattach:skull
   string param=llList2String (tokens_command, 1); // 2222
   integer ind=llListFindList (lRestrictions, [behav]);

   debug("param=" + param);

   if (param=="n" || param=="add") // add to lRestrictions
   {
       if (ind<0) lRestrictions+=[behav];
       if(kSource == NULL_KEY && !lockstatus)
           llOwnerSay("@detach=n");
               
       kSource=id; // we know that kSource is either NULL_KEY or id already
   }
   else if (param=="y" || param=="rem") // remove from lRestrictions
   {
       if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
       if (llGetListLength (lRestrictions)==0)
       {
           kSource=NULL_KEY;
           if(!lockstatus)
               llOwnerSay("@detach=y");
       }
       
   }
   else if (param == "force" && !ALLOW_REMOVE_DETACH)
   {
       debug("force: " + behav);
       list temp = llParseString2List(behav, [":"], []);
       string commandName = llList2String (temp, 0); // @sit
       if (commandName == "@detach" || commandName == "@remoutfit")
       {
           debug("rejecting remove/detach");
           llWhisper(0, "Not stripping");
           ack(cmd_id, id, command, "ko");
           return;
       }
   }

   workaroundForAtClear(command);
   rememberForceSit(command);
   sendRLCmd(command); // execute command
   ack(cmd_id, id, command, "ok"); // acknowledge

}

// check for @clear // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around // a bug in the first relay implementation. You should refuse to use relay versions < 1013 // instead.) workaroundForAtClear(string command) {

   if (command == "@clear")
   {
       releaseRestrictions();
   }

}

// remembers the time and object if this command is a force sit rememberForceSit(string command) {

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

   tokens_command=llParseString2List(behav, [":"], []);
   behav=llList2String (tokens_command, 0); // @sit
   param=llList2String (tokens_command, 1); // <uuid>
   debug("'force'-command:" + behav + "/" + param);
   if (behav != "@sit")
   {
       return;
   }
   lastForceSitDestination = (key) param;
   lastForceSitTime = llGetUnixTime();
   debug("remembered force sit");

}

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

   if (command==PREFIX_METACOMMAND+"version") // checking relay version
   {
       ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
   }
   else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
   {
       releaseRestrictions();
       ack(cmd_id, id, command, "ok");
   }

}

// lift all the restrictions (called by !release and by turning the relay off) releaseRestrictions () {

   kSource=NULL_KEY;
   if(!lockstatus)
       llOwnerSay("@detach=y");
   integer i;
   integer len=llGetListLength (lRestrictions);
   for (i=0; i<len; ++i)
   {
       sendRLCmd(llList2String (lRestrictions, i)+"=y");
   }
   lRestrictions = [];
   loginPendingForceSit = FALSE;

}


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

init() {

   nMode=1;
   kSource=NULL_KEY;
   lRestrictions=[];
   sPendingId=NULL_KEY;
   sPendingName="";
   sPendingMessage="";
   llListen (RLVRS_CHANNEL, "", "", "");
   llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
   llOwnerSay (getModeDescription());

}

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

   integer i;
   integer len=llGetListLength(lRestrictions);
   string restr;
   debug("kSource=" + (string) kSource);
   for (i=0; i<len; ++i)
   {
       restr=llList2String(lRestrictions, i);
       debug("restr=" + restr);
       sendRLCmd(restr+"=n");
       if (restr=="@unsit")
       {
           loginPendingForceSit = TRUE;
       }
   }

}

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

   loginWaitingForPong = FALSE;
   if (kSource != NULL_KEY)
   {
       ack("ping", kSource, "ping", "ping");
       timerTickCounter = 0;
       llSetTimerEvent(1.0);
       lastPingTime = llGetUnixTime();
       loginWaitingForPong = TRUE;
   }

}

// Handle commands HandleCommand(string message, key id) {

   list templist = llParseString2List(llToLower(message), [" "], []);
   string cmd = llList2String(templist, 0);
   if(cmd == "relay")
   {
       if(id == ownerkey || (llListFindList(secowners, [id]) > -1 && (ownerkey == NULL_KEY || secaccess)) || (id == llGetOwner() && (setby == NULL_KEY || setby == llGetOwner() || (setby != ownerkey && llListFindList(secowners, [setby]) <= -1))))
       {
           integer change = 0;
           
           string second = llList2String(templist, 1);
           
           if(kSource != NULL_KEY && id == llGetOwner())
           {
               llOwnerSay("You cannot change relay modes while the relay is locked.");
               return;   
           }
           
           if(id == ownerkey && (second == "secondaries" || second == "sec"))
           {
               string third = llList2String(templist, 2);
               if(third == "on" || third == "auto" || (third == "" && !secaccess))
               {
                   secaccess = 1;
                   llWhisper(0, "Secondary owners can now adjust Restrained Life Relay settings.");
               }
               else
               {
                   secaccess = 0;
                   llWhisper(0, "Secondary owners cannot adjust Restrained Life Relay settings.");
               }
           }
           
           else if(second == "on" || second == "auto")
           {
               nMode = MODE_AUTO;
               change = 1;
           }
           else if(second == "off")
           {
               nMode = MODE_OFF;
               change = 1;   
           }
           else if(second == "ask")
           {
               nMode = MODE_ASK;
               change = 1;   
           }
           
           else if(second == "" || second == "mode")
           {
               nMode++;
               if(nMode > 2) nMode = 0;
               change = 1;   
           }
           
           if(change)
           {
               setby = NULL_KEY;
               if (nMode == MODE_OFF)
               {
                   llSetTimerEvent(0.0);
                   releaseRestrictions();
               }
               else
               {
                   llSetTimerEvent((float)PING_INTERVAL);
                   if(nMode == MODE_AUTO) setby = id;
               }
               if(id == llGetOwner())
                   llOwnerSay(getModeDescription());   
               else
                   llSay(0, getModeDescription());
                   
               llMessageLinked(LINK_THIS, 63, nullstr, nullstr);
           }
       }
       else if(id == llGetOwner())
       {
           llOwnerSay("Sorry, only your owner can deactivate the relay once they enable it.");   
       }
   }

}

default {

   state_entry()
   {
       // Request owner list from the collar
       llMessageLinked(LINK_THIS, 47, nullstr, nullstr);
       // Reset the plugin list
       llMessageLinked(LINK_THIS, 62, nullstr, nullstr);
       init();
   }
   
   // Handle messages from the collar script
   link_message(integer sender, integer num, string str, key id)
   {
       if(num == 47)
       {
           list templist = llParseString2List(str, [","], []);
           integer x;
           integer count = llGetListLength(templist);
           
           // Handle owner list reply
           ownerkey = id;
           secowners = [];
           for(x=0;x<count;x++)
           {
               secowners = secowners + [ (key)llList2String(templist, x) ];
           }
       }
       // Prefixless commands
       else if(num == 48)
       {
           // Handle Commands on the public or alternate channel
           HandleCommand(str, id);
       }
       else if(num == 33 && id != nullkey)
       {
           // Collar script is giving us an owner
           ownerkey = id;
       }
       else if(num == 34 && id != nullkey)
       {
           // Collar script is giving us a secondary owner
           secowners = secowners + [ id ];
       }
       else if(num == 35)
       {
           // Collar script is clearing owners
           ownerkey = nullkey;
           secowners = [];
       }
       else if(num == 36)
       {
           // Collar script is clearing secondary owners
           secowners = [];
       }
       // Handle plugin update
       else if(num == 62)
       {
           string buttons = "Relay Mode";
           
           if(str == nullstr && (id == nullstr || id == nullkey))
           {
               // Add for owner and owners (key)
               llMessageLinked(LINK_SET, 62, "Relay Sec", buttons);
               // Add for sub and unowned sub (key)
               llMessageLinked(LINK_SET, 63, buttons, nullstr);
           }
       }
       else if(num == 65)
       {
           if((integer)str == TRUE)
               lockstatus = 1;
           else
               lockstatus = 0;   
       }
       else if(num == 66) //== Safeword, unlock
       {
           nMode = MODE_OFF;
           releaseRestrictions();
           llOwnerSay(getModeDescription());
       }
   }
   
   attach(key id)
   {
       if(id == NULL_KEY)
           llOwnerSay("@clear");   
   }
   
   on_rez(integer start_param)
   {
       // relogging, we must refresh the viewer and ping the object if any
       // if mode is not OFF, fire all the stored restrictions
       if (nMode)
       {
           reinforceKnownRestrictions();
           pingWorldObjectIfUnderRestrictions();
           llSetTimerEvent((float)PING_INTERVAL);
       }
       // remind the current mode to the user
       llOwnerSay(getModeDescription());
   }


   timer()
   {
       timerTickCounter++;
       debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
       if (loginWaitingForPong && (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_PONG || lastPingTime + PING_INTERVAL <= llGetUnixTime()))
       {
           llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
           loginWaitingForPong = FALSE;
           loginPendingForceSit = FALSE;
           releaseRestrictions();
       }

       if (loginPendingForceSit)
       {
           integer agentInfo = llGetAgentInfo(llGetOwner());
           if (agentInfo & AGENT_SITTING)
           {
               loginPendingForceSit = FALSE;
               debug("is sitting now");
           }
           else if (timerTickCounter >= LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
           {
               llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
               loginPendingForceSit = FALSE;
               releaseRestrictions();
           }
           else
           {
                sendRLCmd ("@sit:"+(string)kSource+"=force");
           }
       }
       
       if(sPendingId != NULL_KEY)
       {
           if(sPendingTime + PERMISSION_DIALOG_TIMEOUT <= llGetUnixTime())
           {
               llDialog(llGetOwner(),"Request to control your viewer by "+ sPendingName +" automatically denied due to timeout.", ["OK"], DIALOG_CHANNEL);
               sPendingId = NULL_KEY;
               sPendingName = "";
               sPendingMessage = "";    
           }  
       }

       if (!loginPendingForceSit && !loginWaitingForPong && sPendingId == NULL_KEY)
       {
           pingWorldObjectIfUnderRestrictions();
           llSetTimerEvent((float)PING_INTERVAL);
       }
   }

   listen(integer channel, string name, key id, string message)
   {
       if (channel==RLVRS_CHANNEL)
       {
           debug("LISTEN: " + message);
           if (!verifyWeAreTarget(message))
           {
              return;
           }

           if (nMode== MODE_OFF)
           {
               debug("deactivated - ignoring commands");
               return; // mode is 0 (off) => reject
           }
           if (!isObjectNear(id)) return;

           debug("Got message (active world object " + (string) kSource + "): name=" + name+ "id=" + (string) id + " message=" + message);

           if (kSource != NULL_KEY && kSource != id)
           {
               debug("already used by another object => reject");
               return;
           }

           loginWaitingForPong = FALSE; // whatever the message, it is for me => it satisfies the ping request

           if (!isObjectKnow(id))
           {
               debug("asking for permission because kSource is NULL_KEY");
               if (!verifyPermission(id, name, message))
               {
                   return;
               }
           }

           debug("Executing: " + (string) kSource);
           execute(name, id, message);
       }
       else if (channel==DIALOG_CHANNEL)
       {
           if (id != llGetOwner())
           {
               return; // only accept dialog responses from the owner
           }
           if (sPendingId!=NULL_KEY)
           {
               if (message=="Yes") // pending request authorized => process it
               {
                   execute(sPendingName, sPendingId, sPendingMessage);
               }
               else if(kSource == sPendingId)
                   releaseRestrictions();

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

}

</lsl>