Skip to content

Commit 8ed7c0a

Browse files
committed
robot, command: add quiet time
1 parent 78c28b0 commit 8ed7c0a

File tree

6 files changed

+110
-0
lines changed

6 files changed

+110
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ These are *not* recognized as commands:
140140
- `echo bocchi` causes Robot to say `bocchi`, or whatever other message you give.
141141
- `talk about ranked competitive marriage` gives a short description of Robot's marriage system.
142142
- `forget bocchi` causes Robot to forget everything she's learned from messages containing `bocchi` in the last fifteen minutes. As a special case, `forget everything` tells her to forget all messages in the last fifteen minutes.
143+
- `be quiet for 8 hours` has Robot stop learning and speaking for eight hours; other durations like `an hour`, `1h30m`, `until tomorrow` work as well. Some commands relating to moderation and privacy will still cause her to talk. There is a twelve hour limit on quiet time.
143144

144145

145146
## Effects

channel/channel.go

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"regexp"
66
"sync"
77
"sync/atomic"
8+
"time"
89

910
"gitlab.com/zephyrtronium/pick"
1011
"golang.org/x/time/rate"
@@ -44,8 +45,15 @@ type Channel struct {
4445
Emotes *pick.Dist[string]
4546
// Effects is the distribution of effects.
4647
Effects *pick.Dist[string]
48+
// Silent is the earliest time that speaking and learning is allowed in the
49+
// channel as nanoseconds from the Unix epoch.
50+
Silent atomic.Int64
4751
// Extra is extra channel data that may be added by commands.
4852
Extra sync.Map // map[any]any; key is a type
4953
// Enabled indicates whether a channel is allowed to learn messages.
5054
Enabled atomic.Bool
5155
}
56+
57+
func (ch *Channel) SilentTime() time.Time {
58+
return time.Unix(0, ch.Silent.Load())
59+
}

command/marriage.go

