Skip to content

Commit

Permalink
robot, command: use safe message formatting facilities that have exis…
Browse files Browse the repository at this point in the history
…ted for a long time but went mostly unused
  • Loading branch information
zephyrtronium committed Nov 21, 2024
1 parent 3041b38 commit 2e88963
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 40 deletions.
4 changes: 2 additions & 2 deletions channel/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import (
"sync"
"sync/atomic"

"gitlab.com/zephyrtronium/pick"
"golang.org/x/time/rate"

"github.com/zephyrtronium/robot/message"
"gitlab.com/zephyrtronium/pick"
)

type Channel struct {
// Name is the name of the channel.
Name string
// Message sends a message to the channel with an optional reply message ID.
Message func(ctx context.Context, reply, text string)
Message func(ctx context.Context, msg message.Sent)
// Learn and Send are the channel tags.
Learn, Send string
// Block is a regex that matches messages which should not be used for
Expand Down
6 changes: 4 additions & 2 deletions command/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package command
import (
"context"
"log/slog"

"github.com/zephyrtronium/robot/message"
)

// EchoIn sends a plain text message to any channel.
Expand All @@ -15,11 +17,11 @@ func EchoIn(ctx context.Context, robo *Robot, call *Invocation) {
robo.Log.WarnContext(ctx, "echo into unknown channel", slog.String("target", t))
return
}
ch.Message(ctx, "", call.Args["msg"])
ch.Message(ctx, message.Sent{Text: call.Args["msg"]})
}

// Echo sends a plain text message to the channel in which it is invoked.
// - msg: Message to send.
func Echo(ctx context.Context, robo *Robot, call *Invocation) {
call.Channel.Message(ctx, "", call.Args["msg"])
call.Channel.Message(ctx, message.Sent{Text: call.Args["msg"]})
}
30 changes: 16 additions & 14 deletions command/marriage.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,21 @@ func Affection(ctx context.Context, robo *Robot, call *Invocation) {
// Check for the broadcaster. They get special treatment.
if strings.EqualFold(call.Message.Name, strings.TrimPrefix(call.Channel.Name, "#")) {
if _, ok := call.Channel.Extra.LoadOrStore(broadcasterAffectionKey{}, struct{}{}); ok {
call.Channel.Message(ctx, call.Message.ID, "Don't make me repeat myself, it's embarrassing! "+e)
call.Channel.Message(ctx, message.Format("", "Don't make me repeat myself, it's embarrassing! %s", e).AsReply(call.Message.ID))
return
}
const funnyMessage = `It's a bit awkward to think of you like that, streamer... But, well, it's so fun to be here, and I have you to thank for that! So I'd say a whole bunch!`
call.Channel.Message(ctx, call.Message.ID, funnyMessage+" "+e)
const funnyMessage = `It's a bit awkward to think of you like that, streamer... But, well, it's so fun to be here, and I have you to thank for that! So I'd say a whole bunch! %s`
call.Channel.Message(ctx, message.Format("", funnyMessage, e).AsReply(call.Message.ID))
return
}
// possible!
call.Channel.Message(ctx, call.Message.ID, "literally zero "+e)
call.Channel.Message(ctx, message.Format("", "literally zero %s", e).AsReply(call.Message.ID))
return
}
s := affections.Pick(rand.Uint32())
call.Channel.Message(ctx, call.Message.ID, fmt.Sprintf(s, x, e, c, f, l, n))
// The single scenario where message.Format requiring a constant formatting
// string is a drawback:
call.Channel.Message(ctx, message.Sent{Reply: call.Message.ID, Text: fmt.Sprintf(s, x, e, c, f, l, n)})
}

