Difference between revisions of "LSL Protocol/Restrained Love Relay/Reference Implementation"

From Second Life Wiki
Jump to navigation Jump to search
Line 213: Line 213:
//~ 2008-02-24 more fixes by Maike Short
//~ 2008-02-24 more fixes by Maike Short
//~ 2008-03-03 code cleanup by Maike Short
//~ 2008-03-03 code cleanup by Maike Short
//~ 2008-03-05 silently ignore commands for removing restrictions if they are not active anyway


//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy,
Line 231: Line 232:
// ---------------------------------------------------
// ---------------------------------------------------
   
   
integer RLVRS_PROTOCOL_VERSION = 1013; // version of the protocol, stated on the specification page
integer RLVRS_PROTOCOL_VERSION = 1014; // version of the protocol, stated on the specification page
   
   
string PREFIX_RL_COMMAND = "@";
string PREFIX_RL_COMMAND = "@";
Line 239: Line 240:
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers
integer DIALOG_CHANNEL = -1812220409; // RLVDI in numbers


integer MAX_OBJECT_DISTANCE = 20;    // 20 m 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 = 300; // 300 is 5 minutes
integer PERMISSION_DIALOG_TIMEOUT = 30;


integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
integer LOGIN_DELAY_WAIT_FOR_PONG = 10;
Line 262: Line 265:
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;


// used on login
// used on login
Line 282: Line 286:
   
   
// acknowledge or reject
// acknowledge or reject
Ack (string cmd_id, key id, string cmd, string ack)
ack(string cmd_id, key id, string cmd, string ack)
{
{
  llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
    llSay(RLVRS_CHANNEL, cmd_id + "," + (string)id + "," + cmd + "," + ack);
}
}


// cmd begins with a '@'  
// cmd begins with a '@'  
SendRLCmd (string cmd)
sendRLCmd(string cmd)
{
{
  llOwnerSay (cmd);
    llOwnerSay(cmd);
}
}


Line 296: Line 300:
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)";  
    else if (nMode == 1) return "RLV Relay is ON (permission needed)";  
  else return "RLV Relay is ON (auto-accept)";  
    else return "RLV Relay is ON (auto-accept)";  
}
}


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


Line 322: Line 326:
integer isObjectKnow(key id)
integer isObjectKnow(key id)
{
{
    // first some error handling
     if (id == NULL_KEY)
     if (id == NULL_KEY)
     {
     {
Line 327: Line 332:
     }
     }


    // are we already under command by this object?
     if (kSource == id)
     if (kSource == id)
     {
     {
Line 332: Line 338:
     }
     }


     if (id == lastForceSitDestination)
    // 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");
         debug("on last force sit target");
Line 362: Line 369:
integer isObjectIdentityTrustworthy(key id)
integer isObjectIdentityTrustworthy(key id)
{
{
  key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);
    key parcel_owner=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_OWNER]), 0);
  key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);
    key parcel_group=llList2Key (llGetParcelDetails (llGetPos (), [PARCEL_DETAILS_GROUP]), 0);
  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
      || 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;
 
    // now check every single atomic command
    for (i=0; i < len; ++i)
    {
        string command = llList2String(list_of_commands, i);
        if (!isSimpleAtomicCommand(command))
        {
          return FALSE;
        }
    }


  if (object_owner==llGetOwner ()        // IF I am the owner of the object
     // all atomic commands passed the test
    || 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 TRUE;
  }
  return FALSE;
}
}


