all repos — felt @ 65c30712382e1da8757cdaabafa725b42770ed59

virtual tabletop for dungeons and dragons (and similar) using Go, MongoDB, and websockets

connect to websocket using table key as subproto
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmPpAqAACgkQO3+8IhRO
Y5iR+BAAh6WLewIXBc2DoOOOrtY+ifjSt79vg6dSlTuhrxyzCJpYzdjyzYggl7Ax
j61D/iQaeGeMzF7OblUOWF0PtIfWrK5TLRwaCm9VyAsXlR8nJebyI7Ma5rk5t6Ap
2YbQ2CtgPUX2XeTWWMjq8sCH+WwMoSXx3nqPlwc5uWYokr3UjJv8lSEw9cuTXEEQ
7x768BiLBhWFvUU29z3cendjQwBoXBERQ1KLlXvj4iW2nfsnwktE/CEZlVKWm65M
mSsX0omOBMJzNsWmSXb8Lw2aJ/v8n1TjhmlmJySBfw+yi7ntlcRUY15bNfthekOV
oXnOGVGG3ug5ZDTwNcQjhmxIsxSlXYEvNs+pd7K4DCWf0EFGWt6uARkyQgy7Votv
zS9JzbENacWV/M65WD2cy9HbS2tTuaToaH196XvPx1m716Sa8aQ5DHlIwEC2DPL3
gLzgPt54b/f8Loqe5zJE9BVdN520WfQ+bp9l9JYOkcGqJn7t8MeBMnWNq4moCWcL
I22WFzzQ7Z9eto7UhonlB5purz+qEVTvRQfHQvIixUWmqrzZmobYWAHEbG/tS70m
SEJMy1dOXVfkkm6N1XK0dXpgUfUOc6LRhWFd99SeDB8hewN2dvq+UV3yzXYco328
qKO9xzciKnOsKWHUyGf2IRN6eIeQWyY7wgH0/uRlraybI/6DjEY=
=e7Qf
-----END PGP SIGNATURE-----
commit

65c30712382e1da8757cdaabafa725b42770ed59

parent

6fc78c333ce18f6951ea6809580861d131cdc3a1

5 files changed, 108 insertions(+), 90 deletions(-)

jump to
M gametable/server.gogametable/server.go

@@ -1,7 +1,7 @@

package gametable import ( - "bytes" + "bytes" "context" "encoding/json" "errors"

@@ -16,6 +16,7 @@ "io/ioutil"

"log" "net/http" "nhooyr.io/websocket" + "strings" "sync" "time" )

@@ -56,7 +57,15 @@ self.serveMux.ServeHTTP(w, r)

} func (self *GameTableServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) + protocols, err := self.dbAdapter.GetProtocols() + if err != nil { + self.logf("%v", err) + return + } + + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: protocols, + }) if err != nil { self.logf("%v", err) return

@@ -87,14 +96,16 @@ closeSlow: func() {

c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") }, } - - tableKey := models.TableKey{} - err := json.NewDecoder(r.Body).Decode(&tableKey) - if err != nil { - fmt.Println(err.Error()) - return err + subprotoParts := strings.Split(c.Subprotocol(), ".") + if len(subprotoParts) != 2 { + return errors.New("Couldn't decode subprotocol") + } + tableKey := models.TableKey{ + Name: subprotoParts[0], + Passcode: subprotoParts[1], } + // subprotocol is guaranteed to be an existing table, but maybe just leave this here if !self.dbAdapter.CheckTable(tableKey) { return errors.New("Table with matching key was not found on this server") }

@@ -165,12 +176,32 @@ }

} } -func (self *GameTableServer) getCurrentState(tableKey models.TableKey) ([]byte) { +func (self *GameTableServer) getCurrentState(tableKey models.TableKey) []byte { // get diceroll log, map, and token state + if self.dbAdapter.CheckTable(tableKey) { + mapUrl, _ := self.dbAdapter.GetMapImageUrl(tableKey) + auxMessage, _ := self.dbAdapter.GetAuxMessage(tableKey) + availableTokens, _ := self.dbAdapter.GetTokens(tableKey, true) + activeTokens, _ := self.dbAdapter.GetTokens(tableKey, false) + diceRolls, _ := self.dbAdapter.GetDiceRolls(tableKey) - // build into a []byte message - - return make([]byte, 1) + table := models.Table{ + Name: tableKey.Name, + Passcode: tableKey.Passcode, + DiceRolls: diceRolls, + MapImageUrl: mapUrl, + Tokens: activeTokens, + AvailableTokens: availableTokens, + AuxMessage: auxMessage, + } + data, err := json.Marshal(table) + if err != nil { + return []byte("{\"error\": \"" + err.Error() + "\"}") + } else { + return data + } + } + return []byte("{\"error\": \"table not found\"}") } func (self *GameTableServer) writeToDB(tableMsg models.TableMessage) error {
M mongodb/adapter.gomongodb/adapter.go

@@ -23,6 +23,7 @@ CreateTable(table models.TableKey) error

DestroyTable(table models.TableKey) error CheckTable(table models.TableKey) bool + GetProtocols() ([]string, error) InsertDiceRoll(table models.TableKey, diceRoll models.DiceRoll) error GetDiceRolls(table models.TableKey) ([]models.DiceRoll, error)

@@ -132,6 +133,30 @@ })