type partnerKey struct{}
Expand All @@ -113,15 +115,15 @@ func Marry(ctx context.Context, robo *Robot, call *Invocation) {
e := call.Channel.Emotes.Pick(rand.Uint32())
broadcaster := strings.EqualFold(call.Message.Name, strings.TrimPrefix(call.Channel.Name, "#")) && x == 0
if x < 10 && !broadcaster {
call.Channel.Message(ctx, call.Message.ID, "no "+e)
call.Channel.Message(ctx, message.Format("", "no %s", e).AsReply(call.Message.ID))
return
}
me := &partner{who: call.Message.Sender, until: call.Message.Time().Add(time.Hour)}
for {
l, ok := call.Channel.Extra.LoadOrStore(partnerKey{}, me)
if !ok {
// No competition. We're a shoo-in.
call.Channel.Message(ctx, call.Message.ID, "sure why not "+e)
call.Channel.Message(ctx, message.Format("", "sure why not %s", e).AsReply(call.Message.ID))
return
}
cur := l.(*partner)
Expand All @@ -133,19 +135,19 @@ func Marry(ctx context.Context, robo *Robot, call *Invocation) {
// but start over anyway.
continue
}
call.Channel.Message(ctx, call.Message.ID, "How could you forget we're already together? I hate you! Unsubbed, unfollowed, unloved! "+e)
call.Channel.Message(ctx, message.Format("", "How could you forget we're already together? I hate you! Unsubbed, unfollowed, unloved! %s", e).AsReply(call.Message.ID))
return
}
call.Channel.Message(ctx, call.Message.ID, "We're already together, silly! You're so funny and cute haha. "+e)
call.Channel.Message(ctx, message.Format("", "We're already together, silly! You're so funny and cute haha. %s", e).AsReply(call.Message.ID))
return
}
if call.Message.Time().Before(cur.until) {
call.Channel.Message(ctx, call.Message.ID, "My heart yet belongs to another... "+e)
call.Channel.Message(ctx, message.Format("", "My heart yet belongs to another... %s", e).AsReply(call.Message.ID))
return
}
y, _, _, _, _ := score(robo.Log, &call.Channel.History, cur.who)
if x < y && !broadcaster {
call.Channel.Message(ctx, call.Message.ID, "I'm touched, but I must decline. I'm in love with someone else. "+e)
call.Channel.Message(ctx, message.Format("", "I'm touched, but I must decline. I'm in love with someone else. %s", e).AsReply(call.Message.ID))
return
}
if !call.Channel.Extra.CompareAndSwap(partnerKey{}, cur, me) {
Expand All @@ -155,9 +157,9 @@ func Marry(ctx context.Context, robo *Robot, call *Invocation) {
// We win. Now just decide which message to send.
// TODO(zeph): since pick.Dist exists now, we could randomize
if call.Args["partnership"] != "" {
call.Channel.Message(ctx, call.Message.ID, fmt.Sprintf("Yes! I'll be your %s! %s", call.Args["partnership"], e))
call.Channel.Message(ctx, message.Format("", "Yes! I'll be your %s! %s", call.Args["partnership"], e).AsReply(call.Message.ID))
} else {
call.Channel.Message(ctx, call.Message.ID, "Yes! I'll marry you! "+e)
call.Channel.Message(ctx, message.Format("", "Yes! I'll marry you! %s", e).AsReply(call.Message.ID))
}
return
}
Expand All @@ -167,5 +169,5 @@ func Marry(ctx context.Context, robo *Robot, call *Invocation) {
// No args.
func DescribeMarriage(ctx context.Context, robo *Robot, call *Invocation) {
const s = `I am looking for a long series of short-term relationships and am holding a ranked competitive how-much-I-like-you tournament to decide my suitors! Politely ask me to marry you (or become your partner) and I'll evaluate your score. I like copypasta, memes, and long walks in the chat.`
call.Channel.Message(ctx, "", s)
call.Channel.Message(ctx, message.Sent{Text: s})
}
9 changes: 5 additions & 4 deletions command/moderate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package command

import (
"context"
"fmt"
"log/slog"
"strings"

"github.com/zephyrtronium/robot/message"
)

func Forget(ctx context.Context, robo *Robot, call *Invocation) {
Expand Down Expand Up @@ -32,10 +33,10 @@ func Forget(ctx context.Context, robo *Robot, call *Invocation) {
}
switch n {
case 0:
call.Channel.Message(ctx, call.Message.ID, fmt.Sprintf("No messages contained %q.", term))
call.Channel.Message(ctx, message.Format("", "No messages contained %q.", term).AsReply(call.Message.ID))
case 1:
call.Channel.Message(ctx, call.Message.ID, "Forgot 1 message.")
call.Channel.Message(ctx, message.Format("", "Forgot 1 message."))
default:
call.Channel.Message(ctx, call.Message.ID, fmt.Sprintf("Forgot %d messages.", n))
call.Channel.Message(ctx, message.Format("", "Forgot %d messages.", n).AsReply(call.Message.ID))
}
}
12 changes: 7 additions & 5 deletions command/privacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,33 @@ import (
"context"
"log/slog"
"math/rand/v2"

"github.com/zephyrtronium/robot/message"
)

func Private(ctx context.Context, robo *Robot, call *Invocation) {
err := robo.Privacy.Add(ctx, call.Message.Sender)
if err != nil {
robo.Log.ErrorContext(ctx, "privacy add failed", slog.Any("err", err), slog.String("channel", call.Channel.Name))
call.Channel.Message(ctx, call.Message.ID, "Something went wrong while trying to add you to the privacy list. Try again. Sorry!")
call.Channel.Message(ctx, message.Format("", "Something went wrong while trying to add you to the privacy list. Try again. Sorry!").AsReply(call.Message.ID))
return
}
e := call.Channel.Emotes.Pick(rand.Uint32())
call.Channel.Message(ctx, call.Message.ID, `Sure, I won't learn from your messages. Most of my functionality will still work for you. If you'd like to have me learn from you again, just tell me, "learn from me again." `+e)
call.Channel.Message(ctx, message.Format("", `Sure, I won't learn from your messages. Most of my functionality will still work for you. If you'd like to have me learn from you again, just tell me, "learn from me again." %s`, e).AsReply(call.Message.ID))
}

func Unprivate(ctx context.Context, robo *Robot, call *Invocation) {
err := robo.Privacy.Remove(ctx, call.Message.Sender)
if err != nil {
robo.Log.ErrorContext(ctx, "privacy remove failed", slog.Any("err", err), slog.String("channel", call.Channel.Name))
call.Channel.Message(ctx, call.Message.ID, "Something went wrong while trying to add you to the privacy list. Try again. Sorry!")
call.Channel.Message(ctx, message.Format("", "Something went wrong while trying to add you to the privacy list. Try again. Sorry!").AsReply(call.Message.ID))
return
}
e := call.Channel.Emotes.Pick(rand.Uint32())
call.Channel.Message(ctx, call.Message.ID, `Sure, I'll learn from you again! `+e)
call.Channel.Message(ctx, message.Format("", `Sure, I'll learn from you again! %s`, e).AsReply(call.Message.ID))
}

func DescribePrivacy(ctx context.Context, robo *Robot, call *Invocation) {
// TODO(zeph): describe privacy
call.Channel.Message(ctx, call.Message.ID, `See here for a description of what information I collect, and how to opt out of all collection: https://github.com/zephyrtronium/robot#what-data-does-robot-store`)
call.Channel.Message(ctx, message.Format("", `See here for a description of what information I collect, and how to opt out of all collection: https://github.com/zephyrtronium/robot#what-data-does-robot-store`).AsReply(call.Message.ID))
}
22 changes: 11 additions & 11 deletions command/talk.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package command

import (
"context"
"fmt"
"log/slog"
"math/rand/v2"
"regexp"
"strconv"
"time"

"github.com/zephyrtronium/robot/brain"
"github.com/zephyrtronium/robot/message"
)

func speakCmd(ctx context.Context, robo *Robot, call *Invocation, effect string) string {
Expand Down Expand Up @@ -61,7 +62,7 @@ func speakCmd(ctx context.Context, robo *Robot, call *Invocation, effect string)
}
// block the generated message from being later recognized as a meme.
call.Channel.Memery.Block(call.Message.Time(), s)
robo.Metrics.SpeakLatency.Observe(time.Since(start).Seconds(), call.Channel.Send, fmt.Sprintf("%t", len(call.Args["prompt"]) == 0))
robo.Metrics.SpeakLatency.Observe(time.Since(start).Seconds(), call.Channel.Send, strconv.FormatBool(len(call.Args["prompt"]) == 0))
robo.Metrics.UsedMessagesForGeneration.Observe(float64(len(trace)))
robo.Log.InfoContext(ctx, "speak", "in", call.Channel.Name, "text", m, "emote", e)
return m + " " + e
Expand All @@ -77,7 +78,7 @@ func Speak(ctx context.Context, robo *Robot, call *Invocation) {
return
}
u = lenlimit(u, 450)
call.Channel.Message(ctx, "", u)
call.Channel.Message(ctx, message.Sent{Text: u})
}

// OwO genyewates an uwu message.
Expand All @@ -88,7 +89,7 @@ func OwO(ctx context.Context, robo *Robot, call *Invocation) {
return
}
u = lenlimit(owoize(u), 450)
call.Channel.Message(ctx, "", u)
call.Channel.Message(ctx, message.Sent{Text: u})
}