// is this a simple atmar command
// Is this a simple request for information or a meta command like !release?
// (a command which only queries some information or releases restrictions)
// (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 IsSimpleRequest (string cmd)  
integer isSimpleAtomicCommand(string cmd)
{
{
  integer multiple = llSubStringIndex (cmd, "|");
    // check right hand side of the "=" - sign
  if (multiple > -1)
    integer index = llSubStringIndex (cmd, "=");
  {
    if (index > -1) // there is a "="
    return FALSE; // this is a list of commands, so it is not simple
    {
  }
        // check for a number after the "="
        string param = llGetSubString (cmd, index + 1, -1);
        if ((integer)param!=0 || param=="0") // is it an integer (channel number)?
        {
            return TRUE;
        }


  // check for a number after the "="
        // removing restriction
  integer ind=llSubStringIndex (cmd, "=");
        if ((param == "y") || (param == "rem"))
  if (ind>-1) // there is a "="  
        {
  {
            return TRUE;
     string param=llGetSubString (cmd, ind+1, -1);
        }
     if ((integer)param!=0 || param=="0") // is integer?
    }
 
     // check for a leading ! (meta command)
     if (llSubStringIndex(cmd, PREFIX_METACOMMAND) == 0)
     {
     {
      return TRUE;
        return TRUE;
    }
 
    // check for @clear
    // Note: @clear MUST NOT be used because the restrictions will be reapplied on next login
    // (but we need this check here because "!release|@clear" is a BROKEN attempt to work around
    // a bug in the first relay implementation. You should refuse to use relay versions < 1013
    // instead.)
    if (cmd == "@clear")
    {
        return TRUE;
     }
     }
  }
    
    
  // check for a leading ! (meta command)
    // this one is not "simple".
  list tokens=llParseString2List (cmd, [","], []);
    return FALSE;
  if (llGetListLength (tokens)>=3)
}
  {
 
    string cmd_cmd=llList2String (tokens, 2);
// If we already have commands from this object pending
    return (llSubStringIndex(cmd_cmd, PREFIX_METACOMMAND) == 0); // starts with !
// because of a permission request dialog, just add the
  }
// new commands at the end.
 
// Note: We use a timeout here because the player may
  // this one is not "simple".
// have "ignored" the dialog.
  return FALSE;
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;
}
}


Line 419: Line 472:
integer verifyPermission(key id, string name, string message)
integer verifyPermission(key id, string name, string message)
{
{
  // is it switched off?
    // is it switched off?
  if (nMode == MODE_OFF)
    if (nMode == MODE_OFF)
  {
    {
     return FALSE;
        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))
    {
        return TRUE;
    }


  // accept harmless commands silently
    // if we are already having a pending permission-dialog request for THIS object,
  if (IsSimpleRequest(message))
    // just add the new commands at the end of the pending command list.
  {
    if (tryToGluePendingCommands(id, commands))
    return TRUE;
    {
  }
        return TRUE;
    }


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


  // ask in permission-request-mode and/OR in case the object identity is suspisous.
    // ask in permission-request-mode and/OR in case the object identity is suspisous.
  if (nMode == MODE_ASK || !trustworthy)
    if (nMode == MODE_ASK || !trustworthy)
  {
    {
    // TODO: handle multiple messages
        sPendingId=id;
    sPendingId=id;
        sPendingName=name;
    sPendingName=name;
        sPendingMessage=message;
    sPendingMessage=message;
        sPendingTime = llGetUnixTime();
    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");
    return FALSE;
        return FALSE;
  }
    }
  return TRUE;
    return TRUE;
}
}


Line 461: Line 530:
// 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)
// but this time there will be an acknowledgement
// but this time there will be an acknowledgement
Execute (string name, key id, string message)
execute(string name, key id, string message)
{
{
  list tokens=llParseString2List (message, [","], []);
    list tokens=llParseString2List (message, [","], []);
  if (llGetListLength (tokens)==3) // this is a normal command
    if (llGetListLength (tokens)==3) // this is a normal command
  {
    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), ["|"], []);
         string cmd_id=llList2String (tokens, 0); // CheckAttach
         integer len=llGetListLength (list_of_commands);
         key target=llList2Key (tokens, 1); // UUID
         integer i;
         if (target==llGetOwner ()) // talking to me ?
        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 '!'
            list list_of_commands=llParseString2List (llList2String (tokens, 2), ["|"], []);
          command=llList2String (list_of_commands, i);
            integer len=llGetListLength (list_of_commands);
          prefix=llGetSubString (command, 0, 0);
            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
                if (prefix==PREFIX_RL_COMMAND) // this is a RL command
          {
                {
            executeRLVCommand(cmd_id, id, command);
                    executeRLVCommand(cmd_id, id, command);
          }
                }
          else if (prefix==PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
                else if (prefix==PREFIX_METACOMMAND) // this is a metacommand, aimed at the relay itself
          {
                {
            executeMetaCommand(cmd_id, id, command);
                    executeMetaCommand(cmd_id, id, command);
          }
                }
      }
            }
        }
     }
     }
  }
}
}


