Skip to content

Commit

Permalink
robot, command: add quiet time
Browse files Browse the repository at this point in the history
  • Loading branch information
zephyrtronium committed Nov 27, 2024
1 parent 78c28b0 commit 8ed7c0a
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ These are *not* recognized as commands:
- `echo bocchi` causes Robot to say `bocchi`, or whatever other message you give.
- `talk about ranked competitive marriage` gives a short description of Robot's marriage system.
- `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.
- `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.


## Effects
Expand Down
8 changes: 8 additions & 0 deletions channel/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"regexp"
"sync"
"sync/atomic"
"time"

"gitlab.com/zephyrtronium/pick"
"golang.org/x/time/rate"
Expand Down Expand Up @@ -44,8 +45,15 @@ type Channel struct {
Emotes *pick.Dist[string]
// Effects is the distribution of effects.
Effects *pick.Dist[string]
// Silent is the earliest time that speaking and learning is allowed in the
// channel as nanoseconds from the Unix epoch.
Silent atomic.Int64
// Extra is extra channel data that may be added by commands.
Extra sync.Map // map[any]any; key is a type
// Enabled indicates whether a channel is allowed to learn messages.
Enabled atomic.Bool
}

func (ch *Channel) SilentTime() time.Time {
return time.Unix(0, ch.Silent.Load())
}
12 changes: 12 additions & 0 deletions command/marriage.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ 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) {
if call.Message.Time().Before(call.Channel.SilentTime()) {
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
return
}
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())
Expand Down Expand Up @@ -111,6 +115,10 @@ 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) {
if call.Message.Time().Before(call.Channel.SilentTime()) {
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
return
}
x, _, _, _, _ := score(robo.Log, &call.Channel.History, call.Message.Sender.ID)
e := call.Channel.Emotes.Pick(rand.Uint32())
broadcaster := strings.EqualFold(call.Message.Sender.Name, strings.TrimPrefix(call.Channel.Name, "#")) && x == 0
Expand Down Expand Up @@ -168,6 +176,10 @@ func Marry(ctx context.Context, robo *Robot, call *Invocation) {
// DescribeMarriage gives some exposition about the marriage system.
// No args.
func DescribeMarriage(ctx context.Context, robo *Robot, call *Invocation) {
if t := call.Channel.SilentTime(); call.Message.Time().Before(t) {
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))
return
}
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, message.Sent{Text: s})
}
69 changes: 69 additions & 0 deletions command/moderate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package command
import (
"context"
"log/slog"
"regexp"
"strconv"
"strings"
"time"

"github.com/zephyrtronium/robot/message"
)
Expand Down Expand Up @@ -40,3 +43,69 @@ func Forget(ctx context.Context, robo *Robot, call *Invocation) {
call.Channel.Message(ctx, message.Format("", "Forgot %d messages.", n).AsReply(call.Message.ID))
}
}

// Quiet makes the bot temporarily stop learning and speaking in the channel.
// - dur: Duration to stop learning and speaking. Optional.
// - until: Marker to stop "until tomrrow" if not empty. Optional.
//
// NOTE(zeph): Quiet waits for a timer which can be up to twelve hours.
func Quiet(ctx context.Context, robo *Robot, call *Invocation) {
var dur time.Duration
switch {
case call.Args["dur"] == "" && call.Args["until"] == "":
dur = 2 * time.Hour
case call.Args["until"] != "":
// The only "until" option right now is "tomorrow".
dur = 12 * time.Hour
default:
if m := quietA.FindStringSubmatch(call.Args["dur"]); m != nil {
switch m[1][0] {
case 'h', 'H':
dur = time.Hour
default:
dur = time.Minute
}
break
}
if m := quietN.FindStringSubmatch(call.Args["dur"]); m != nil {
n, err := strconv.Atoi(m[1])
if err != nil {
// Should be impossible.
call.Channel.Message(ctx, message.Format("", `sorry? (%v)`, err).AsReply(call.Message.ID))
return
}
switch m[2][0] {
case 'h', 'H':
dur = time.Hour * time.Duration(n)
default:
dur = time.Minute * time.Duration(n)
}
break
}
var err error
dur, err = time.ParseDuration(call.Args["dur"])
if err != nil {
call.Channel.Message(ctx, message.Format("", `sorry? (%v)`, err).AsReply(call.Message.ID))
return
}
}
if dur > 12*time.Hour {
dur = 12 * time.Hour
}
call.Channel.Silent.Store(call.Message.Time().Add(dur).UnixNano())
robo.Log.InfoContext(ctx, "silent", slog.Duration("duration", dur), slog.Time("until", call.Channel.SilentTime()))
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))
t := time.NewTimer(dur)
defer t.Stop()
select {
case <-ctx.Done():
return
case <-t.C:
call.Channel.Message(ctx, message.Format("", `@%s My quiet time has ended.`, call.Message.Sender.Name))
}
}

var (
quietA = regexp.MustCompile(`(?i)^an?\s+(ho?u?r|mi?n)`)
quietN = regexp.MustCompile(`(?i)^(\d+)\s+(ho?u?r|mi?n)`)
)
8 changes: 8 additions & 0 deletions command/talk.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
)

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

// Rawr says rawr.
func Rawr(ctx context.Context, robo *Robot, call *Invocation) {
if call.Message.Time().Before(call.Channel.SilentTime()) {
robo.Log.InfoContext(ctx, "silent", slog.Time("until", call.Channel.SilentTime()))
return
}
e := call.Channel.Emotes.Pick(rand.Uint32())
if e == "" {
e = ":3"
Expand Down
12 changes: 12 additions & 0 deletions privmsg.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ func (robo *Robot) tmiMessage(ctx context.Context, send chan<- *tmi.Message, msg
return
}
ch.History.Add(m.Time(), m)
// Check for the channel being silent. This prevents learning, copypasta,
// and random speaking (among other things), which happens to be all the
// rest of this function.
if s := ch.SilentTime(); msg.Time().Before(s) {
log.DebugContext(ctx, "channel is silent", slog.Time("until", s))
return
}
// If the message is a reply to e.g. Bocchi, TMI adds @Bocchi to the
// start of the message text.
// That's helpful for commands, which we've already processed, but
Expand Down Expand Up @@ -364,6 +371,11 @@ var twitchMod = []twitchCommand{
fn: command.Forget,
name: "forget",
},
{
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))?$`),
fn: command.Quiet,
name: "quiet",
},
}

var twitchAny = []twitchCommand{
Expand Down

0 comments on commit 8ed7c0a

Please sign in to comment.