サーバ側で Room モジュールを操作する方法

ここでは、サーバ側で Room モジュールを操作する方法を説明し、組み込みの Room コマンドを設定する方法も紹介します。

重要

Room と Field は同時に使用することはできません。

ルームの組み込みコマンドの設定方法

Room モジュールには、クライアントの作成、参加、メッセージの送信、離脱を行うためのコマンドが組み込まれています。

room-setup-builtin-commands

以下のサンプルコードは、ボイラープレートによって生成されたものです。

package roomcmds

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

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

Room バックアップの設定方法

Room モジュールでは、オプションで2つの追加バックアップノードまで、リアルタイムでバックアップと同期をとることができます。

NOTE:  バックアップを有効にするとサーバの負荷が増えます。

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

Room のプロパティを取得する方法

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

Room のプロパティを更新する方法

コールバック関数は、Room のプロパティを更新した場合は true を返し、プロパティを更新しなかった場合は false を返すことが期待されます。

_ := 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
})

Room のプロパティの更新に失敗した場合の例

_ := 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
})

Room の数値プロパティをインクリメントまたはデクリメントする方法

Room モジュールには、数値プロパティをインクリメントおよび/またはデクリメントする API があります。これは、同期的に数値プロパティを更新する必要がある場合に便利です。

// 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)

Data Capsule

Data Capsuleは、interface{} のデータを安全に管理するために、Diarkis が提供するデータ構造です。

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

Data Capsuleを使った Room のプロパティの更新

_ := 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()
})

Data Capsuleを使った Room のプロパティ読み込み

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")

リモート Room のプロパティを更新する方法

Diarkis の内部通信機構であるメッシュを利用することで、リモート Room のプロパティを読み込んだり、更新したりすることができます。

対象となる Room があるサーバノードへのアップデートコマンドの送信

addrlist, err := room.GetRoomNodeAddressList(roomID)
if err != nil {
// Handle error
return
}
req := make(map[string]interface{})
req["roomID"] = roomID
addr := addrlist[0] // the rest in the array are the backups
mesh.SendRequest(cmdID, addr, req, func(err error, res map[string]interface{}) {
if err != nil {
// Handle error
return
  }
  // Remote room property updated successfully
})

リモートサーバノードでのロジックの更新

// 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
}

Room 参加者を特定のユーザー ID 予約する方法

特定のユーザー ID で Room を予約することができます。予約すると、予約したIDを持つユーザーは、Room の上限を気にすることなく参加することができます。ただし、全員分の予約をしてしまうと、予約をしていないユーザーの参加を拒否することができます。

注: この操作は、Room のオーナーにのみ許可されています。

予約する

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)

予約を取り消す

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)

Room のプロパティを利用したロック機能の実装方法

Room のプロパティを使って Room のロックを実装することができます。以下の例は、Room  のロックを解除しようとするクライアントが、 Room に設定されているパスコードを持っていることを要求する Room のロックメカニズムの実装です。同様の仕組みは、Room モジュールの SetJoinCondition メソッドでも実装できます。

: 以下の例では、リモート Room のプロパティ更新手法を使用しています。

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
}

すべての Join 操作に条件を追加する方法

Room の Join 操作には、機能を制御するための条件をオプションで追加することができます。

// 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")
})

Room の変化イベントを捕捉する

Room は、Room に変更があったときにイベントを発生させます。プロパティの変更、メンバーの変更(加入・離脱)、オーナーの変更などがこのイベントを発生させます。

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

Room のオーナーが変わった時のイベントを補足する

Room には、オーナー(Room を作成したクライアント、または自動的に選出されたメンバー)がいます。オーナーが変更されたときに発生するイベントをキャプチャすることができます。

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)
})

Room のプロパティ変更イベントを補足する

このイベントは、部屋のプロパティが変更されたときに発生します。

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)
})

Room の削除イベントを補足する

このイベントは、Room がサーバから削除されたときに発生するイベントです。

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

Announce() の使い方

room.Announce() を使うと、Room の他のメンバーにメッセージを送ることができます。ブロードキャストとメッセージの違いは、メッセージを送るために送信者のクライアントが Room のメンバーである必要がないことです。

メッセージを送信するために Room のメンバーであることを必要としないことは、柔軟で便利な場合もありますが、予期せぬセキュリティホールを作ってしまう可能性があります。

例えば任意のクライアントに Announce() へのアクセスを許可した場合、クライアントは対象となる Room ID を知っていれば、どの Room にもメッセージを送ることができ、このことを悪用される可能性があります。

クライアントからの悪用を防ぐために、Announce() を呼び出す制御ロジックを用意することは非常に重要です。

以下の例では、Announce() をイベントのコールバックで使用しています。クライアントがAnnounce() に直接アクセスしないようにすることで、潜在的なセキュリティリスクを防ぐことができます。

// 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)
})

Room ID から Room の内部アドレスを取得する方法

Room IDには、Room が存在するサーバーの内部アドレスが含まれており、これを取得することができます。

roomID, err := room.GetRoomNodeAddressList(roomID)