Line 498: Line 567:
executeRLVCommand(string cmd_id, string id, string command)
executeRLVCommand(string cmd_id, string id, string command)
{
{
  // we need to know whether whether is a rule or a simple command
    // we need to know whether whether is a rule or a simple command
  list tokens_command=llParseString2List (command, ["="], []);
    list tokens_command=llParseString2List (command, ["="], []);
  string behav=llList2String (tokens_command, 0); // @getattach:skull
    string behav=llList2String (tokens_command, 0); // @getattach:skull
  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 (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];
    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
  }
    }
  else if (param=="y" || param=="rem") // remove from lRestrictions
    else if (param=="y" || param=="rem") // remove from lRestrictions
  {
    {
    if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
        if (ind>-1) lRestrictions=llDeleteSubList (lRestrictions, ind, ind);
    if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY;
        if (llGetListLength (lRestrictions)==0) kSource=NULL_KEY;
  }
    }


  rememberForceSit(command);
    workaroundForAtClear(command);
  SendRLCmd (command); // execute command
    rememberForceSit(command);
  Ack (cmd_id, id, command, "ok"); // acknowledge
    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();
    }
}
}


Line 547: Line 630:
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==PREFIX_METACOMMAND+"version") // checking relay version
  {
    {
    Ack (cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
        ack(cmd_id, id, command, (string)RLVRS_PROTOCOL_VERSION);
  }
    }
  else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
    else if (command==PREFIX_METACOMMAND+"release") // release all the restrictions (end session)
  {
    {
    releaseRestrictions ();
        releaseRestrictions();
    Ack (cmd_id, id, command, "ok");
        ack(cmd_id, id, command, "ok");
  }
    }
}
}
   
   
Line 561: Line 644:
releaseRestrictions ()
releaseRestrictions ()
{
{
  kSource=NULL_KEY;
    kSource=NULL_KEY;
  integer i;
    integer i;
  integer len=llGetListLength (lRestrictions);
    integer len=llGetListLength (lRestrictions);
  for (i=0; i<len; ++i)
    for (i=0; i<len; ++i)
  {
    {
    SendRLCmd (llList2String (lRestrictions, i)+"=y");
        sendRLCmd(llList2String (lRestrictions, i)+"=y");
  }
    }
  lRestrictions = [];
    lRestrictions = [];
  loginPendingForceSit = FALSE;
    loginPendingForceSit = FALSE;
}
}


   
   
// ---------------------------------------------------
// ---------------------------------------------------
//                 Event Handling
//           initialisation and login handling
// ---------------------------------------------------
// ---------------------------------------------------
   
   
Init () {
init() {
  nMode=1;
    nMode=1;
  kSource=NULL_KEY;
    kSource=NULL_KEY;
  lRestrictions=[];
    lRestrictions=[];
  sPendingId=NULL_KEY;
    sPendingId=NULL_KEY;
  sPendingName="";
    sPendingName="";
  sPendingMessage="";
    sPendingMessage="";
  llListen (RLVRS_CHANNEL, "", "", "");
    llListen (RLVRS_CHANNEL, "", "", "");
  llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
    llListen (DIALOG_CHANNEL, "", llGetOwner(), "");
  llOwnerSay (getModeDescription());
    llOwnerSay (getModeDescription());
}
}


