A few gaming features added, e.g. rolling dice, flipping coin, eightball.

This commit is contained in:
Storm Dragon
2026-09-03 13:25:32 -04:00
parent d0f78b1295
commit 77c687b6fe
12 changed files with 871 additions and 11 deletions
+50
View File
@@ -0,0 +1,50 @@
package rtpconn
import (
"crypto/rand"
"fmt"
"io"
"math/big"
"strings"
"git.stormux.org/storm/skald/hall"
)
var coinSides = []string{"heads", "tails"}
func chooseCoinSide(reader io.Reader) (string, error) {
index, err := rand.Int(reader, big.NewInt(int64(len(coinSides))))
if err != nil {
return "", err
}
return coinSides[index.Int64()], nil
}
func validateCoinRequest(value any) error {
if value == nil {
return nil
}
arguments, ok := value.(string)
if !ok || strings.TrimSpace(arguments) != "" {
return hall.UserError("/coin does not take any arguments")
}
return nil
}
func handleCoinFlip(c *webClient, g *hall.Hall, value any) error {
if !member("message", c.Permissions()) {
return c.error(hall.UserError("not authorised"))
}
if err := validateCoinRequest(value); err != nil {
return c.error(err)
}
side, err := chooseCoinSide(rand.Reader)
if err != nil {
return c.error(err)
}
announcement := fmt.Sprintf("%s flips a coin: %s.", c.Username(), side)
if err := broadcastServerChat(g, "Coin", "coin", announcement); err != nil {
return c.error(err)
}
return nil
}
+90
View File
@@ -0,0 +1,90 @@
package rtpconn
import (
"bytes"
"strings"
"testing"
)
func TestChooseCoinSide(t *testing.T) {
for i, want := range []string{"heads", "tails"} {
got, err := chooseCoinSide(bytes.NewReader([]byte{byte(i)}))
if err != nil {
t.Fatalf("chooseCoinSide(%d): %v", i, err)
}
if got != want {
t.Fatalf("chooseCoinSide(%d) = %q, want %q", i, got, want)
}
}
}
func TestCoinRejectsArguments(t *testing.T) {
for _, value := range []any{nil, "", " "} {
if err := validateCoinRequest(value); err != nil {
t.Fatalf("validateCoinRequest(%#v): %v", value, err)
}
}
for _, value := range []any{"twice", 2} {
if err := validateCoinRequest(value); err == nil {
t.Fatalf("validateCoinRequest(%#v) unexpectedly succeeded", value)
}
}
}
func TestCoinFlipIsServerGeneratedAndStoredInHistory(t *testing.T) {
hallName := newChalkboardTestHall(t)
flipper := addTestWebClient(t, hallName, "Participant", "message")
observer := addTestWebClient(t, hallName, "Operator", "op")
if err := handleClientMessage(flipper, clientMessage{
Type: "hallaction", Kind: "coin", Value: "",
}); err != nil {
t.Fatalf("handleClientMessage(coin): %v", err)
}
for name, messages := range map[string][]clientMessage{
"flipper": drainMessages(flipper),
"observer": drainMessages(observer),
} {
if len(messages) != 1 {
t.Fatalf("%s received %d messages, want 1: %#v", name, len(messages), messages)
}
message := messages[0]
if message.Type != "chat" || message.Kind != "coin" || !message.Privileged {
t.Fatalf("%s received non-authoritative coin message: %#v", name, message)
}
if message.Source != "" || message.Username == nil || *message.Username != "Coin" {
t.Fatalf("%s received coin message with unexpected identity: %#v", name, message)
}
text, ok := message.Value.(string)
if !ok || (text != "Participant flips a coin: heads." &&
text != "Participant flips a coin: tails.") {
t.Fatalf("%s received malformed coin announcement %q", name, text)
}
}
history := flipper.hall.GetChatHistory()
if len(history) != 1 || history[0].Kind != "coin" {
t.Fatalf("coin history = %#v", history)
}
}
func TestCoinFlipRequiresMessagePermission(t *testing.T) {
hallName := newChalkboardTestHall(t)
flipper := addTestWebClient(t, hallName, "Participant", "message")
flipper.permissions = remove("message", flipper.permissions)
if err := handleClientMessage(flipper, clientMessage{
Type: "hallaction", Kind: "coin", Value: "",
}); err != nil {
t.Fatalf("handleClientMessage(coin): %v", err)
}
if got := flipper.hall.GetChatHistory(); len(got) != 0 {
t.Fatalf("unauthorised coin flip entered history: %#v", got)
}
messages := drainMessages(flipper)
if len(messages) != 1 || messages[0].Kind != "error" ||
!strings.Contains(messages[0].Value.(string), "not authorised") {
t.Fatalf("unauthorised coin response = %#v", messages)
}
}
+147
View File
@@ -0,0 +1,147 @@
package rtpconn
import (
"crypto/rand"
"fmt"
"math/big"
"regexp"
"strconv"
"strings"
"git.stormux.org/storm/skald/hall"
)
const (
maxDiceCount = 100
maxDiceSides = 1_000_000
maxDiceModifier = 1_000_000
dicePreviewSize = 5
)
var diceExpressionRegexp = regexp.MustCompile(
`^\s*([0-9]+)\s*[dD]\s*([0-9]+)(?:\s*([+-])\s*([0-9]+))?\s*$`,
)
type diceRoll struct {
Count int64
Sides int64
Modifier int64
Results []int64
Total int64
}
func parseDiceExpression(expression string) (diceRoll, error) {
match := diceExpressionRegexp.FindStringSubmatch(expression)
if match == nil {
return diceRoll{}, hall.UserError(
"invalid dice roll; example: /2d6 + 3",
)
}
count, err := strconv.ParseInt(match[1], 10, 64)
if err != nil || count < 1 || count > maxDiceCount {
return diceRoll{}, hall.UserError(
fmt.Sprintf("dice count must be between 1 and %d", maxDiceCount),
)
}
sides, err := strconv.ParseInt(match[2], 10, 64)
if err != nil || sides < 2 || sides > maxDiceSides {
return diceRoll{}, hall.UserError(
fmt.Sprintf("die sides must be between 2 and %d", maxDiceSides),
)
}
var modifier int64
if match[4] != "" {
modifier, err = strconv.ParseInt(match[4], 10, 64)
if err != nil || modifier > maxDiceModifier {
return diceRoll{}, hall.UserError(
fmt.Sprintf("dice modifier must be between -%d and %d", maxDiceModifier, maxDiceModifier),
)
}
if match[3] == "-" {
modifier = -modifier
}
}
return diceRoll{Count: count, Sides: sides, Modifier: modifier}, nil
}
func generateDiceRoll(roll diceRoll) (diceRoll, error) {
maximum := big.NewInt(roll.Sides)
roll.Results = make([]int64, roll.Count)
roll.Total = roll.Modifier
for i := range roll.Results {
result, err := rand.Int(rand.Reader, maximum)
if err != nil {
return diceRoll{}, err
}
roll.Results[i] = result.Int64() + 1
roll.Total += roll.Results[i]
}
return roll, nil
}
func formatDiceResults(results []int64) string {
shown := len(results)
if shown > dicePreviewSize {
shown = dicePreviewSize
}
parts := make([]string, shown)
for i := range parts {
parts[i] = strconv.FormatInt(results[i], 10)
}
if len(results) > dicePreviewSize {
return strings.Join(parts, ", ") + fmt.Sprintf(", and %d more", len(results)-shown)
}
if shown == 1 {
return parts[0]
}
if shown == 2 {
return parts[0] + " and " + parts[1]
}
return strings.Join(parts[:shown-1], ", ") + ", and " + parts[shown-1]
}
func formatDiceAnnouncement(username string, roll diceRoll) string {
dieWord := "dice"
if roll.Count == 1 {
dieWord = "die"
}
modifier := ""
if roll.Modifier > 0 {
modifier = fmt.Sprintf(", plus %d", roll.Modifier)
} else if roll.Modifier < 0 {
modifier = fmt.Sprintf(", minus %d", -roll.Modifier)
}
return fmt.Sprintf(
"%s rolls %d %d-sided %s: %s%s, for a total of %d.",
username, roll.Count, roll.Sides, dieWord,
formatDiceResults(roll.Results), modifier, roll.Total,
)
}
func handleDiceRoll(c *webClient, g *hall.Hall, value any) error {
if !member("message", c.Permissions()) {
return c.error(hall.UserError("not authorised"))
}
expression, ok := value.(string)
if !ok {
return c.error(hall.UserError("invalid dice roll; example: /2d6 + 3"))
}
roll, err := parseDiceExpression(expression)
if err != nil {
return c.error(err)
}
roll, err = generateDiceRoll(roll)
if err != nil {
return c.error(err)
}
announcement := formatDiceAnnouncement(c.Username(), roll)
if err := broadcastServerChat(g, "Dice", "dice", announcement); err != nil {
return c.error(err)
}
return nil
}
+223
View File
@@ -0,0 +1,223 @@
package rtpconn
import (
"regexp"
"strings"
"testing"
"git.stormux.org/storm/skald/hall"
)
func TestParseDiceExpression(t *testing.T) {
tests := []struct {
expression string
count int64
sides int64
modifier int64
}{
{"2d4", 2, 4, 0},
{"10d8 + 5", 10, 8, 5},
{"3 d 2+5", 3, 2, 5},
{"2 D 20 - 1", 2, 20, -1},
{" 1d6 ", 1, 6, 0},
}
for _, test := range tests {
t.Run(test.expression, func(t *testing.T) {
got, err := parseDiceExpression(test.expression)
if err != nil {
t.Fatalf("parseDiceExpression(%q): %v", test.expression, err)
}
if got.Count != test.count || got.Sides != test.sides || got.Modifier != test.modifier {
t.Fatalf("parseDiceExpression(%q) = %#v", test.expression, got)
}
})
}
}
func TestParseDiceExpressionRejectsInvalidAndOversizedRolls(t *testing.T) {
tests := []string{
"", "d20", "2d", "2d1", "0d6", "101d6", "2d1000001",
"2d6 + 1000001", "2 0d6", "2d6 trailing",
}
for _, expression := range tests {
t.Run(expression, func(t *testing.T) {
if _, err := parseDiceExpression(expression); err == nil {
t.Fatalf("parseDiceExpression(%q) unexpectedly succeeded", expression)
}
})
}
}
func TestFormatDiceAnnouncement(t *testing.T) {
tests := []struct {
name string
roll diceRoll
expected string
}{
{
"two dice",
diceRoll{Count: 2, Sides: 4, Results: []int64{1, 2}, Total: 3},
"Storm rolls 2 4-sided dice: 1 and 2, for a total of 3.",
},
{
"many dice with modifier",
diceRoll{Count: 10, Sides: 10, Modifier: 5, Results: []int64{2, 3, 4, 3, 4, 1, 2, 1, 2, 3}, Total: 30},
"Storm rolls 10 10-sided dice: 2, 3, 4, 3, 4, and 5 more, plus 5, for a total of 30.",
},
{
"one die with negative modifier",
diceRoll{Count: 1, Sides: 20, Modifier: -1, Results: []int64{17}, Total: 16},
"Storm rolls 1 20-sided die: 17, minus 1, for a total of 16.",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := formatDiceAnnouncement("Storm", test.roll); got != test.expected {
t.Fatalf("formatDiceAnnouncement() = %q, want %q", got, test.expected)
}
})
}
}
func TestGenerateDiceRollStaysWithinBounds(t *testing.T) {
roll, err := generateDiceRoll(diceRoll{Count: 100, Sides: 6, Modifier: -50})
if err != nil {
t.Fatalf("generateDiceRoll: %v", err)
}
if len(roll.Results) != 100 {
t.Fatalf("generated %d results, want 100", len(roll.Results))
}
wantTotal := int64(-50)
for _, result := range roll.Results {
if result < 1 || result > 6 {
t.Fatalf("generated result %d outside 1..6", result)
}
wantTotal += result
}
if roll.Total != wantTotal {
t.Fatalf("total = %d, want %d", roll.Total, wantTotal)
}
}
func TestDiceRollIsServerGeneratedAndStoredInHistory(t *testing.T) {
hallName := newChalkboardTestHall(t)
roller := addTestWebClient(t, hallName, "Participant", "message")
observer := addTestWebClient(t, hallName, "Operator", "op")
if err := handleClientMessage(roller, clientMessage{
Type: "hallaction",
Kind: "roll",
Value: "2d4 + 5",
}); err != nil {
t.Fatalf("handleClientMessage(roll): %v", err)
}
for name, messages := range map[string][]clientMessage{
"roller": drainMessages(roller),
"observer": drainMessages(observer),
} {
if len(messages) != 1 {
t.Fatalf("%s received %d messages, want 1: %#v", name, len(messages), messages)
}
message := messages[0]
if message.Type != "chat" || message.Kind != "dice" || !message.Privileged {
t.Fatalf("%s received non-authoritative dice message: %#v", name, message)
}
if message.Source != "" || message.Username == nil || *message.Username != "Dice" {
t.Fatalf("%s received dice message with unexpected identity: %#v", name, message)
}
text, ok := message.Value.(string)
if !ok || !regexp.MustCompile(`^Participant rolls 2 4-sided dice: [1-4] and [1-4], plus 5, for a total of (7|8|9|10|11|12|13)\.$`).MatchString(text) {
t.Fatalf("%s received malformed dice announcement %q", name, text)
}
}
history := roller.hall.GetChatHistory()
if len(history) != 1 || history[0].Kind != "dice" {
t.Fatalf("dice history = %#v", history)
}
newcomer := testWebClient("admin-id")
adminName := "Admin"
if _, err := hall.AddClient(hallName, newcomer, hall.ClientCredentials{
Username: &adminName,
Password: "pw",
}); err != nil {
t.Fatalf("AddClient(Admin): %v", err)
}
newcomer.hall = hall.Get(hallName)
drainActions(t, newcomer)
var historicalDice *clientMessage
for _, message := range drainMessages(newcomer) {
if message.Type == "chathistory" && message.Kind == "dice" {
message := message
historicalDice = &message
break
}
}
if historicalDice == nil || !historicalDice.Privileged {
t.Fatalf("new participant did not receive authoritative dice history: %#v", historicalDice)
}
}
func TestDiceRollRequiresMessagePermission(t *testing.T) {
hallName := newChalkboardTestHall(t)
roller := addTestWebClient(t, hallName, "Participant", "message")
roller.permissions = remove("message", roller.permissions)
if err := handleClientMessage(roller, clientMessage{
Type: "hallaction", Kind: "roll", Value: "1d6",
}); err != nil {
t.Fatalf("handleClientMessage(roll): %v", err)
}
if got := roller.hall.GetChatHistory(); len(got) != 0 {
t.Fatalf("unauthorised roll entered history: %#v", got)
}
messages := drainMessages(roller)
if len(messages) != 1 || messages[0].Kind != "error" ||
!strings.Contains(messages[0].Value.(string), "not authorised") {
t.Fatalf("unauthorised roll response = %#v", messages)
}
}
func TestClientsCannotSubmitServerGeneratedMessages(t *testing.T) {
hallName := newChalkboardTestHall(t)
roller := addTestWebClient(t, hallName, "Participant", "message")
for _, messageKind := range []string{"dice", "eightball", "coin"} {
for _, messageType := range []string{"chat", "usermessage"} {
drainMessages(roller)
if err := handleClientMessage(roller, clientMessage{
Type: messageType, Kind: messageKind, Value: "whatever result I wanted",
}); err != nil {
t.Fatalf("handleClientMessage(%s, %s): %v", messageType, messageKind, err)
}
messages := drainMessages(roller)
if len(messages) != 1 || messages[0].Kind != "error" {
t.Fatalf("reserved %s/%s response = %#v", messageType, messageKind, messages)
}
}
}
if got := roller.hall.GetChatHistory(); len(got) != 0 {
t.Fatalf("forged dice message entered history: %#v", got)
}
}
func TestChatIdentityComesFromConnection(t *testing.T) {
hallName := newChalkboardTestHall(t)
sender := addTestWebClient(t, hallName, "Participant", "message")
observer := addTestWebClient(t, hallName, "Operator", "op")
if err := handleClientMessage(sender, clientMessage{
Type: "chat", Value: "hello",
}); err != nil {
t.Fatalf("handleClientMessage(chat): %v", err)
}
messages := drainMessages(observer)
if len(messages) != 1 || messages[0].Source != sender.id ||
messages[0].Username == nil || *messages[0].Username != "Participant" {
t.Fatalf("chat did not use connection identity: %#v", messages)
}
}
+85
View File
@@ -0,0 +1,85 @@
package rtpconn
import (
"crypto/rand"
"fmt"
"io"
"math/big"
"strings"
"unicode/utf8"
"git.stormux.org/storm/skald/hall"
)
const maxEightBallQuestionRunes = 500
var eightBallResponses = []string{
"It is certain.",
"It is decidedly so.",
"Without a doubt.",
"Yes, definitely.",
"You may rely on it.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Cannot predict now.",
"Concentrate and ask again.",
"Don't count on it.",
"My reply is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful.",
}
func normalizeEightBallQuestion(value any) (string, error) {
question, ok := value.(string)
if !ok {
return "", hall.UserError("/8ball requires a question")
}
question = strings.Join(strings.Fields(question), " ")
if question == "" {
return "", hall.UserError("/8ball requires a question")
}
if utf8.RuneCountInString(question) > maxEightBallQuestionRunes {
return "", hall.UserError(fmt.Sprintf(
"Eight Ball questions may not exceed %d characters",
maxEightBallQuestionRunes,
))
}
return question, nil
}
func chooseEightBallResponse(reader io.Reader) (string, error) {
index, err := rand.Int(reader, big.NewInt(int64(len(eightBallResponses))))
if err != nil {
return "", err
}
return eightBallResponses[index.Int64()], nil
}
func handleEightBall(c *webClient, g *hall.Hall, value any) error {
if !member("message", c.Permissions()) {
return c.error(hall.UserError("not authorised"))
}
question, err := normalizeEightBallQuestion(value)
if err != nil {
return c.error(err)
}
response, err := chooseEightBallResponse(rand.Reader)
if err != nil {
return c.error(err)
}
announcement := fmt.Sprintf(
"%s asks: %s\nThe Eight Ball says: %s",
c.Username(), question, response,
)
if err := broadcastServerChat(g, "Eight Ball", "eightball", announcement); err != nil {
return c.error(err)
}
return nil
}
+120
View File
@@ -0,0 +1,120 @@
package rtpconn
import (
"bytes"
"strings"
"testing"
)
func TestEightBallHasClassicResponses(t *testing.T) {
want := []string{
"It is certain.",
"It is decidedly so.",
"Without a doubt.",
"Yes, definitely.",
"You may rely on it.",
"As I see it, yes.",
"Most likely.",
"Outlook good.",
"Yes.",
"Signs point to yes.",
"Reply hazy, try again.",
"Ask again later.",
"Better not tell you now.",
"Cannot predict now.",
"Concentrate and ask again.",
"Don't count on it.",
"My reply is no.",
"My sources say no.",
"Outlook not so good.",
"Very doubtful.",
}
if len(eightBallResponses) != len(want) {
t.Fatalf("eightBallResponses has %d entries, want %d", len(eightBallResponses), len(want))
}
for i := range want {
if eightBallResponses[i] != want[i] {
t.Fatalf("eightBallResponses[%d] = %q, want %q", i, eightBallResponses[i], want[i])
}
response, err := chooseEightBallResponse(bytes.NewReader([]byte{byte(i)}))
if err != nil {
t.Fatalf("chooseEightBallResponse(%d): %v", i, err)
}
if response != want[i] {
t.Fatalf("chooseEightBallResponse(%d) = %q, want %q", i, response, want[i])
}
}
}
func TestNormalizeEightBallQuestion(t *testing.T) {
question, err := normalizeEightBallQuestion(" Will\nwe win tonight? ")
if err != nil {
t.Fatalf("normalizeEightBallQuestion: %v", err)
}
if question != "Will we win tonight?" {
t.Fatalf("question = %q", question)
}
for _, value := range []any{" ", strings.Repeat("x", maxEightBallQuestionRunes+1), 42} {
if _, err := normalizeEightBallQuestion(value); err == nil {
t.Fatalf("normalizeEightBallQuestion(%#v) unexpectedly succeeded", value)
}
}
}
func TestEightBallIsServerGeneratedAndStoredInHistory(t *testing.T) {
hallName := newChalkboardTestHall(t)
asker := addTestWebClient(t, hallName, "Participant", "message")
observer := addTestWebClient(t, hallName, "Operator", "op")
if err := handleClientMessage(asker, clientMessage{
Type: "hallaction", Kind: "eightball", Value: "Will we win tonight?",
}); err != nil {
t.Fatalf("handleClientMessage(eightball): %v", err)
}
for name, messages := range map[string][]clientMessage{
"asker": drainMessages(asker),
"observer": drainMessages(observer),
} {
if len(messages) != 1 {
t.Fatalf("%s received %d messages, want 1: %#v", name, len(messages), messages)
}
message := messages[0]
if message.Type != "chat" || message.Kind != "eightball" || !message.Privileged {
t.Fatalf("%s received non-authoritative Eight Ball message: %#v", name, message)
}
if message.Source != "" || message.Username == nil || *message.Username != "Eight Ball" {
t.Fatalf("%s received Eight Ball message with unexpected identity: %#v", name, message)
}
text, ok := message.Value.(string)
if !ok || !strings.HasPrefix(text, "Participant asks: Will we win tonight?\nThe Eight Ball says: ") {
t.Fatalf("%s received malformed Eight Ball announcement %q", name, text)
}
}
history := asker.hall.GetChatHistory()
if len(history) != 1 || history[0].Kind != "eightball" {
t.Fatalf("Eight Ball history = %#v", history)
}
}
func TestEightBallRequiresMessagePermission(t *testing.T) {
hallName := newChalkboardTestHall(t)
asker := addTestWebClient(t, hallName, "Participant", "message")
asker.permissions = remove("message", asker.permissions)
if err := handleClientMessage(asker, clientMessage{
Type: "hallaction", Kind: "eightball", Value: "Will this work?",
}); err != nil {
t.Fatalf("handleClientMessage(eightball): %v", err)
}
if got := asker.hall.GetChatHistory(); len(got) != 0 {
t.Fatalf("unauthorised Eight Ball question entered history: %#v", got)
}
messages := drainMessages(asker)
if len(messages) != 1 || messages[0].Kind != "error" ||
!strings.Contains(messages[0].Value.(string), "not authorised") {
t.Fatalf("unauthorised Eight Ball response = %#v", messages)
}
}
+32
View File
@@ -0,0 +1,32 @@
package rtpconn
import (
"crypto/rand"
"encoding/base64"
"time"
"git.stormux.org/storm/skald/hall"
)
func isServerGeneratedChatKind(kind string) bool {
return kind == "dice" || kind == "eightball" || kind == "coin"
}
func broadcastServerChat(g *hall.Hall, name, kind, message string) error {
idBytes := make([]byte, 8)
if _, err := rand.Read(idBytes); err != nil {
return err
}
id := base64.RawURLEncoding.EncodeToString(idBytes)
now := time.Now()
g.AddToChatHistory(id, "", &name, now, kind, message)
return broadcast(g.GetClients(nil), clientMessage{
Type: "chat",
Id: id,
Username: &name,
Privileged: true,
Time: now.Format(time.RFC3339),
Kind: kind,
Value: message,
})
}
+23 -10
View File
@@ -1248,13 +1248,14 @@ func handleAction(c *webClient, a any) error {
h := g.GetChatHistory()
for _, m := range h {
err := c.write(clientMessage{
Type: "chathistory",
Id: m.Id,
Source: m.Source,
Username: m.User,
Time: m.Time.Format(time.RFC3339),
Value: m.Value,
Kind: m.Kind,
Type: "chathistory",
Id: m.Id,
Source: m.Source,
Username: m.User,
Privileged: isServerGeneratedChatKind(m.Kind),
Time: m.Time.Format(time.RFC3339),
Value: m.Value,
Kind: m.Kind,
})
if err != nil {
return err
@@ -1711,6 +1712,12 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if !member("message", c.permissions) {
return c.error(hall.UserError("not authorised"))
}
if isServerGeneratedChatKind(m.Kind) {
return c.error(hall.UserError("this message kind is server generated"))
}
source := c.Id()
username := c.Username()
id := m.Id
if m.Type == "chat" && m.Dest == "" && id == "" {
@@ -1723,7 +1730,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
if m.Type == "chat" {
if m.Dest == "" {
g.AddToChatHistory(
id, m.Source, m.Username,
id, source, &username,
now, m.Kind, m.Value,
)
}
@@ -1731,9 +1738,9 @@ func handleClientMessage(c *webClient, m clientMessage) error {
mm := clientMessage{
Type: m.Type,
Id: id,
Source: m.Source,
Source: source,
Dest: m.Dest,
Username: m.Username,
Username: &username,
Privileged: member("op", c.permissions),
Time: now.Format(time.RFC3339),
Kind: m.Kind,
@@ -1817,6 +1824,12 @@ func handleClientMessage(c *webClient, m clientMessage) error {
}
state := g.UpdateChalkboard(text)
broadcastChalkboard(g, state)
case "roll":
return handleDiceRoll(c, g, m.Value)
case "eightball":
return handleEightBall(c, g, m.Value)
case "coin":
return handleCoinFlip(c, g, m.Value)
case "lock", "unlock":
if !member("op", c.permissions) {
return c.error(hall.UserError("not authorised"))