From 78c28b02bdb751679d715aea2f230a8f19b1f76a Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Sun, 24 Nov 2024 09:44:37 -0500 Subject: [PATCH] message: make username part of the sender This allows us to eliminate a user's display name from contexts where we don't want to provide it, particularly learning. --- brain/learn_test.go | 2 -- channel/channel.go | 2 +- command/command.go | 2 +- command/marriage.go | 14 +++++++------- command/privacy.go | 4 ++-- command/talk.go | 1 - message/irc.go | 7 +++---- message/irc_test.go | 7 ++++--- message/message.go | 11 +++++++++-- privmsg.go | 19 +++++++++---------- tmi.go | 2 +- 11 files changed, 37 insertions(+), 34 deletions(-) diff --git a/brain/learn_test.go b/brain/learn_test.go index 869b4f8..20f4158 100644 --- a/brain/learn_test.go +++ b/brain/learn_test.go @@ -109,7 +109,6 @@ func TestRecall(t *testing.T) { ID: "1", To: "#bocchi", Sender: userhash.Hash{1}, - Name: "bocchi", Text: "bocchi the rock!", Timestamp: 1, }, @@ -122,7 +121,6 @@ func TestRecall(t *testing.T) { ID: "1", To: "#bocchi", Sender: userhash.Hash{1}, - Name: "bocchi", Text: "bocchi the rock!", Timestamp: 1, }, diff --git a/channel/channel.go b/channel/channel.go index cf8ff18..83425f6 100644 --- a/channel/channel.go +++ b/channel/channel.go @@ -37,7 +37,7 @@ type Channel struct { // History is a list of recent messages seen in the channel. // Note that messages which are forgotten due to moderation are not removed // from this list in general. - History History[*message.Received[string]] + History History[*message.Received[message.User]] // Memery is the meme detector for the channel. Memery *MemeDetector // Emotes is the distribution of emotes. diff --git a/command/command.go b/command/command.go index 836f275..38bd0d3 100644 --- a/command/command.go +++ b/command/command.go @@ -33,7 +33,7 @@ type Invocation struct { // Message is the message which triggered the invocation with the platform // user ID as the type argument. // It is always non-nil, but not all fields are guaranteed to be populated. - Message *message.Received[string] + Message *message.Received[message.User] // Args is the parsed arguments to the command. Args map[string]string } diff --git a/command/marriage.go b/command/marriage.go index 70cd4de..14bbc09 100644 --- a/command/marriage.go +++ b/command/marriage.go @@ -15,10 +15,10 @@ import ( "github.com/zephyrtronium/robot/message" ) -func score(log *slog.Logger, h *channel.History[*message.Received[string]], user string) (x float64, c, f, l, n int) { +func score(log *slog.Logger, h *channel.History[*message.Received[message.User]], user string) (x float64, c, f, l, n int) { mine := make(map[string]map[string]struct{}) for m := range h.All() { - who, text := m.Sender, m.Text + who, text := m.Sender.ID, m.Text n++ // Count the number of distinct other users who said each of your messages // after you said it. @@ -77,12 +77,12 @@ var affections = pick.New([]pick.Case[string]{ // Affection describes the caller's affection MMR. // No arguments. func Affection(ctx context.Context, robo *Robot, call *Invocation) { - x, c, f, l, n := score(robo.Log, &call.Channel.History, call.Message.Sender) + x, c, f, l, n := score(robo.Log, &call.Channel.History, call.Message.Sender.ID) // Anything we do will require an emote. e := call.Channel.Emotes.Pick(rand.Uint32()) if x == 0 { // Check for the broadcaster. They get special treatment. - if strings.EqualFold(call.Message.Name, strings.TrimPrefix(call.Channel.Name, "#")) { + if strings.EqualFold(call.Message.Sender.Name, strings.TrimPrefix(call.Channel.Name, "#")) { if _, ok := call.Channel.Extra.LoadOrStore(broadcasterAffectionKey{}, struct{}{}); ok { call.Channel.Message(ctx, message.Format("", "Don't make me repeat myself, it's embarrassing! %s", e).AsReply(call.Message.ID)) return @@ -111,14 +111,14 @@ type partner struct { // Marry proposes to the robo. // - partnership: Type of partnership requested, e.g. "wife", "waifu", "daddy". Optional. func Marry(ctx context.Context, robo *Robot, call *Invocation) { - x, _, _, _, _ := score(robo.Log, &call.Channel.History, call.Message.Sender) + x, _, _, _, _ := score(robo.Log, &call.Channel.History, call.Message.Sender.ID) e := call.Channel.Emotes.Pick(rand.Uint32()) - broadcaster := strings.EqualFold(call.Message.Name, strings.TrimPrefix(call.Channel.Name, "#")) && x == 0 + broadcaster := strings.EqualFold(call.Message.Sender.Name, strings.TrimPrefix(call.Channel.Name, "#")) && x == 0 if x < 10 && !broadcaster { 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)} + me := &partner{who: call.Message.Sender.ID, until: call.Message.Time().Add(time.Hour)} for { l, ok := call.Channel.Extra.LoadOrStore(partnerKey{}, me) if !ok { diff --git a/command/privacy.go b/command/privacy.go index 20a45b1..1d3d195 100644 --- a/command/privacy.go +++ b/command/privacy.go @@ -9,7 +9,7 @@ import ( ) func Private(ctx context.Context, robo *Robot, call *Invocation) { - err := robo.Privacy.Add(ctx, call.Message.Sender) + err := robo.Privacy.Add(ctx, call.Message.Sender.ID) if err != nil { robo.Log.ErrorContext(ctx, "privacy add failed", slog.Any("err", err), slog.String("channel", call.Channel.Name)) 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)) @@ -20,7 +20,7 @@ func Private(ctx context.Context, robo *Robot, call *Invocation) { } func Unprivate(ctx context.Context, robo *Robot, call *Invocation) { - err := robo.Privacy.Remove(ctx, call.Message.Sender) + err := robo.Privacy.Remove(ctx, call.Message.Sender.ID) if err != nil { robo.Log.ErrorContext(ctx, "privacy remove failed", slog.Any("err", err), slog.String("channel", call.Channel.Name)) 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)) diff --git a/command/talk.go b/command/talk.go index 7870ba9..cde02e0 100644 --- a/command/talk.go +++ b/command/talk.go @@ -18,7 +18,6 @@ func speakCmd(ctx context.Context, robo *Robot, call *Invocation, effect string) if ngPrompt.MatchString(call.Args["prompt"]) { robo.Log.WarnContext(ctx, "nasty prompt", slog.String("in", call.Channel.Name), - slog.String("from", call.Message.Name), slog.String("prompt", call.Args["prompt"]), ) e := call.Channel.Emotes.Pick(rand.Uint32()) diff --git a/message/irc.go b/message/irc.go index b50c8b1..89f0b10 100644 --- a/message/irc.go +++ b/message/irc.go @@ -7,16 +7,15 @@ import ( ) // FromTMI adapts a TMI IRC message. -func FromTMI(m *tmi.Message) *Received[string] { +func FromTMI(m *tmi.Message) *Received[User] { id, _ := m.Tag("id") sender, _ := m.Tag("user-id") ts, _ := m.Tag("tmi-sent-ts") u, _ := strconv.ParseInt(ts, 10, 64) - r := Received[string]{ + r := Received[User]{ ID: id, To: m.To(), - Sender: sender, - Name: m.DisplayName(), + Sender: User{ID: sender, Name: m.DisplayName()}, Text: m.Trailing, Timestamp: u, IsModerator: moderator(m), diff --git a/message/irc_test.go b/message/irc_test.go index 9271e19..26c993c 100644 --- a/message/irc_test.go +++ b/message/irc_test.go @@ -6,8 +6,9 @@ import ( "testing" "time" - "github.com/zephyrtronium/robot/message" "gitlab.com/zephyrtronium/tmi" + + "github.com/zephyrtronium/robot/message" ) func TestFromIRC(t *testing.T) { @@ -74,10 +75,10 @@ func TestFromIRC(t *testing.T) { if got := msg.To; got != c.to { t.Errorf("wrong to: want %q, got %q", c.to, got) } - if got := msg.Sender; got != c.sender { + if got := msg.Sender.ID; got != c.sender { t.Errorf("wrong sender: want %q, got %q", c.sender, got) } - if got := msg.Name; got != c.disp { + if got := msg.Sender.Name; got != c.disp { t.Errorf("wrong display name: want %q, got %q", c.disp, got) } if got := msg.Text; got != c.text { diff --git a/message/message.go b/message/message.go index 346bbb7..eec346c 100644 --- a/message/message.go +++ b/message/message.go @@ -17,8 +17,6 @@ type Received[U comparable] struct { // Whether it remains constant for a given sender depends on the semantics // of the type argument. Sender U - // Name is the display name of the message sender. - Name string // Text is the text of the message. Text string // Timestamp is the timestamp of the message as milliseconds since the @@ -37,6 +35,15 @@ func (m *Received[U]) Time() time.Time { return time.UnixMilli(m.Timestamp) } +// User is a user's ID and display name. +type User struct { + // ID is a user's ID. + // It must remain constant for a given user even across name changes. + ID string + // Name is a user's display name. + Name string +} + // Sent is a message to be sent to a service. type Sent struct { // Reply is a message to reply to. If empty, the message is not interpreted diff --git a/privmsg.go b/privmsg.go index 3ab911d..6622fb3 100644 --- a/privmsg.go +++ b/privmsg.go @@ -33,7 +33,7 @@ func (robo *Robot) tmiMessage(ctx context.Context, send chan<- *tmi.Message, msg log := slog.With(slog.String("trace", m.ID), slog.String("in", ch.Name)) log.InfoContext(ctx, "privmsg", slog.Duration("bias", time.Since(m.Time()))) defer log.InfoContext(ctx, "end") - if ch.Ignore[m.Sender] { + if ch.Ignore[m.Sender.ID] { log.InfoContext(ctx, "message from ignored user") return } @@ -42,7 +42,7 @@ func (robo *Robot) tmiMessage(ctx context.Context, send chan<- *tmi.Message, msg return } if cmd, ok := parseCommand(robo.tmi.name, m.Text); ok { - robo.command(ctx, log, ch, m, m.Sender, cmd) + robo.command(ctx, log, ch, m, cmd) return } ch.History.Add(m.Time(), m) @@ -56,7 +56,7 @@ func (robo *Robot) tmiMessage(ctx context.Context, send chan<- *tmi.Message, msg m.Text = t } robo.learn(ctx, log, ch, robo.hashes(), m) - switch err := ch.Memery.Check(m.Time(), m.Sender, m.Text); err { + switch err := ch.Memery.Check(m.Time(), m.Sender.ID, m.Text); err { case channel.ErrNotCopypasta: // do nothing case nil: // Meme detected. Copypasta. @@ -142,20 +142,20 @@ func (robo *Robot) tmiMessage(ctx context.Context, send chan<- *tmi.Message, msg robo.sendTMI(ctx, send, out) } -func (robo *Robot) command(ctx context.Context, log *slog.Logger, ch *channel.Channel, m *message.Received[string], from, cmd string) { +func (robo *Robot) command(ctx context.Context, log *slog.Logger, ch *channel.Channel, m *message.Received[message.User], cmd string) { robo.Metrics.TMICommandCount.Observe(1) var c *twitchCommand var args map[string]string level := "any" switch { - case from == robo.tmi.owner: + case m.Sender.ID == robo.tmi.owner: c, args = findTwitch(twitchOwner, cmd) if c != nil { level = "owner" break } fallthrough - case ch.Mod[from], m.IsModerator: + case ch.Mod[m.Sender.ID], m.IsModerator: c, args = findTwitch(twitchMod, cmd) if c != nil { level = "mod" @@ -192,12 +192,12 @@ func (robo *Robot) command(ctx context.Context, log *slog.Logger, ch *channel.Ch } // learn learns a given message's text if it passes ch's filters. -func (robo *Robot) learn(ctx context.Context, log *slog.Logger, ch *channel.Channel, hasher userhash.Hasher, msg *message.Received[string]) { +func (robo *Robot) learn(ctx context.Context, log *slog.Logger, ch *channel.Channel, hasher userhash.Hasher, msg *message.Received[message.User]) { if !ch.Enabled.Load() { log.DebugContext(ctx, "not learning in disabled channel") return } - switch err := robo.privacy.Check(ctx, msg.Sender); err { + switch err := robo.privacy.Check(ctx, msg.Sender.ID); err { case nil: // do nothing case privacy.ErrPrivate: log.DebugContext(ctx, "private sender") @@ -214,12 +214,11 @@ func (robo *Robot) learn(ctx context.Context, log *slog.Logger, ch *channel.Chan log.DebugContext(ctx, "no learn tag") return } - user := hasher.Hash(msg.Sender, msg.To, msg.Time()) + user := hasher.Hash(msg.Sender.ID, msg.To, msg.Time()) m := brain.Message{ ID: msg.ID, To: msg.To, Sender: user, - Name: msg.Name, Text: msg.Text, Timestamp: msg.Timestamp, IsModerator: msg.IsModerator, diff --git a/tmi.go b/tmi.go index 6a2fa9d..5d0d4d2 100644 --- a/tmi.go +++ b/tmi.go @@ -138,7 +138,7 @@ func (robo *Robot) clearchat(ctx context.Context, msg *tmi.Message) { default: // Delete from user. for m := range ch.History.All() { - if m.Sender != t { + if m.Sender.ID != t { continue } slog.DebugContext(ctx, "forget from user", slog.String("channel", msg.To()), slog.String("id", m.ID))