default
// sends the known restrictions (again) to the RL-viewer
// (call this functions on login)
reinforceKnownRestrictions()
{
{
  state_entry ()
    integer i;
  {
     integer len=llGetListLength(lRestrictions);
     Init ();
    string restr;
  }
    debug("kSource=" + (string) kSource);
     for (i=0; i<len; ++i)
  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)
     {
     {
      integer i;
         restr=llList2String(lRestrictions, i);
      integer len=llGetListLength (lRestrictions);
      string restr;
      debug("kSource=" + (string) kSource);
      for (i=0; i<len; ++i)
      {
         restr=llList2String (lRestrictions, i);
         debug("restr=" + restr);
         debug("restr=" + restr);
         SendRLCmd (restr+"=n");
         sendRLCmd(restr+"=n");
         if (restr=="@unsit")
         if (restr=="@unsit")
         {
         {
          loginPendingForceSit = TRUE;
            loginPendingForceSit = TRUE;
         }
         }
      }
    }
      // send a ping request, start a timer
}
      loginWaitingForPong = FALSE;
 
      if (kSource != NULL_KEY)
// send a ping request and start a timer
      {
pingWorldObjectIfUnderRestrictions()
         Ack ("ping", kSource, "ping", "ping");
{
    loginWaitingForPong = FALSE;
    if (kSource != NULL_KEY)
    {
         ack("ping", kSource, "ping", "ping");
         timerTickCounter = 0;
         timerTickCounter = 0;
         llSetTimerEvent(10.0);
         llSetTimerEvent(1.0);
         loginWaitingForPong = TRUE;
         loginWaitingForPong = TRUE;
      }
     }
     }
     // remind the current mode to the user
}
    llOwnerSay (getModeDescription());
 
  }
default
{
    state_entry()
    {
        init();
    }
    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.");
        loginWaitingForPong = FALSE;
        loginPendingForceSit = FALSE;
        releaseRestrictions();
      }


      if (loginPendingForceSit)
    timer()
      {
    {
         integer agentInfo = llGetAgentInfo(llGetOwner());
         timerTickCounter++;
         if (agentInfo & AGENT_SITTING)
        debug("timer (" + (string) timerTickCounter + "): waiting for pong: " + (string) loginWaitingForPong + " pendingForceSit: " + (string) loginPendingForceSit);
         if (loginWaitingForPong && (timerTickCounter == LOGIN_DELAY_WAIT_FOR_PONG))
         {
         {
          loginPendingForceSit = FALSE;
            llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because the device is not available.");
          debug("is sitting now");
            loginWaitingForPong = FALSE;
            loginPendingForceSit = FALSE;
            releaseRestrictions();
         }
         }
         else if (timerTickCounter == LOGIN_DELAY_WAIT_FOR_FORCE_SIT)
 
         if (loginPendingForceSit)
         {
         {
          llWhisper(0, "Lucky Day: " + llKey2Name(llGetOwner()) + " is freed because sitting down again was not possible.");
            integer agentInfo = llGetAgentInfo(llGetOwner());
          loginPendingForceSit = FALSE;
            if (agentInfo & AGENT_SITTING)
          releaseRestrictions();
            {
                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");
            }
         }
         }
         else
 
         if (!loginPendingForceSit && !loginWaitingForPong)
         {
         {
          SendRLCmd ("@sit:"+(string)kSource+"=force");
            llSetTimerEvent(0.0);
         }
         }
      }
    }
 
      if (!loginPendingForceSit && !loginWaitingForPong)
      {
        llSetTimerEvent(0.0);
      }
  }
   
   
  listen(integer channel, string name, key id, string message)
    listen(integer channel, string name, key id, string message)
  {
    if (channel==RLVRS_CHANNEL)
     {
     {
      if (!verifyWeAreTarget(message))
        if (channel==RLVRS_CHANNEL)
      {
        {
        return;
            if (!verifyWeAreTarget(message))
      }
            {
              return;
            }
          
          
      if (nMode== MODE_OFF)
            if (nMode== MODE_OFF)
      {
            {
          debug("deactivated - ignoring commands");
                debug("deactivated - ignoring commands");
          return; // mode is 0 (off) => reject
                return; // mode is 0 (off) => reject
      }
            }
      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);
   
   
      if (kSource != NULL_KEY && kSource != id)
            if (kSource != NULL_KEY && kSource != id)
      {
            {
        debug("already used by another object => reject");
                debug("already used by another object => reject");
        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))
            {
                debug("asking for permission because kSource is NULL_KEY");
                if (!verifyPermission(id, name, message))
                {
                    return;
                }
            }


      if (!isObjectKnow(id))
            debug("Executing: " + (string) kSource);
      {
            execute(name, id, message);
        debug("asking for permission because kSource is NULL_KEY");
         }
         if (!verifyPermission(id, name, message))
        else if (channel==DIALOG_CHANNEL)
         {
         {
          return;
            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);
                }
 
                // clear pending request
                sPendingName="";
                sPendingId=NULL_KEY;
                sPendingMessage="";
            }
         }
         }
      }
      debug("Executing: " + (string) kSource);
      Execute (name, id, message);
     }
     }
     else if (channel==DIALOG_CHANNEL)
     touch_start(integer num_detected)
     {
     {
      if (id != llGetOwner())
        // touched by user => cycle through OFF/ON_PERMISSION/ON_ALWAYS modes
      {
        key toucher=llDetectedKey(0);
        return; // only accept dialog responses from the owner
         if (toucher==llGetOwner())
      }
      if (sPendingId!=NULL_KEY)
      {
         if (message=="Yes") // pending request authorized => process it
         {
         {
          Execute (sPendingName, sPendingId, sPendingMessage);
            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());
         }
         }
    }


        // clear pending request
     changed(integer change)
        sPendingName="";
        sPendingId=NULL_KEY;
        sPendingMessage="";
      }
     }
  }
  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)
        if (change & CHANGED_OWNER)  
      {
        {
        llOwnerSay("Sorry, you cannot change the relay mode while it is locked.");
            llResetScript();
         return;
         }
      }
      ++nMode;
      if (nMode>2) nMode=0;
      if (nMode==MODE_OFF) releaseRestrictions ();
      llOwnerSay (getModeDescription());
     }
     }
  }
  changed(integer change)
  {
    if (change & CHANGED_OWNER) llResetScript ();
  }
}
}
</lsl>
</lsl>

