Skip to content

Commit cc976d0

Browse files
committed
robot, spoken: forget all recent traces when target of CLEARCHAT
1 parent 1fc2c5a commit cc976d0

File tree

4 files changed

+152
-3
lines changed

4 files changed

+152
-3
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/zephyrtronium/robot
22

3-
go 1.22.0
3+
go 1.23.0
44

55
require (
66
github.com/BurntSushi/toml v1.4.0

spoken/spoken.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
_ "embed"
66
"encoding/json"
77
"fmt"
8+
"iter"
89
"time"
910

1011
"zombiezen.com/go/sqlite"
@@ -103,6 +104,43 @@ func (h *History) Trace(ctx context.Context, tag, msg string) ([]string, time.Ti
103104
return trace, time.Unix(0, tm), nil
104105
}
105106

107+
func once2[K, V any](k K, v V) iter.Seq2[K, V] {
108+
return func(yield func(K, V) bool) {
109+
yield(k, v)
110+
}
111+
}
112+
113+
// AllSince provides an iterator over all trace IDs since the given time.
114+
func (h *History) Since(ctx context.Context, tag string, tm time.Time) iter.Seq2[string, error] {
115+
conn, err := h.db.Take(ctx)
116+
defer h.db.Put(conn)
117+
if err != nil {
118+
return once2("", fmt.Errorf("couldn't get conn to find recent traces: %w", err))
119+
}
120+
const sel = `SELECT DISTINCT value FROM spoken, JSON_EACH(spoken.trace) WHERE tag = :tag AND time >= :time`
121+
st, err := conn.Prepare(sel)
122+
if err != nil {
123+
return once2("", fmt.Errorf("couldn't prepare statement to find recent traces: %w", err))
124+
}
125+
st.SetText(":tag", tag)
126+
st.SetInt64(":time", tm.UnixNano())
127+
return func(yield func(string, error) bool) {
128+
for {
129+
ok, err := st.Step()
130+
if err != nil {
131+
yield("", fmt.Errorf("couldn't get recent traces: %w", err))
132+
return
133+
}
134+
if !ok {
135+
return
136+
}
137+
if !yield(st.ColumnText(0), nil) {
138+
return
139+
}
140+
}
141+
}
142+
}
143+
106144
//go:embed schema.sql
107145
var schemaSQL string
108146

spoken/spoken_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,93 @@ func TestTrace(t *testing.T) {
190190
})
191191
}
192192
}
193+
194+
func TestSince(t *testing.T) {
195+
// Create test fixture first.
196+
ctx := context.Background()
197+
db := testDB(ctx)
198+
h, err := spoken.Open(ctx, db)
199+
if err != nil {
200+
t.Fatal(err)
201+
}
202+
insert := []struct {
203+
tag string
204+
msg string
205+
trace string
206+
time int64
207+
}{
208+
{"kessoku", "bocchi", `["1"]`, 10},
209+
{"kessoku", "ryo", `["2"]`, 20},
210+
{"sickhack", "bocchi", `["3"]`, 30},
211+
{"kessoku", "ryo", `["4"]`, 40},
212+
}
213+
{
214+
conn, err := db.Take(ctx)
215+
if err != nil {
216+
t.Fatalf("couldn't get conn: %v", err)
217+
}
218+
st, err := conn.Prepare("INSERT INTO spoken (tag, msg, trace, time, meta) VALUES (:tag, :msg, JSONB(:trace), :time, JSONB('{}'))")
219+
if err != nil {
220+
t.Fatalf("couldn't prep insert: %v", err)
221+
}
222+
for _, r := range insert {
223+
st.SetText(":tag", r.tag)
224+
st.SetText(":msg", r.msg)
225+
st.SetText(":trace", r.trace)
226+
st.SetInt64(":time", r.time)
227+
_, err := st.Step()
228+
if err != nil {
229+
t.Errorf("failed to insert %v: %v", r, err)
230+
}
231+
if err := st.Reset(); err != nil {
232+
t.Errorf("couldn't reset: %v", err)
233+
}
234+
}
235+
if err := st.Finalize(); err != nil {
236+
t.Fatalf("couldn't finalize insert: %v", err)
237+
}
238+
db.Put(conn)
239+
}
240+
241+
cases := []struct {
242+
name string
243+
tag string
244+
time int64
245+
want []string
246+
}{
247+
{
248+
name: "none",
249+
tag: "kessoku",
250+
time: 1000,
251+
want: nil,
252+
},
253+
{
254+
name: "some",
255+
tag: "kessoku",
256+
time: 15,
257+
want: []string{"2", "4"},
258+
},
259+
{
260+
name: "tagged",
261+
tag: "sickhack",
262+
time: 15,
263+
want: []string{"3"},
264+
},
265+
}
266+
for _, c := range cases {
267+
t.Run(c.name, func(t *testing.T) {
268+
var got []string
269+
for id, err := range h.Since(ctx, c.tag, time.Unix(0, c.time)) {
270+
if err != nil {
271+
t.Error(err)
272+
continue
273+
}
274+
got = append(got, id)
275+
}
276+
slices.Sort(got)
277+
if !slices.Equal(c.want, got) {
278+
t.Errorf("wrong ids: want %q, got %q", c.want, got)
279+
}
280+
})
281+
}
282+
}

tmi.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,29 @@ func (robo *Robot) clearchat(ctx context.Context, group *errgroup.Group, msg *tm
9898
}
9999
}
100100
case msg.Trailing == robo.tmi.me: // TODO(zeph): get own user id
101-
// TODO(zeph): forget all recent generated traces
102-
return
101+
work = func(ctx context.Context) {
102+
// We use the send tag because we are forgetting something we sent.
103+
tag := ch.Send
104+
slog.InfoContext(ctx, "forget recent generated", slog.String("channel", msg.To()), slog.String("tag", tag))
105+
for id, err := range robo.spoken.Since(ctx, tag, msg.Time().Add(-15*time.Minute)) {
106+
if err != nil {
107+
slog.ErrorContext(ctx, "failed to get recent traces",
108+
slog.Any("err", err),
109+
slog.String("channel", msg.To()),
110+
slog.String("tag", tag),
111+
)
112+
continue
113+
}
114+
if err := robo.brain.ForgetMessage(ctx, tag, id); err != nil {
115+
slog.ErrorContext(ctx, "failed to forget from recent trace",
116+
slog.Any("err", err),
117+
slog.String("channel", msg.To()),
118+
slog.String("tag", tag),
119+
slog.String("id", id),
120+
)
121+
}
122+
}
123+
}
103124
default:
104125
// Delete from user.
105126
// We use the user's current and previous userhash, since userhashes

0 commit comments

Comments
 (0)