diff --git a/channel/channel.go b/channel/channel.go index 919c5b9..cf8ff18 100644 --- a/channel/channel.go +++ b/channel/channel.go @@ -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 diff --git a/command/echo.go b/command/echo.go index d73e6a8..be04651 100644 --- a/command/echo.go +++ b/command/echo.go @@ -3,6 +3,8 @@ package command import ( "context" "log/slog" + + "github.com/zephyrtronium/robot/message" ) // EchoIn sends a plain text message to any channel. @@ -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"]}) } diff --git a/command/marriage.go b/command/marriage.go index 4e62f01..70cd4de 100644 --- a/command/marriage.go +++ b/command/marriage.go @@ -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{} @@ -113,7 +115,7 @@ 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)} @@ -121,7 +123,7 @@ func Marry(ctx context.Context, robo *Robot, call *Invocation) { 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) @@ -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) { @@ -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 } @@ -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}) } diff --git a/command/moderate.go b/command/moderate.go index 3bac19d..bf7054b 100644 --- a/command/moderate.go +++ b/command/moderate.go @@ -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) { @@ -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)) } } diff --git a/command/privacy.go b/command/privacy.go index 1430742..20a45b1 100644 --- a/command/privacy.go +++ b/command/privacy.go @@ -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)) } diff --git a/command/talk.go b/command/talk.go index dc53fbc..7870ba9 100644 --- a/command/talk.go +++ b/command/talk.go @@ -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 { @@ -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 @@ -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. @@ -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. @@ -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. @@ -123,7 +124,7 @@ 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. @@ -131,19 +132,18 @@ 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)) } diff --git a/config.go b/config.go index 3405a05..93b5365 100644 --- a/config.go +++ b/config.go @@ -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)