Revision as of 12:46, 4 March 2008

Restrained Life viewer v1.10.1 Relay Protocol Specification

VERSION 1.013

By Marine Kelley


Audience

This document is meant for people who want to create or modify in-world objects to use the features of someone else's RestrainedLife viewer, typically cages and pieces of furniture, which per definition are usually not owned by that person.


Introduction

The RestrainedLife viewer only executes commands issued through llOwnerSay () messages. Therefore, in order to issue commands to someone using the viewer who does not own the object, that person must wear an attachment that relays commands after some security checks.


Why this spec ?

Now that the RestrainedLife viewer v1.10.1 is out, many cages and furniture creators are interested in using its features such as sit, outfit, tp etc. These objects can be found in public places, or owned by friends... but as they are usually not owned by the user, the relay has to implement some basic security in order to avoid griefing. On top of it, the user does not want to be forced to switch to another relay when going to the next piece of furniture.

This is the purpose of this specification : to lay common rules so all the relays implementing it are compatible with all the furnitures implementing it too. Without such a specification, one cage/furniture would talk to the relay specifically made to operate with it and that's all, eventually making the creator stay behind because people rather use standard objects than proprietary closed ones.

Basic principle

Here is a sample use case :

  1. User is wearing a Relay
  2. User enters a cage in a public area
  3. Cage sends a chat message on a known private channel (for instance "@tploc=n")
  4. Relay receives the message, decides to repeat the command to the user and blocks their ability to teleport from the map with an llOwnerSay ("@tploc=n");
  5. User is allowed to get out after some time, the cage issues a "@tploc=y" command, immediately repeated by the relay

Without the relay, the cage could never prevent the user from teleporting since it doesn't belong to them.


Requirements

Here are the informal requirements for a relay (formal requirements below).


Security

Any object sending commands over the channel the relay listens to is likely to harm the user if there is no security implemented in the former. For instance, one could rez an object that sends a "@remoutfit=force" command over the chat channel to force an avatar to get naked in front of everyone without a warning. Of course nobody wants that, so basic security is needed.


User-friendliness

Security often implies control (access lists, switch, permissions...) so the user must be given some basic control over what kind of objects are allowed to issue commands.


Versatility

Some users will prefer wearing a dedicated attachment that they can unwear any time they want, others will be required to have the relay locked on them so they cannot detach them, others will want the script only... It is important to keep those differences in mind when deciding about the permissions of the relay. However, it is the user's responsibility to choose the relay that suits their needs best.


Licensing

According to the level of complexity and support of the relay, the creator is allowed to either provide it for free (open/close source) or to sell it, as long as it implements all the formal requirements.


Common questions

How hard is it to implement such a specification ?