return res != nil } return false +} + +func (self *DbEngine) GetProtocols() ([]string, error) { + tables := self.db.Collection("tables") + if tables != nil { + var results []models.Table + cursor, err := tables.Find(self.mkCtx(10), bson.D{}) + if err != nil { + return []string{}, err + } + + if err = cursor.All(self.mkCtx(10), &results); err != nil { + return []string{}, err + } + + var protocols []string + + for _, t := range results { + protocols = append(protocols, fmt.Sprintf("%s.%s", t.Name, t.Passcode)) + } + return protocols, nil + + } + return []string{}, errors.New(fmt.Sprintf(errNoCollection, "tables")) } func (self *DbEngine) InsertDiceRoll(table models.TableKey, diceRoll models.DiceRoll) error {
M static/index.htmlstatic/index.html

@@ -11,9 +11,14 @@ <body>

<div id="errWrapper" style='display:none'><button id="closeErr" onclick="closeErr()">x</button><div id="errDiv"></div></div> <nav> <input id="name_entry"> - <button id="goto_table">Change Table</button> + <button id="goto_table">Join Table</button> <button id="admin_login">Admin Login</button> </nav> + <form id="table_modal" onsubmit="return false"> + <label>Table Name<input id="input_table_name"></label> + <label>Table Passcode<input id="input_table_pass"></label> + <button type="submit" id="table_join" onclick="dial()">Join</button> + </form> <form id="admin_modal" onsubmit="return false"> <label>usr<input id="input_admin_usr"></label> <label>pass<input type="password" id="input_admin_pass"></label>

@@ -67,6 +72,6 @@ <div id="adminZone"></div>

</div> </body> <script src="./util.js" type="text/javascript"></script> + <script src="./socket.js" type="text/javascript"></script> <script src="./admin.js" type="text/javascript"></script> - <script src="./index.js" type="text/javascript"></script> </html>
D static/index.js

@@ -1,76 +0,0 @@

-;(() => { - // expectingMessage is set to true - // if the user has just submitted a message - // and so we should scroll the next message into view when received. - let expectingMessage = false - function dial() { - const conn = new WebSocket(`ws://${location.host}/subscribe`) - - conn.addEventListener("close", ev => { - appendLog(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`, true) - if (ev.code !== 1001) { - appendLog("Reconnecting in 1s", true) - setTimeout(dial, 1000) - } - }) - conn.addEventListener("open", ev => { - console.info("websocket connected") - }) - - // This is where we handle messages received. - conn.addEventListener("message", ev => { - if (typeof ev.data !== "string") { - console.error("unexpected message type", typeof ev.data) - return - } - const p = appendLog(ev.data) - if (expectingMessage) { - p.scrollIntoView() - expectingMessage = false - } - }) - } - dial() - - const messageLog = document.getElementById("message-log") - const publishForm = document.getElementById("publish-form") - const messageInput = document.getElementById("message-input") - - // appendLog appends the passed text to messageLog. - function appendLog(text, error) { - const p = document.createElement("p") - // Adding a timestamp to each message makes the log easier to read. - p.innerText = `${new Date().toLocaleTimeString()}: ${text}` - if (error) { - p.style.color = "red" - p.style.fontStyle = "bold" - } - messageLog.append(p) - return p - } - appendLog("Submit a message to get started!") - - // onsubmit publishes the message from the user when the form is submitted. - publishForm.onsubmit = async ev => { - ev.preventDefault() - - const msg = messageInput.value - if (msg === "") { - return - } - messageInput.value = "" - - expectingMessage = true - try { - const resp = await fetch("/publish", { - method: "POST", - body: msg, - }) - if (resp.status !== 202) { - throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText}`) - } - } catch (err) { - appendLog(`Publish failed: ${err.message}`, true) - } - } -})()
A static/socket.js

@@ -0,0 +1,33 @@

+let tableKey = { + name: "", + passcode: "" +} + +let table = null; + +let conn = null; + +function dial() { + // get tableKey from UI + const tblNameInput = document.getElementById("input_table_name"); + const tblPassInput = document.getElementById("input_table_pass"); + if (tblNameInput && tblPassInput && tblNameInput.value && tblPassInput.value) { + tableKey.name = tblNameInput.value; + tableKey.passcode = tblPassInput.value; + + conn = new WebSocket(`ws://${location.host}/subscribe`, `${tableKey.name}.${tableKey.passcode}`); + conn.addEventListener("close", e => { + if (e.code !== 1001) { + // TODO: add message to let user know they are reconnecting + setTimeout(dial, 1000) + } + }); + conn.addEventListener("open", e => { + // TODO: add message to let user know they are at the table + console.info("socket connected"); + }); + conn.addEventListener("message", e => { + console.dir(e); + }); + } +}