Room

How to interact with the Room module on the server side including how to set up the builtin Room commands.

IMPORTANT

Room may not be used with Field. What is means is that the user client may not be a member of a room and join a Field at the simultaneously.

How To Set Up Builtin Room Commands

Room module has builtin commands to allow the client create, join, send messages, and leave.

room-setup-builtin-commands

The sample code below is generated by the boilerplate from the builder server.

package roomcmds

import (
    "github.com/Diarkis/diarkis/room"
    "github.com/Diarkis/diarkis/roomSupport"
)

func Expose() {
room.ExposeCommands()
roomSupport.ExposeCommands()
}

How To Set Up Room Backups

Room module optionally allows you to backup and synchronize backups in real-time up to 2 extra backup nodes. 

NOTE: Using room backup increases server stress.

// This will instruct all rooms to create backups on other server nodes and synchronize
room.SetBackup(2)

How To Get Room Property

property := room.GetProperty(roomID, "RoomName")
if property == nil {
    // property RoomName does not exist
}

How To Get Room Properties

properties := room.GetProperties(roomID)

How To Update Room Properties

Callback function is expected to return true if you update the room properties and return false if you do not update the properties.

_ := room.UpdatePropertis(roomID, func(properties map[string]interface{}) bool {
    if _, ok := properties["counter"]; !ok {
        properties["counter"] = 0
        return true
    }
    counter := properties["counter"].(int)
    properties["counter"] = counter + 1
    return true
})

Example of Room property update with failure

_ := room.UpdatePropertis(roomID, func(properties map[string]interface{}) bool {
    if _, ok := properties["booked"]; !ok {
    properties["booked"] = true
    return true
  }
  // The room has already been booked
  return false
})

How To Increment and/or Decrement A Numeric Room Property

Room module has an API to increment and/or decrement a numeric room property. This is useful when you have to update a numeric room property without race conditions.

// If the property called HP does not exist, it will create it
created := room.IncrProperty(roomID, "HP", 1000)

// Decrement the property called HP by -200
decremented := room.IncrProperty(roomID, "HP", -200)

// Increment the property called HP by 100
incremented := room.IncrProperty(roomID, "HP", 100)

Room Properties With Data Capsule

Data Capsule is a data structure provided by Diarkis to help you manage interface{} data safely.

import "github.com/Diarkis/diarkis/datacapsule"

Updating Room Properties Using Data Capsule

_ := room.UpdatePropertis(roomID, func(properties map[string]interface{}) bool {
      if _, ok := properties["counter"]; !ok {
              // Initialize
              properties["counter"] = 0
      }
      capsule := datacapsule.NewCapsule()
      err := capsule.Import(properties["counter"])
      if err != nil {
            // Handle error
            return
      }
      counter := capsule.GetAsInt("counter")
      counter++
      capsule.SetAsInt("counter", counter)
      properties["counter"] = capsule.Export()
})

Reading Room Properties Using Data Capsule

property := room.GetProperty(roomID, "counter")
if property == nil {
    // Handle error
    return
}
capsule := datacapsule.NewCapsule()
err := capsule.Import(property)
if err != nil {
    // Handle error
    return
}
counter := capsule.GetAsInt("counter")

How To Update Remote Room Properties

By using Diarkis’ internal communication mechanism mesh, you can read and/or update remote room properties.

Sending Update Command To The Server Node Where The Target Room Is

property := room.GetProperty(roomID, "counter")
if property == nil {
    // Handle error
    return
}
capsule := datacapsule.NewCapsule()
err := capsule.Import(property)
if err != nil {
    // Handle error
    return
}
counter := capsule.GetAsInt("counter")

Update Logic On A Remote Server Node

// This sets up handleRemoteRoomPropUpdate function to be invoked when mesh.SendRequest sends

// The command message as shown above
mesh.Command(cmdID, handleRemoteRoomPropUpdate)

func handleRemoteRoomPropUpdate(req map[string]interface{}) (error, map[string]interface{}) {
    roomID := mesh.GetString(req, "roomID")
    var err error
    _ := room.UpdateProperties(roomID, func(props map[string]interface{}) bool {
            if _, ok := props["booked"]; !ok {
                    props["booked"] = true
                    // Booked the room successfully
                    return true
            }
            // The room has already been booked...
            err = errors.New("Room has already been booked")
            return false
      })
      res := make(map[string]interface{})
      return res, err
}

How To Reserve A Room With Users

You may reserve places in a room with specific user IDs. When you reserve places in a room, those users with the reserved IDs will be able to join without worrying about if the room is full or not. If you make reservations for all possible members of the room, you may reject all other users without reservations to join.

NOTE: This operation is permitted only to the owner of the room.

Making Reservations

memberIDs := make([]string, 4)
memberIDs[0] = "user-id-123"
memberIDs[1] = "user-id-456"
memberIDs[2] = "user-id-789"
memberIDs[3] = "user-id-012"
// userData is the owner of the room
room.Reserve(roomID, userData, memberIDs)

Canceling Reservations