That depends on what you do. Furniture/cage makers will find it very easy for it only comes down to sending commands over a chat channel and getting feedback. Relay makers will find it harder but then again, that depends on the level of security and user-friendliness they wish to offer. But make no mistake, the relay is what does almost all the job (along with the viewer), because there will be many more kinds of furnitures and cages than relays around.

Why do other people need to write such a relay ? Couldn't you write it yourself and publish it ?

Of course I could, and there is even a working code for a basic relay at the end of this page. However :

  • The protocol is likely to improve because nobody sees the future
  • One object only would not suit everyone's needs
  • It would have to implement perfect security and perfect user-friendliness, in all cases
  • It would of course have to be perfectly scripted, without any bug whatsoever

The security and user-friendliness are the key parts here. Some users will prefer to be safe from griefing, others will prefer a good user interface, others will like a lot of features, others will want to move the script elsewhere... Everyone has their own tastes so there can be no one-size-fits-all relay.


Protocol

In-world objects send chat message over a channel (common to every relay and furniture), that the relay(s) acknowledges or not. If the message is a correct command and passes whatever security checks the relay implements, the latter repeats it as an llOwnerSay ().

When the session ends, possibly after several relogs, the object clears all the restrictions it has put the user under.


Example

Here is a basic example of messages exchanged between an avatar (id "9213...") and a world object (id "7adf...") :

Object : CmdTest,9213f69a-ed7d-4a70-907a-7dba88c8831a,@tploc=n
Relay executes llOwnerSay ("@tploc=n");
Relay  : CmdTest,7adf6218-ab26-8566-8387-660133840794,@tploc=n,ok
Object : BunchoCommands,9213f69a-ed7d-4a70-907a-7dba88c8831a,@tploc=n|@tplm=n|@tplure=n|@remoutfit:shoes=force
Relay executes llOwnerSay ("@tploc=n");
Relay executes llOwnerSay ("@tplm=n");
Relay executes llOwnerSay ("@tplure=n");
Relay  : BunchoCommands,7adf6218-ab26-8566-8387-660133840794,@tploc=n,ok
Relay  : BunchoCommands,7adf6218-ab26-8566-8387-660133840794,@tplm=n,ok
Relay  : BunchoCommands,7adf6218-ab26-8566-8387-660133840794,@tplure=n,ok
Relay  : BunchoCommands,7adf6218-ab26-8566-8387-660133840794,@remoutfit:shoes=force,ko
After a relog :
Relay  : ping,7adf6218-ab26-8566-8387-660133840794,ping,ping
Object : ping,9213f69a-ed7d-4a70-907a-7dba88c8831a,!pong   (UUID found with llGetOwnerKey(id), where id is the sender-parameter of the listen-event)

Formal requirements

Common requirements to the Relay and the in-world object

  • The chat channel used by both the Relay and the in-world object is -1812221819. That's "RLVRS" ("RestrainedLife Viewer Relay Script") translated from alphabet to numbers.
  • Messages on this channel are sent only with llSay (), neither llShout () nor llWhisper () is allowed. This ensures that when closer than 20 m the command is heard, so if no reply is received that means the other party does not want to play or is already busy.
    • Messages from in-world object to Relay (3 tokens) :
      • message  ::= <cmd_name>,<user_uuid>,<list_of_commands>
      • list_of_commands  ::= <command>[|<list_of_commands>] (list_of_commands is *lowercase*)
      • command  ::= <rl_command> or <metacommand>
      • rl_command  ::= @behav[:option][=param]
      • metacommand  ::= !version or !release or !pong
    • Messages from Relay to in-world object (4 tokens) :
      • message  ::= <cmd_name>,<object_uuid>,<command>,<reply> (cmd_name is equal to the one in the incoming message)
      • reply  ::= ok or ko or ping or <protocol_version>
      • protocol_version  ::= integer (it is the version of the specification, not of the script)
  • The effect of the "!release" metacommand is to wipe out all the restrictions issued by the object which sends it.
  • The effect of the "!version" metacommand is to send the version of the protocol the Relay implements. See below.
  • The effect of the "ping" message from the Relay to the object is to check the latter is still available. If not, release the user to avoid having orphaned rules.
  • Notice that acknowledgments do not apply to the list of commands but to one command only. Therefore a list of N commands gives N acknowledgment messages in return (at most).