// AAAAA AAAAAAAAA A AAAAAAA.
Expand All @@ -103,7 +104,7 @@ func AAAAA(ctx context.Context, robo *Robot, call *Invocation) {
return
}
u = lenlimit(aaaaaize(u), 40)
call.Channel.Message(ctx, "", u)
call.Channel.Message(ctx, message.Sent{Text: u})
}

// Rawr says rawr.
Expand All @@ -123,27 +124,26 @@ func Rawr(ctx context.Context, robo *Robot, call *Invocation) {
r.CancelAt(t)
return
}
call.Channel.Message(ctx, call.Message.ID, "rawr "+e)
call.Channel.Message(ctx, message.Format("", "rawr %s", e).AsReply(call.Message.ID))
}

// Source gives a link to the source code.
func Source(ctx context.Context, robo *Robot, call *Invocation) {
const srcMessage = `My source code is at https://github.com/zephyrtronium/robot – ` +
`I'm written in Go, and I'm free, open-source software licensed ` +
`under the GNU General Public License, Version 3.`
call.Channel.Message(ctx, call.Message.ID, srcMessage)
call.Channel.Message(ctx, message.Sent{Reply: call.Message.ID, Text: srcMessage})
}

// Who describes Robot.
func Who(ctx context.Context, robo *Robot, call *Invocation) {
const whoMessage = `I'm a Markov chain bot! I learn from things people say in chat, then spew vaguely intelligible memes back. More info at: https://github.com/zephyrtronium/robot#how-robot-works`
const whoMessage = `I'm a Markov chain bot! I learn from things people say in chat, then spew vaguely intelligible memes back. More info at: https://github.com/zephyrtronium/robot#how-robot-works %s`
e := call.Channel.Emotes.Pick(rand.Uint32())
call.Channel.Message(ctx, call.Message.ID, whoMessage+" "+e)
call.Channel.Message(ctx, message.Format("", whoMessage, e).AsReply(call.Message.ID))
}

// Contact gives information on how to contact the bot owner.
func Contact(ctx context.Context, robo *Robot, call *Invocation) {
s := fmt.Sprintf("My operator is %[1]s. %[2]s is the best way to contact %[1]s.", robo.Owner, robo.Contact)
e := call.Channel.Emotes.Pick(rand.Uint32())
call.Channel.Message(ctx, call.Message.ID, s+" "+e)
call.Channel.Message(ctx, message.Format("", "My operator is %[1]s. %[2]s is the best way to contact %[1]s. %[3]s", robo.Owner, robo.Contact, e).AsReply(call.Message.ID))
}
6 changes: 4 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,10 @@ func (robo *Robot) SetTwitchChannels(ctx context.Context, global Global, channel
Emotes: emotes,
Effects: effects,
}
v.Message = func(ctx context.Context, reply, text string) {
msg := message.Format(v.Name, "%s", text).AsReply(reply)
v.Message = func(ctx context.Context, msg message.Sent) {
if msg.To == "" {
msg.To = v.Name
}
robo.sendTMI(ctx, robo.tmi.send, msg)
}
robo.channels.Store(p, v)
Expand Down

0 comments on commit 2e88963

Please sign in to comment.