memberIDs := make([]string, 4)
memberIDs[0] = "user-id-123"
memberIDs[1] = "user-id-456"
memberIDs[2] = "user-id-789"
memberIDs[3] = "user-id-012"
// userData is the owner of the room
room.CancelReservation(roomID, userData, memberIDs)

How To Implement Locking Room Mechanism Using Room Properties

You may implement a locking mechanism to rooms with room properties. The example below demonstrates the implementation of a room locking mechanism that requires the client trying to unlock a room to have a passcode that is known to the room. You may implement a similar mechanism using the Room module’s SetJoinCondition method as well.

Note: The example uses the remote room property update technique that is described here.

addr, err := room.GetRoomNodeAddress(roomID)
if err != nil {
    // Handle error
    return
}
req := make(map[string]interface{})
req["roomID"] = roomID
// passcode comes from the client
req["passcode"] = passcode
// mesh.SendRequest is a inter-pod communication function
mesh.SendRequest(cmdID, addr, req, func(err error, res map[string]interface{}) {
    if err != nil {
           // Handle error
           return
      }
      // The room has been unlocked successfully
})
mesh.Command(cmdID, unlockRoom)

func unlockRoom(req map[string]interface{}) (error, map[string]interface{}) {
      roomID := mesh.GetString(req, "roomID")
      passcode := mesh.GetString(req, "passcode")
      var err error
      _ := room.UpdateProperties(roomID, func(props map[string]interface{}) bool {
     if _, ok := props["unlocked"].(bool); !ok {
           // the room is already unlocked
           err := errors.New("Room already unlocked")
           return false
            }
            if _, ok := props["passcode"].(string); !ok {
                   // missing pass code...
                  err := errors.New("Room is missing passcode...")
                  return false
            }
            if passcode != props["passcode"].(string) {
                  // pass code the client has does not match
                  err := errors.New("Pass code does not match")
                  return false
            }
            // pass code the client has matches
            props["unlocked"] = true
            return true
    })
    res := make(map[string]interface{})
    return res, err
}

How To Add Conditions To All Join Operations

Room’s join operation can optionally have “conditions” added to control how join operations function.

// This callback function will be called every time a join operation is called. By returning an error, you may reject the client to join.
room.SetJoinCondition(func(roomID string, userData *userData) error) {
       // Implement custom logic to control join
       // Example join condition:
      // The new client has to have "code" that has been pre-defined in the room property
      code := room.GetProperty(roomID)
      if code == nil {
            // The room does not have a code, we assume the room to be public: proceed to join
            return nil
      }
      // clientCode must be set to userData by userData.Set() before calling this
      clientCode := GetRoomCodeFromUser(userData.Get("code"))
      if code == clientCode {
              // The code client has matches. Proceed to join
             return nil
      }
      // join rejected
      return errors.New("ClientCode does not match")
})

Capturing Changes In A Room As An Event

Rooms raise an event whenever there is a change to the room. Changes with room properties, change of room members (joining and leaving), and change of room owner will trigger this event.

room.SetOnRoomChange(func(roomID string, memberIDs []string, props                    map[string]interface{}) {
    logger.Debug("There has been a change in the room %s", roomID)
})

Capturing The Event On Room Owner Change

A room has an owner (The client that created the room or automatically elected member of the room). You may capture the event that is raised when the owner changes.

room.SetOnRoomOwnerChange(func(params interface{}) {
      roomData := params.(map[string]string)
      roomID := roomData["string"]
      roomOWnerUID := roomData["ownerID"]
      logger.Debug("The owner of the room %s has changed to %s", roomID, roomOwnerUID)
})

Capturing The Event On Room Property Change

The event is raised when room properties are changed.

room.SetOnRoomPropertyUpdate(func(params interface{}) {
    roomData := params.(map[string]interface{})
    roomID := roomData["roomID"].(string)
    properties := roomData.["properties"].(interface{}).(map[string]interface{})
    logger.Debug("Room %s has detect change in its properties %v", roomID, properties)
})

Capturing The Event On Room Destruction

The event is raised when a room has been deleted from the server.

room.SetOnRoomDiscard(func(roomID string) {
        logger.Debug("Room %s has been discarded", roomID)
})

How To Use Announce()

room.Announce allows you to send messages to other members of a room. The difference between Broadcast and Message is that the sender client does NOT have to be a member of the room to send messages.

While it is flexible and useful in some cases not to worry about being a member of the room to send messages, it is also difficult to use Announce correctly without creating unexpected security holes.

If you let any client have access to Announce, Clients can send messages to any room as long as they know the targeted room ID. This could be exploited.

It is very important to have a control logic to call Announce in order to avoid abuse from the client.

The example below uses Announce in the callback of an event. By not having the client access directly to Announce, we can prevent numbers of potential security risks.

// SetOnAnnounce is raised when Broadcast, Message, Announce are invoked
room.SetOnAnnounce(func(roomID string, ver uint8, cmd uint16, msg []byte) {
// We capture the message sent to the room and send it outside such as CDN etc.
data := CreateCDNData(roomID, ver, cmd, msg)
SendCDN(data)
})

How To Retrieve The Internal Address Of The Room From Room ID

Room ID contains the internal address of the server that the room is stored.

roomID, err := room.GetRoomNodeAddressList(roomID)