In plain English :

  • <cmd_name> is the name of the command, decided by the object. It will be used to find out which command has been acknowledged, therefore it must be repeated exactly by the Relay (without changing its case). An exception to the freedom of choice of the <cmd_name> token is the "ping" reserved name, see below.
  • <user_uuid> is the UUID of the avatar owning the Relay.
  • <object_uuid> is the UUID of the in-world object. Notice that we never need the UUID of the Relay as it's usually an attachment, prone to change its id after each relog.
  • <list_of_commands> is a list of RL commands separated by pipes ('|'). It can be a single command (meaning no pipe present).
  • <command> is either a regular RL command (@behav:option=param) or a metacommand, aimed at the Relay itself (!version, !release and !pong)
  • Commands are separated by pipes ('|') here, but if they must be sent in the same llOwnerSay to the viewer they must be separated by commas (',') and with only one '@' sign at the beginning of the whole message. This is on purpose, to force the Relay to parse them and check them one by one, as well as facilitate the parsing of the whole message coming from the in-world object.

"!release" metacommand

  • This metacommand is meant to make the Relay clear all the restrictions linked to the object issuing it. It is better to use it than to issue "counter-commands" to lift every restriction one by one without forgetting any.

"!version" metacommand

  • When receiving this metacommand, the Relay must send a special acknowledgment that contains the version of the protocol it implements, on 4 digits. This number must be an integer, equal to the version of this specification, written just after the title on this very page, times 1000. For instance, "1.120" would translate to "1120". It makes it easier to compare versions without fear of losing information with a float cast to a string and back to a float.
  • Do not mistake the version of the viewer with the version of the protocol and the version of the scripts.

"ping" relay message and "!pong" object metacommand

  • When logging on, the relay will reapply all the stored restrictions but it only makes sense if the in-world object is still around and available for use. It could have been reset, crashed, or used by someone else while the primary user was offline. To let the relay apply the restrictions would therefore make no sense. That's why the relay has to ask the object if it's still around and available, and if no appropriate answer is received in a timely fashion then it must lift all the restrictions issued by it before, in order to start fresh again. Notice that "ping" is a simple word (to stay consistent with "ok" and "ko") while "!pong" is a metacommand. However, any other message issued by the object and aimed at the relay can prove it is still available as well.

The relay message must be "ping,<object_uuid>,ping,ping" and the object message must be "ping,<user_uuid>,!pong". This allows the object to keep a listener open with a static filter, to reduce lag. <user_uuid> can be retrieved by a llGetOwnerKey () call.

Relay requirements

  • Send the exact @behav:option=param part in an llOwnerSay (), without any change whatsoever.
  • Retain a list of restrictions and their sources for when the user relogs.
  • Force sit if unsit is prevented when relogging.
  • When relogging, send a "ping" message (see above) to check the in-world object is still available. If no message after a few seconds (not necessarily a "!pong", any message aimed at the relay can do), release the rules linked to this object.
  • The name of the script must contain "RLVnnn" where nnn is the minimal version of the viewer it is compatible with (ex : RLV110). The user must have access to that information (dialog, message, object name...) to check everything works correctly.


In-world object requirements

  • When receiving a "ping" message from a relay, reply immediately with a "!pong" message (as described above) aimed at the avatar owning it, provided they are still restricted by the object.
  • Never rely on an answer from the Relay, requests can be denied silently, the Relay can be unworn, the avatar can TP out or crash... => use timeouts.
  • Don't poll the dataserver for online status, the Relay takes care of the relog part.


Sample code of a basic working relay

This particular example that anyone can distribute freely as open-source only (you are not allowed to sell this code) and including the header comments is just meant to give an idea of how a relay basically works.


What it does

  • Implements the specification described hereabove.
  • Commented to facilitate reading and learning.
  • Tested and working with test objects and with real cages.


What it doesn't do

  • Error-checking.
  • Access lists.
  • Lock on the avatar.
  • Handle more than one object.
  • Reject some commands by nature.
  • And so much more that makes good scripts stand out in the crowd.