+12
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ var affections = pick.New([]pick.Case[string]{
7777
// Affection describes the caller's affection MMR.
7878
// No arguments.
7979
func Affection(ctx context.Context, robo *Robot, call *Invocation) {
80+
if call.Message.Time().Before(call.Channel.SilentTime()) {
81+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
82+
return
83+
}
8084
x, c, f, l, n := score(robo.Log, &call.Channel.History, call.Message.Sender.ID)
8185
// Anything we do will require an emote.
8286
e := call.Channel.Emotes.Pick(rand.Uint32())
@@ -111,6 +115,10 @@ type partner struct {
111115
// Marry proposes to the robo.
112116
// - partnership: Type of partnership requested, e.g. "wife", "waifu", "daddy". Optional.
113117
func Marry(ctx context.Context, robo *Robot, call *Invocation) {
118+
if call.Message.Time().Before(call.Channel.SilentTime()) {
119+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
120+
return
121+
}
114122
x, _, _, _, _ := score(robo.Log, &call.Channel.History, call.Message.Sender.ID)
115123
e := call.Channel.Emotes.Pick(rand.Uint32())
116124
broadcaster := strings.EqualFold(call.Message.Sender.Name, strings.TrimPrefix(call.Channel.Name, "#")) && x == 0
@@ -168,6 +176,10 @@ func Marry(ctx context.Context, robo *Robot, call *Invocation) {
168176
// DescribeMarriage gives some exposition about the marriage system.
169177
// No args.
170178
func DescribeMarriage(ctx context.Context, robo *Robot, call *Invocation) {
179+
if t := call.Channel.SilentTime(); call.Message.Time().Before(t) {
180+
call.Channel.Message(ctx, message.Format("", "I'm being quiet for the next %v, so the marriage system is disabled until then.", t.Sub(call.Message.Time())).AsReply(call.Message.ID))
181+
return
182+
}
171183
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.`
172184
call.Channel.Message(ctx, message.Sent{Text: s})
173185
}

command/moderate.go

+69
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package command
33
import (
44
"context"
55
"log/slog"
6+
"regexp"
7+
"strconv"
68
"strings"
9+
"time"
710

811
"github.com/zephyrtronium/robot/message"
912
)
@@ -40,3 +43,69 @@ func Forget(ctx context.Context, robo *Robot, call *Invocation) {
4043
call.Channel.Message(ctx, message.Format("", "Forgot %d messages.", n).AsReply(call.Message.ID))
4144
}
4245
}
46+
47+
// Quiet makes the bot temporarily stop learning and speaking in the channel.
48+
// - dur: Duration to stop learning and speaking. Optional.
49+
// - until: Marker to stop "until tomrrow" if not empty. Optional.
50+
//
51+
// NOTE(zeph): Quiet waits for a timer which can be up to twelve hours.
52+
func Quiet(ctx context.Context, robo *Robot, call *Invocation) {
53+
var dur time.Duration
54+
switch {
55+
case call.Args["dur"] == "" && call.Args["until"] == "":
56+
dur = 2 * time.Hour
57+
case call.Args["until"] != "":
58+
// The only "until" option right now is "tomorrow".
59+
dur = 12 * time.Hour
60+
default:
61+
if m := quietA.FindStringSubmatch(call.Args["dur"]); m != nil {
62+
switch m[1][0] {
63+
case 'h', 'H':
64+
dur = time.Hour
65+
default:
66+
dur = time.Minute
67+
}
68+
break
69+
}
70+
if m := quietN.FindStringSubmatch(call.Args["dur"]); m != nil {
71+
n, err := strconv.Atoi(m[1])
72+
if err != nil {
73+
// Should be impossible.
74+
call.Channel.Message(ctx, message.Format("", `sorry? (%v)`, err).AsReply(call.Message.ID))
75+
return
76+
}
77+
switch m[2][0] {
78+
case 'h', 'H':
79+
dur = time.Hour * time.Duration(n)
80+
default:
81+
dur = time.Minute * time.Duration(n)
82+
}
83+
break
84+
}
85+
var err error
86+
dur, err = time.ParseDuration(call.Args["dur"])
87+
if err != nil {
88+
call.Channel.Message(ctx, message.Format("", `sorry? (%v)`, err).AsReply(call.Message.ID))
89+
return
90+
}
91+
}
92+
if dur > 12*time.Hour {
93+
dur = 12 * time.Hour
94+
}
95+
call.Channel.Silent.Store(call.Message.Time().Add(dur).UnixNano())
96+
robo.Log.InfoContext(ctx, "silent", slog.Duration("duration", dur), slog.Time("until", call.Channel.SilentTime()))
97+
call.Channel.Message(ctx, message.Format("", `I won't talk or learn for %v. Some commands relating to moderation and privacy will still make me talk. I'll mention when quiet time is up.`, dur).AsReply(call.Message.ID))
98+
t := time.NewTimer(dur)
99+
defer t.Stop()
100+
select {
101+
case <-ctx.Done():
102+
return
103+
case <-t.C:
104+
call.Channel.Message(ctx, message.Format("", `@%s My quiet time has ended.`, call.Message.Sender.Name))
105+
}
106+
}
107+
108+
var (
109+
quietA = regexp.MustCompile(`(?i)^an?\s+(ho?u?r|mi?n)`)
110+
quietN = regexp.MustCompile(`(?i)^(\d+)\s+(ho?u?r|mi?n)`)
111+
)

command/talk.go

+8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import (
1313
)
1414

1515
func speakCmd(ctx context.Context, robo *Robot, call *Invocation, effect string) string {
16+
if call.Message.Time().Before(call.Channel.SilentTime()) {
17+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
18+
return ""
19+
}
1620
// Don't continue prompts that look like they start with TMI commands
1721
// (even though those don't do anything anymore).
1822
if ngPrompt.MatchString(call.Args["prompt"]) {
@@ -108,6 +112,10 @@ func AAAAA(ctx context.Context, robo *Robot, call *Invocation) {
108112

109113
// Rawr says rawr.
110114
func Rawr(ctx context.Context, robo *Robot, call *Invocation) {
115+
if call.Message.Time().Before(call.Channel.SilentTime()) {
116+
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
117+
return
118+
}
111119
e := call.Channel.Emotes.Pick(rand.Uint32())
112120
if e == "" {
113121
e = ":3"

privmsg.go

+12
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ func (robo *Robot) tmiMessage(ctx context.Context, send chan<- *tmi.Message, msg
4646
return
4747
}
4848
ch.History.Add(m.Time(), m)
49+
// Check for the channel being silent. This prevents learning, copypasta,
50+
// and random speaking (among other things), which happens to be all the
51+
// rest of this function.
52+
if s := ch.SilentTime(); msg.Time().Before(s) {
53+
log.DebugContext(ctx, "channel is silent", slog.Time("until", s))
54+
return
55+
}
4956
// If the message is a reply to e.g. Bocchi, TMI adds @Bocchi to the
5057
// start of the message text.
5158
// That's helpful for commands, which we've already processed, but
@@ -364,6 +371,11 @@ var twitchMod = []twitchCommand{
364371
fn: command.Forget,
365372
name: "forget",
366373
},
374+
{
375+
parse: regexp.MustCompile(`(?i)^(?:be\s+quiet|shut\s*up|stfu)(?:\s+for\s+(?P<dur>(?:\d+[hms]){1,3}|an\s+h(?:ou)?r|\d+\s+h(?:ou)?rs?|a\s+min(?:ute)?|\d+\s+min(?:ute)?s?)|\s+until\s+(?P<until>tomorrow))?$`),
376+
fn: command.Quiet,
377+
name: "quiet",
378+
},
367379
}
368380

369381
var twitchAny = []twitchCommand{

0 commit comments

Comments
 (0)