How to use it

  • Create a prim in which you put a script containing this code (don't forget to name it correctly by following the "RLVnnn" requirement).
  • Wear the prim, it says its current on/off status
  • Click on the prim to switch from "Off" to "On with permission" to "On without permission" and back to "Off"
  • Find an object that implements this specification and test.


Special thanks

  • Chorazin Allen for reviewing the code, giving ideas, coding and re-coding his own scripts to make sure everything works properly between the relay and the cage, and for not killing me every time I change my mind here and there on the spec.


Sample code

<lsl>

//~ RestrainedLife Viewer Relay Script example code //~ By Marine Kelley //~ 2008-02-03 //~ v1.1 //~ 2008-02-16 with fixes by Maike Short //~ 2008-02-24 more fixes by Maike Short //~ 2008-03-03 code cleanup by Maike Short //~ 2008-03-05 silently ignore commands for removing restrictions if they are not active anyway

//~ This code is provided AS-IS, OPEN-SOURCE and holds NO WARRANTY of accuracy, //~ completeness or performance. It may only be distributed in its full source code, //~ this header and disclaimer and is not to be sold.

//~ * Possible improvements //~ Do some error checking //~ Handle more than one object //~ Periodically check that the in-world objects are still around, when one is missing purge its restrictions //~ Manage an access list //~ Reject some commands if not on access list (force remove clothes, force remove attachments...) //~ and much more...


// --------------------------------------------------- // 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 = 300; // 300 is 5 minutes

integer PERMISSION_DIALOG_TIMEOUT = 30;

integer LOGIN_DELAY_WAIT_FOR_PONG = 10; integer LOGIN_DELAY_WAIT_FOR_FORCE_SIT = 60;

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;

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


debug(string x) { // 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;
   // now check every single atomic command
   for (i=0; i < len; ++i)
   {
       string command = llList2String(list_of_commands, i);
       if (!isSimpleAtomicCommand(command))
       {
          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") // 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) {

   // 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))
   {
       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))
   {
       return TRUE;
   }
   // 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();
       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]);
   if (param=="n" || param=="add") // add to lRestrictions
   {
       if (ind<0) lRestrictions+=[behav];
       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;
   }
   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;
   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);
       loginWaitingForPong = TRUE;
   }

}

default {

   state_entry()
   {
       init();
   }

   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.");
           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 (!loginPendingForceSit && !loginWaitingForPong)
       {
           llSetTimerEvent(0.0);
       }
   }

   listen(integer channel, string name, key id, string message)
   {
       if (channel==RLVRS_CHANNEL)
       {
           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);
               }
               // clear pending request
               sPendingName="";
               sPendingId=NULL_KEY;
               sPendingMessage="";
           }
       }
   }

   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)
   {
       if (change & CHANGED_OWNER) 
       {
            llResetScript();
       }
   }

} </lsl>

History

1.013e: no changes in the specificiation, just in the sample code

  • Verified how far away the object is that is trying to control you. The specification says that the object must use llSay for 20meters max range. But as the object is not trustworth this must be checked in the relay again.Previously llShout (for 100 meters) and llRegionSay (for the complete sim) did work, too.
  • the permission request dialog was shown even if the command was for another person
  • the ping/pong on login did not verify whether the pong event was for us and not some other person.
  • fixed a problem which caused additional questions for permission dialogs
  • now automatically accepts commands from an object you were forced to sit on by the relay (so you only have to confirm that once)
  • fixed force sit on re-login which could fail if the login was very slow
  • code cleanup

1.013

  • fixed force-sit on login (by delaying it for 10 seconds)
  • allow meta commands without asking for permission
  • fixed a vulnerability which allowed faked responses for the permission dialog
  • extended object identity check for the object/parcel owner (before it checked only the group but there is groupless personal property out there)
  • prevent turning off of the relay when it is locked

1.012 Fixed a bug in !release which caused the relay to reapply those restrictions on login for the object NULL_KEY. But as there is no ping/pong for NULL_KEY the wearer was stuck.

1.011 Precision on the ping-pong routine : relay standard message would be "ping,<object_uuid>,ping,ping" so objects can keep a listener with a static filter, to reduce lag. Thank you again Monica Jewell for the suggestion.

1.010 Added the ping-pong routine as a way to ensure the object is still available when the user relogs. Also updated the sample code to handle the timeout. Thank you Monica Jewell for pointing that possible problem out.

1.000 First release