From dd538e3cbba5d20d9b19fc4690acf97e19092696 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Wed, 20 Nov 2024 21:25:19 -0500 Subject: [PATCH] brain: remove ForgetDuring and ForgetUserSince methods We keep the information needed to implement these in terms of only message IDs elsewhere. Fixes #52. For #90. --- brain/braintest/braintest.go | 54 +-- brain/braintest/braintest_test.go | 44 +- brain/kvbrain/forget.go | 91 +--- brain/kvbrain/forget_test.go | 322 +------------ brain/learn.go | 11 +- brain/learn_test.go | 17 +- brain/sqlbrain/forget.go | 97 +--- brain/sqlbrain/forget_test.go | 761 +----------------------------- command/moderate.go | 3 +- tmi.go | 44 +- 10 files changed, 51 insertions(+), 1393 deletions(-) diff --git a/brain/braintest/braintest.go b/brain/braintest/braintest.go index 688c2e9..47d0208 100644 --- a/brain/braintest/braintest.go +++ b/brain/braintest/braintest.go @@ -18,8 +18,7 @@ import ( // If a brain cannot be created without error, new should call t.Fatal. func Test(ctx context.Context, t *testing.T, new func(context.Context) brain.Brain) { t.Run("speak", testSpeak(ctx, new(ctx))) - t.Run("forgetMessage", testForgetMessage(ctx, new(ctx))) - t.Run("forgetDuring", testForgetDuring(ctx, new(ctx))) + t.Run("forgetMessage", testForget(ctx, new(ctx))) t.Run("combinatoric", testCombinatoric(ctx, new(ctx))) } @@ -182,11 +181,11 @@ func testSpeak(ctx context.Context, br brain.Brain) func(t *testing.T) { } } -// testForgetMessage tests that a brain can forget messages by ID. -func testForgetMessage(ctx context.Context, br brain.Brain) func(t *testing.T) { +// testForget tests that a brain can forget messages by ID. +func testForget(ctx context.Context, br brain.Brain) func(t *testing.T) { return func(t *testing.T) { learn(ctx, t, br) - if err := br.ForgetMessage(ctx, "kessoku", messages[0].ID); err != nil { + if err := br.Forget(ctx, "kessoku", messages[0].ID); err != nil { t.Errorf("failed to forget first message: %v", err) } got := speak(ctx, t, br, "kessoku", "", 2048) @@ -233,51 +232,6 @@ func testForgetMessage(ctx context.Context, br brain.Brain) func(t *testing.T) { } } -// testForgetDuring tests that a brain can forget messages in a time range. -func testForgetDuring(ctx context.Context, br brain.Brain) func(t *testing.T) { - return func(t *testing.T) { - learn(ctx, t, br) - if err := br.ForgetDuring(ctx, "kessoku", time.Unix(1, 0).Add(-time.Millisecond), time.Unix(2, 0).Add(time.Millisecond)); err != nil { - t.Errorf("failed to forget: %v", err) - } - got := speak(ctx, t, br, "kessoku", "", 2048) - want := map[string]struct{}{ - "1#member bocchi": {}, - "1 4#member bocchi": {}, - "1 4#member kita": {}, - "4#member kita": {}, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("wrong messages after forgetting (+got/-want):\n%s", diff) - } - got = speak(ctx, t, br, "sickhack", "", 2048) - want = map[string]struct{}{ - "5#member bocchi": {}, - "5 6#member bocchi": {}, - "5 7#member bocchi": {}, - "5 8#member bocchi": {}, - "5 6#member ryou": {}, - "6#member ryou": {}, - "6 7#member ryou": {}, - "6 8#member ryou": {}, - "5 7#member nijika": {}, - "6 7#member nijika": {}, - "7#member nijika": {}, - "7 8#member nijika": {}, - "5 8#member kita": {}, - "6 8#member kita": {}, - "7 8#member kita": {}, - "8#member kita": {}, - "9#manager seika": {}, - } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("wrong spoken messages for sickhack (+got/-want):\n%s", diff) - } - } -} - -// TODO(zeph): testForgetUser - // testCombinatoric tests that chains can generate even with substantial // overlap in learned material. func testCombinatoric(ctx context.Context, br brain.Brain) func(t *testing.T) { diff --git a/brain/braintest/braintest_test.go b/brain/braintest/braintest_test.go index d13fe09..f6d88bd 100644 --- a/brain/braintest/braintest_test.go +++ b/brain/braintest/braintest_test.go @@ -66,55 +66,13 @@ func (m *membrain) forgetIDLocked(tag, id string) { } } -func (m *membrain) Forget(ctx context.Context, tag string, tuples []brain.Tuple) error { - m.mu.Lock() - defer m.mu.Unlock() - for _, tup := range tuples { - p := strings.Join(tup.Prefix, "\xff") - u := m.tups[tag][p] - k := slices.IndexFunc(u, func(v [2]string) bool { return v[1] == tup.Suffix }) - if k < 0 { - continue - } - u[k], u[len(u)-1] = u[len(u)-1], u[k] - m.tups[tag][p] = u[:len(u)-1] - } - return nil -} - -func (m *membrain) ForgetMessage(ctx context.Context, tag, id string) error { +func (m *membrain) Forget(ctx context.Context, tag, id string) error { m.mu.Lock() defer m.mu.Unlock() m.forgetIDLocked(tag, id) return nil } -func (m *membrain) ForgetDuring(ctx context.Context, tag string, since, before time.Time) error { - m.mu.Lock() - defer m.mu.Unlock() - s, b := since.UnixNano(), before.UnixNano() - for tm, u := range m.tms[tag] { - if tm < s || tm > b { - continue - } - for _, v := range u { - m.forgetIDLocked(tag, v) - } - delete(m.tms[tag], tm) // yea i modify the map during iteration, yea i'm cool - } - return nil -} - -func (m *membrain) ForgetUser(ctx context.Context, user *userhash.Hash) error { - m.mu.Lock() - defer m.mu.Unlock() - for _, v := range m.users[*user] { - m.forgetIDLocked(v[0], v[1]) - } - delete(m.users, *user) - return nil -} - func (m *membrain) Speak(ctx context.Context, tag string, prompt []string, w *brain.Builder) error { m.mu.Lock() defer m.mu.Unlock() diff --git a/brain/kvbrain/forget.go b/brain/kvbrain/forget.go index 1300036..8312735 100644 --- a/brain/kvbrain/forget.go +++ b/brain/kvbrain/forget.go @@ -6,7 +6,6 @@ import ( "fmt" "slices" "sync" - "time" "github.com/zephyrtronium/robot/userhash" ) @@ -53,45 +52,9 @@ func (p *past) findID(id string) [][]byte { return nil } -// findDuring finds all knowledge keys of messages recorded with timestamps in -// the given time span. -func (p *past) findDuring(since, before int64) [][]byte { - r := make([][]byte, 0, 64) - p.mu.Lock() - defer p.mu.Unlock() - for k, v := range p.time { - if since <= v && v <= before { - keys := p.key[k] - r = slices.Grow(r, len(keys)) - for _, v := range keys { - r = append(r, bytes.Clone(v)) - } - } - } - return r -} - -// findUser finds all knowledge keys of messages recorded from a given user -// since a timestamp. -func (p *past) findUser(user userhash.Hash) [][]byte { - r := make([][]byte, 0, 64) - p.mu.Lock() - defer p.mu.Unlock() - for k, v := range p.user { - if v == user { - keys := p.key[k] - r = slices.Grow(r, len(keys)) - for _, v := range keys { - r = append(r, bytes.Clone(v)) - } - } - } - return r -} - -// ForgetMessage forgets everything learned from a single given message. +// Forget forgets everything learned from a single given message. // If nothing has been learned from the message, it should be ignored. -func (br *Brain) ForgetMessage(ctx context.Context, tag, id string) error { +func (br *Brain) Forget(ctx context.Context, tag, id string) error { past, _ := br.past.Load(tag) if past == nil { return nil @@ -111,53 +74,3 @@ func (br *Brain) ForgetMessage(ctx context.Context, tag, id string) error { } return nil } - -// ForgetDuring forgets all messages learned in the given time span. -func (br *Brain) ForgetDuring(ctx context.Context, tag string, since, before time.Time) error { - past, _ := br.past.Load(tag) - if past == nil { - return nil - } - keys := past.findDuring(since.UnixNano(), before.UnixNano()) - batch := br.knowledge.NewWriteBatch() - defer batch.Cancel() - for _, key := range keys { - err := batch.Delete(key) - if err != nil { - return err - } - } - err := batch.Flush() - if err != nil { - return fmt.Errorf("couldn't commit deleting between times %v and %v: %w", since, before, err) - } - return nil -} - -// ForgetUser forgets all messages associated with a userhash. -func (br *Brain) ForgetUser(ctx context.Context, user *userhash.Hash) error { - var rangeErr error - u := *user - br.past.Range(func(tag string, past *past) bool { - keys := past.findUser(u) - if len(keys) == 0 { - return true - } - batch := br.knowledge.NewWriteBatch() - defer batch.Cancel() - for _, key := range keys { - err := batch.Delete(key) - if err != nil { - rangeErr = err - return false - } - } - err := batch.Flush() - if err != nil { - rangeErr = fmt.Errorf("couldn't commit deleting messages by user: %w", err) - return false - } - return false - }) - return rangeErr -} diff --git a/brain/kvbrain/forget_test.go b/brain/kvbrain/forget_test.go index 25aef53..a43b851 100644 --- a/brain/kvbrain/forget_test.go +++ b/brain/kvbrain/forget_test.go @@ -67,7 +67,7 @@ func TestPastRecord(t *testing.T) { } } -func TestPastFindID(t *testing.T) { +func TestPastFind(t *testing.T) { uu := "1" p := past{ k: 127, @@ -84,82 +84,6 @@ func TestPastFindID(t *testing.T) { } } -func TestPastFindDuring(t *testing.T) { - p := past{ - k: 127, - key: [256][][]byte{ - 0: {[]byte("bocchi")}, - 1: {[]byte("ryou")}, - 2: {[]byte("nijika")}, - 3: {[]byte("kita")}, - }, - id: [256]string{ - 0: "2", - 1: "3", - 2: "4", - 3: "1", - }, - user: [256]userhash.Hash{ - 0: {3}, - 1: {4}, - 2: {5}, - 3: {2}, - }, - time: [256]int64{ - 0: 4, - 1: 5, - 2: 6, - 3: 3, - }, - } - want := [][]byte{ - []byte("bocchi"), - []byte("ryou"), - } - got := p.findDuring(4, 5) - if !slices.EqualFunc(got, want, bytes.Equal) { - t.Errorf("wrong result: want %q, got %q", want, got) - } -} - -func TestPastFindUser(t *testing.T) { - p := past{ - k: 127, - key: [256][][]byte{ - 127: {[]byte("bocchi")}, - 192: {[]byte("ryou")}, - 200: {[]byte("nijika")}, - 255: {[]byte("kita")}, - }, - id: [256]string{ - 127: "2", - 192: "3", - 200: "4", - 255: "1", - }, - user: [256]userhash.Hash{ - 127: {8}, - 192: {8}, - 200: {5}, - 255: {5}, - }, - time: [256]int64{ - 127: 4, - 192: 5, - 200: 6, - 255: 3, - }, - } - want := [][]byte{ - []byte("bocchi"), - []byte("ryou"), - } - got := p.findUser(userhash.Hash{8}) - if !slices.EqualFunc(got, want, bytes.Equal) { - t.Errorf("wrong result: want %q, got %q", want, got) - } -} - func BenchmarkPastRecord(b *testing.B) { var p past uu := "1" @@ -170,7 +94,7 @@ func BenchmarkPastRecord(b *testing.B) { } } -func BenchmarkPastFindID(b *testing.B) { +func BenchmarkPastFind(b *testing.B) { var p past for i := range len(p.id) { p.record(string(byte(i)), userhash.Hash{byte(i)}, int64(i), [][]byte{{byte(i)}}) @@ -182,34 +106,10 @@ func BenchmarkPastFindID(b *testing.B) { } } -func BenchmarkPastFindDuring(b *testing.B) { - var p past - for i := range len(p.id) { - p.record(string(byte(i)), userhash.Hash{byte(i)}, int64(i), [][]byte{{byte(i)}}) - } - b.ReportAllocs() - b.ResetTimer() - for i := range b.N { - use(p.findDuring(int64(i), int64(i))) - } -} - -func BenchmarkPastFindUser(b *testing.B) { - var p past - for i := range len(p.id) { - p.record(string(byte(i)), userhash.Hash{byte(i)}, int64(i), [][]byte{{byte(i)}}) - } - b.ReportAllocs() - b.ResetTimer() - for i := range b.N { - use(p.findUser(userhash.Hash{byte(i)})) - } -} - //go:noinline func use(x [][]byte) {} -func TestForgetMessage(t *testing.T) { +func TestForget(t *testing.T) { type message struct { id string user userhash.Hash @@ -308,224 +208,10 @@ func TestForgetMessage(t *testing.T) { t.Errorf("failed to learn: %v", err) } } - if err := br.ForgetMessage(ctx, "kessoku", c.uu); err != nil { + if err := br.Forget(ctx, "kessoku", c.uu); err != nil { t.Errorf("couldn't forget: %v", err) } dbcheck(t, db, c.want) }) } } - -func TestForgetDuring(t *testing.T) { - type message struct { - id string - user userhash.Hash - tag string - time time.Time - tups []brain.Tuple - } - cases := []struct { - name string - msgs []message - a, b int64 - want map[string]string - }{ - { - name: "single", - msgs: []message{ - { - id: "1", - user: userhash.Hash{2}, - tag: "kessoku", - time: time.Unix(1, 0), - tups: []brain.Tuple{ - { - Prefix: []string{"ryou", "bocchi"}, - Suffix: "kita", - }, - }, - }, - }, - a: 0, - b: 2, - want: map[string]string{}, - }, - { - name: "several", - msgs: []message{ - { - id: "1", - user: userhash.Hash{2}, - tag: "kessoku", - time: time.Unix(1, 0), - tups: []brain.Tuple{ - { - Prefix: []string{"ryou", "bocchi"}, - Suffix: "kita", - }, - }, - }, - { - id: "2", - user: userhash.Hash{2}, - tag: "kessoku", - time: time.Unix(1, 0), - tups: []brain.Tuple{ - { - Prefix: []string{"ryou", "bocchi"}, - Suffix: "kita", - }, - }, - }, - }, - a: 0, - b: 2, - want: map[string]string{}, - }, - { - name: "none", - msgs: []message{ - { - id: "1", - user: userhash.Hash{2}, - tag: "kessoku", - time: time.Unix(5, 0), - tups: []brain.Tuple{ - { - Prefix: []string{"ryou", "bocchi"}, - Suffix: "kita", - }, - }, - }, - }, - a: 0, - b: 2, - want: map[string]string{ - mkey("kessoku", "ryou\xffbocchi\xff\xff", "1"): "kita", - }, - }, - { - name: "tagged", - msgs: []message{ - { - id: "1", - user: userhash.Hash{2}, - tag: "sickhack", - time: time.Unix(1, 0), - tups: []brain.Tuple{ - { - Prefix: []string{"ryou", "bocchi"}, - Suffix: "kita", - }, - }, - }, - }, - a: 0, - b: 2, - want: map[string]string{ - mkey("sickhack", "ryou\xffbocchi\xff\xff", "1"): "kita", - }, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - ctx := context.Background() - db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true).WithLogger(nil)) - if err != nil { - t.Fatal(err) - } - br := New(db) - for _, msg := range c.msgs { - err := br.Learn(ctx, msg.tag, msg.id, msg.user, msg.time, msg.tups) - if err != nil { - t.Errorf("failed to learn: %v", err) - } - } - since := time.Unix(c.a, 0) - before := time.Unix(c.b, 0) - if err := br.ForgetDuring(ctx, "kessoku", since, before); err != nil { - t.Errorf("failed to forget between %v and %v: %v", since, before, err) - } - dbcheck(t, db, c.want) - }) - } -} - -func TestForgetUserSince(t *testing.T) { - type message struct { - id string - user userhash.Hash - tag string - time time.Time - tups []brain.Tuple - } - cases := []struct { - name string - msgs []message - user userhash.Hash - want map[string]string - }{ - { - name: "match", - msgs: []message{ - { - id: "1", - user: userhash.Hash{2}, - tag: "kessoku", - time: time.Unix(1, 0), - tups: []brain.Tuple{ - { - Prefix: []string{"ryou", "bocchi"}, - Suffix: "kita", - }, - }, - }, - }, - user: userhash.Hash{2}, - want: map[string]string{}, - }, - { - name: "different", - msgs: []message{ - { - id: "1", - user: userhash.Hash{2}, - tag: "kessoku", - time: time.Unix(1, 0), - tups: []brain.Tuple{ - { - Prefix: []string{"ryou", "bocchi"}, - Suffix: "kita", - }, - }, - }, - }, - user: userhash.Hash{1}, - want: map[string]string{ - mkey("kessoku", "ryou\xffbocchi\xff\xff", "1"): "kita", - }, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - ctx := context.Background() - db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true).WithLogger(nil)) - if err != nil { - t.Fatal(err) - } - br := New(db) - for _, msg := range c.msgs { - err := br.Learn(ctx, msg.tag, msg.id, msg.user, msg.time, msg.tups) - if err != nil { - t.Errorf("failed to learn: %v", err) - } - } - if err := br.ForgetUser(ctx, &c.user); err != nil { - t.Errorf("failed to forget from user %02x: %v", c.user, err) - } - dbcheck(t, db, c.want) - }) - } -} diff --git a/brain/learn.go b/brain/learn.go index fefb855..54befc3 100644 --- a/brain/learn.go +++ b/brain/learn.go @@ -27,13 +27,10 @@ type Learner interface { // Each tuple's prefix has entropy reduction transformations applied. // Tuples in the argument may share storage for prefixes. Learn(ctx context.Context, tag, id string, user userhash.Hash, t time.Time, tuples []Tuple) error - // ForgetMessage forgets everything learned from a single given message. - // If nothing has been learned from the message, it should be ignored. - ForgetMessage(ctx context.Context, tag, id string) error - // ForgetDuring forgets all messages learned in the given time span. - ForgetDuring(ctx context.Context, tag string, since, before time.Time) error - // ForgetUser forgets all messages associated with a userhash. - ForgetUser(ctx context.Context, user *userhash.Hash) error + // Forget forgets everything learned from a single given message. + // If nothing has been learned from the message, it should prevent anything + // from being learned from a message with that ID. + Forget(ctx context.Context, tag, id string) error } var tuplesPool tpool.Pool[[]Tuple] diff --git a/brain/learn_test.go b/brain/learn_test.go index c839ff1..13d8722 100644 --- a/brain/learn_test.go +++ b/brain/learn_test.go @@ -13,29 +13,14 @@ import ( type testLearner struct { learned []brain.Tuple - forgot []brain.Tuple - err error } func (t *testLearner) Learn(ctx context.Context, tag, id string, user userhash.Hash, tm time.Time, tuples []brain.Tuple) error { t.learned = append(t.learned, tuples...) - return t.err -} - -func (t *testLearner) Forget(ctx context.Context, tag string, tuples []brain.Tuple) error { - t.forgot = tuples - return nil -} - -func (t *testLearner) ForgetMessage(ctx context.Context, tag, id string) error { - return nil -} - -func (t *testLearner) ForgetDuring(ctx context.Context, tag string, since, before time.Time) error { return nil } -func (t *testLearner) ForgetUser(ctx context.Context, user *userhash.Hash) error { +func (t *testLearner) Forget(ctx context.Context, tag, id string) error { return nil } diff --git a/brain/sqlbrain/forget.go b/brain/sqlbrain/forget.go index 2accd95..ad080c6 100644 --- a/brain/sqlbrain/forget.go +++ b/brain/sqlbrain/forget.go @@ -4,17 +4,15 @@ import ( "context" _ "embed" "fmt" - "time" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" - - "github.com/zephyrtronium/robot/userhash" ) -// ForgetMessage forgets everything learned from a single given message. -// If nothing has been learned from the message, nothing happens. -func (br *Brain) ForgetMessage(ctx context.Context, tag, id string) (err error) { +// Forget forgets everything learned from a single given message. +// If nothing has been learned from the message, a message with that ID cannot +// be learned in the future. +func (br *Brain) Forget(ctx context.Context, tag, id string) (err error) { conn, err := br.db.Take(ctx) defer br.db.Put(conn) if err != nil { @@ -53,93 +51,6 @@ func (br *Brain) ForgetMessage(ctx context.Context, tag, id string) (err error) return nil } -// ForgetDuring forgets all messages learned in the given time span. -func (br *Brain) ForgetDuring(ctx context.Context, tag string, since time.Time, before time.Time) error { - conn, err := br.db.Take(ctx) - defer br.db.Put(conn) - if err != nil { - return fmt.Errorf("couldn't get connection to forget time span: %w", err) - } - defer sqlitex.Transaction(conn)(&err) - // Forget messages by time and get their IDs. - const forgetTime = `UPDATE messages SET deleted = 'TIME' WHERE tag=:tag AND time BETWEEN :since AND :before RETURNING id` - sm, err := conn.Prepare(forgetTime) - if err != nil { - return fmt.Errorf("couldn't prepare delete for messages in time span: %w", err) - } - sm.SetText(":tag", tag) - sm.SetInt64(":since", since.UnixNano()) - sm.SetInt64(":before", before.UnixNano()) - const forgetTuple = `UPDATE knowledge SET deleted = 'TIME' WHERE tag=:tag AND id=:id` - st, err := conn.Prepare(forgetTuple) - if err != nil { - return fmt.Errorf("couldn't prepare delete for tuples in time span: %w", err) - } - st.SetText(":tag", tag) - // Now forget tuples by the IDs. - for { - ok, err := sm.Step() - if err != nil { - return fmt.Errorf("couldn't step delete for messages in time span: %w", err) - } - if !ok { - break - } - id := sm.GetText("id") - st.SetText(":id", id) - if err := allsteps(st); err != nil { - return fmt.Errorf("couldn't step delete for tuples in time span: %w", err) - } - if err := st.Reset(); err != nil { - return fmt.Errorf("couldn't reset delete for tuples in time span: %w", err) - } - } - return nil -} - -// ForgetUser forgets all messages associated with a userhash. -func (br *Brain) ForgetUser(ctx context.Context, user *userhash.Hash) error { - conn, err := br.db.Take(ctx) - defer br.db.Put(conn) - if err != nil { - return fmt.Errorf("couldn't get connection to forget from user: %w", err) - } - defer sqlitex.Transaction(conn)(&err) - // Forget messages by user and get their IDs. - const forgetUser = `UPDATE messages SET deleted = 'CLEARCHAT' WHERE user = :user RETURNING tag, id` - sm, err := conn.Prepare(forgetUser) - if err != nil { - return fmt.Errorf("couldn't prepare delete for messages from user: %w", err) - } - sm.SetBytes(":user", user[:]) - const forgetTuple = `UPDATE knowledge SET deleted = 'CLEARCHAT' WHERE tag=:tag AND id=:id` - st, err := conn.Prepare(forgetTuple) - if err != nil { - return fmt.Errorf("couldn't prepare delete for tuples from user: %w", err) - } - // Now forget by the IDs. - for { - ok, err := sm.Step() - if err != nil { - return fmt.Errorf("couldn't step delete for messages from user: %w", err) - } - if !ok { - break - } - tag := sm.GetText("tag") - id := sm.GetText("id") - st.SetText(":tag", tag) - st.SetText(":id", id) - if err := allsteps(st); err != nil { - return fmt.Errorf("couldn't step delete for tuples from user: %w", err) - } - if err := st.Reset(); err != nil { - return fmt.Errorf("couldn't reset delete for tuples from user: %w", err) - } - } - return nil -} - func allsteps(st *sqlite.Stmt) error { for { ok, err := st.Step() diff --git a/brain/sqlbrain/forget_test.go b/brain/sqlbrain/forget_test.go index f86383a..e9e326b 100644 --- a/brain/sqlbrain/forget_test.go +++ b/brain/sqlbrain/forget_test.go @@ -11,7 +11,7 @@ import ( "github.com/zephyrtronium/robot/userhash" ) -func TestForgetMessage(t *testing.T) { +func TestForget(t *testing.T) { learn := []learn{ { tag: "kessoku", @@ -417,767 +417,10 @@ func TestForgetMessage(t *testing.T) { if t.Failed() { t.Fatal("setup failed") } - if err := br.ForgetMessage(ctx, c.tag, c.id); err != nil { + if err := br.Forget(ctx, c.tag, c.id); err != nil { t.Errorf("failed to delete %v/%v: %v", c.tag, c.id, err) } contents(t, conn, c.know, c.msgs) }) } } - -func TestForgetDuring(t *testing.T) { - learn := []learn{ - { - tag: "kessoku", - user: userhash.Hash{1}, - id: "2", - t: 3, - tups: []brain.Tuple{ - {Prefix: strings.Fields("kita nijika ryo bocchi"), Suffix: ""}, - {Prefix: strings.Fields("nijika ryo bocchi"), Suffix: "kita"}, - {Prefix: strings.Fields("ryo bocchi"), Suffix: "nijika"}, - {Prefix: strings.Fields("bocchi"), Suffix: "ryo"}, - {Prefix: nil, Suffix: "bocchi"}, - }, - }, - { - tag: "kessoku", - user: userhash.Hash{4}, - id: "5", - t: 6, - tups: []brain.Tuple{ - {Prefix: []string{"bocchi"}, Suffix: ""}, - {Prefix: nil, Suffix: "bocchi"}, - }, - }, - { - tag: "sickhack", - user: userhash.Hash{1}, - id: "2", - t: 3, - tups: []brain.Tuple{ - {Prefix: []string{"kikuri"}, Suffix: ""}, - {Prefix: nil, Suffix: "kikuri"}, - }, - }, - } - initKnow := []know{ - { - tag: "kessoku", - id: "2", - prefix: "kita\x00nijika\x00ryo\x00bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "2", - prefix: "nijika\x00ryo\x00bocchi\x00\x00", - suffix: "kita", - }, - { - tag: "kessoku", - id: "2", - prefix: "ryo\x00bocchi\x00\x00", - suffix: "nijika", - }, - { - tag: "kessoku", - id: "2", - prefix: "bocchi\x00\x00", - suffix: "ryo", - }, - { - tag: "kessoku", - id: "2", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "kessoku", - id: "5", - prefix: "bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "5", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "sickhack", - id: "2", - prefix: "kikuri\x00\x00", - suffix: "", - }, - { - tag: "sickhack", - id: "2", - prefix: "\x00", - suffix: "kikuri", - }, - } - initMsgs := []msg{ - { - tag: "kessoku", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - { - tag: "kessoku", - id: "5", - time: 6, - user: userhash.Hash{4}, - }, - { - tag: "sickhack", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - } - cases := []struct { - name string - tag string - since int64 - before int64 - know []know - msgs []msg - }{ - { - name: "none", - tag: "kessoku", - since: 100, - before: 200, - know: initKnow, - msgs: initMsgs, - }, - { - name: "early", - tag: "kessoku", - since: 1, - before: 4, - know: []know{ - { - tag: "kessoku", - id: "2", - prefix: "kita\x00nijika\x00ryo\x00bocchi\x00\x00", - suffix: "", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "nijika\x00ryo\x00bocchi\x00\x00", - suffix: "kita", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "ryo\x00bocchi\x00\x00", - suffix: "nijika", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "bocchi\x00\x00", - suffix: "ryo", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "\x00", - suffix: "bocchi", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "5", - prefix: "bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "5", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "sickhack", - id: "2", - prefix: "kikuri\x00\x00", - suffix: "", - }, - { - tag: "sickhack", - id: "2", - prefix: "\x00", - suffix: "kikuri", - }, - }, - msgs: []msg{ - { - tag: "kessoku", - id: "2", - time: 3, - user: userhash.Hash{1}, - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "5", - time: 6, - user: userhash.Hash{4}, - }, - { - tag: "sickhack", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - }, - }, - { - name: "late", - tag: "kessoku", - since: 5, - before: 8, - know: []know{ - { - tag: "kessoku", - id: "2", - prefix: "kita\x00nijika\x00ryo\x00bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "2", - prefix: "nijika\x00ryo\x00bocchi\x00\x00", - suffix: "kita", - }, - { - tag: "kessoku", - id: "2", - prefix: "ryo\x00bocchi\x00\x00", - suffix: "nijika", - }, - { - tag: "kessoku", - id: "2", - prefix: "bocchi\x00\x00", - suffix: "ryo", - }, - { - tag: "kessoku", - id: "2", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "kessoku", - id: "5", - prefix: "bocchi\x00\x00", - suffix: "", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "5", - prefix: "\x00", - suffix: "bocchi", - deleted: ref("TIME"), - }, - { - tag: "sickhack", - id: "2", - prefix: "kikuri\x00\x00", - suffix: "", - }, - { - tag: "sickhack", - id: "2", - prefix: "\x00", - suffix: "kikuri", - }, - }, - msgs: []msg{ - { - tag: "kessoku", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - { - tag: "kessoku", - id: "5", - time: 6, - user: userhash.Hash{4}, - deleted: ref("TIME"), - }, - { - tag: "sickhack", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - }, - }, - { - name: "all", - tag: "kessoku", - since: 1, - before: 8, - know: []know{ - { - tag: "kessoku", - id: "2", - prefix: "kita\x00nijika\x00ryo\x00bocchi\x00\x00", - suffix: "", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "nijika\x00ryo\x00bocchi\x00\x00", - suffix: "kita", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "ryo\x00bocchi\x00\x00", - suffix: "nijika", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "bocchi\x00\x00", - suffix: "ryo", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "2", - prefix: "\x00", - suffix: "bocchi", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "5", - prefix: "bocchi\x00\x00", - suffix: "", - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "5", - prefix: "\x00", - suffix: "bocchi", - deleted: ref("TIME"), - }, - { - tag: "sickhack", - id: "2", - prefix: "kikuri\x00\x00", - suffix: "", - }, - { - tag: "sickhack", - id: "2", - prefix: "\x00", - suffix: "kikuri", - }, - }, - msgs: []msg{ - { - tag: "kessoku", - id: "2", - time: 3, - user: userhash.Hash{1}, - deleted: ref("TIME"), - }, - { - tag: "kessoku", - id: "5", - time: 6, - user: userhash.Hash{4}, - deleted: ref("TIME"), - }, - { - tag: "sickhack", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - }, - }, - { - name: "tagged", - tag: "sickhack", - since: 1, - before: 7, - know: []know{ - { - tag: "kessoku", - id: "2", - prefix: "kita\x00nijika\x00ryo\x00bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "2", - prefix: "nijika\x00ryo\x00bocchi\x00\x00", - suffix: "kita", - }, - { - tag: "kessoku", - id: "2", - prefix: "ryo\x00bocchi\x00\x00", - suffix: "nijika", - }, - { - tag: "kessoku", - id: "2", - prefix: "bocchi\x00\x00", - suffix: "ryo", - }, - { - tag: "kessoku", - id: "2", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "kessoku", - id: "5", - prefix: "bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "5", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "sickhack", - id: "2", - prefix: "kikuri\x00\x00", - suffix: "", - deleted: ref("TIME"), - }, - { - tag: "sickhack", - id: "2", - prefix: "\x00", - suffix: "kikuri", - deleted: ref("TIME"), - }, - }, - msgs: []msg{ - { - tag: "kessoku", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - { - tag: "kessoku", - id: "5", - time: 6, - user: userhash.Hash{4}, - }, - { - tag: "sickhack", - id: "2", - time: 3, - user: userhash.Hash{1}, - deleted: ref("TIME"), - }, - }, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - ctx := context.Background() - db := testDB(ctx) - br, err := sqlbrain.Open(ctx, db) - if err != nil { - t.Fatalf("couldn't open brain: %v", err) - } - for _, m := range learn { - err := br.Learn(ctx, m.tag, m.id, m.user, time.Unix(0, m.t), m.tups) - if err != nil { - t.Errorf("failed to learn %v/%v: %v", m.tag, m.id, err) - } - } - conn, err := db.Take(ctx) - defer db.Put(conn) - if err != nil { - t.Fatalf("couldn't get conn to check db state: %v", err) - } - contents(t, conn, initKnow, initMsgs) - if t.Failed() { - t.Fatal("setup failed") - } - since, before := time.Unix(0, c.since), time.Unix(0, c.before) - if err := br.ForgetDuring(ctx, c.tag, since, before); err != nil { - t.Errorf("couldn't delete in %v between %d and %d: %v", c.tag, c.since, c.before, err) - } - contents(t, conn, c.know, c.msgs) - }) - } -} - -func TestForgetUser(t *testing.T) { - learn := []learn{ - { - tag: "kessoku", - user: userhash.Hash{1}, - id: "2", - t: 3, - tups: []brain.Tuple{ - {Prefix: strings.Fields("kita nijika ryo bocchi"), Suffix: ""}, - {Prefix: strings.Fields("nijika ryo bocchi"), Suffix: "kita"}, - {Prefix: strings.Fields("ryo bocchi"), Suffix: "nijika"}, - {Prefix: strings.Fields("bocchi"), Suffix: "ryo"}, - {Prefix: nil, Suffix: "bocchi"}, - }, - }, - { - tag: "kessoku", - user: userhash.Hash{1}, - id: "5", - t: 6, - tups: []brain.Tuple{ - {Prefix: []string{"bocchi"}, Suffix: ""}, - {Prefix: nil, Suffix: "bocchi"}, - }, - }, - { - tag: "sickhack", - user: userhash.Hash{4}, - id: "2", - t: 3, - tups: []brain.Tuple{ - {Prefix: []string{"kikuri"}, Suffix: ""}, - {Prefix: nil, Suffix: "kikuri"}, - }, - }, - } - initKnow := []know{ - { - tag: "kessoku", - id: "2", - prefix: "kita\x00nijika\x00ryo\x00bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "2", - prefix: "nijika\x00ryo\x00bocchi\x00\x00", - suffix: "kita", - }, - { - tag: "kessoku", - id: "2", - prefix: "ryo\x00bocchi\x00\x00", - suffix: "nijika", - }, - { - tag: "kessoku", - id: "2", - prefix: "bocchi\x00\x00", - suffix: "ryo", - }, - { - tag: "kessoku", - id: "2", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "kessoku", - id: "5", - prefix: "bocchi\x00\x00", - suffix: "", - }, - { - tag: "kessoku", - id: "5", - prefix: "\x00", - suffix: "bocchi", - }, - { - tag: "sickhack", - id: "2", - prefix: "kikuri\x00\x00", - suffix: "", - }, - { - tag: "sickhack", - id: "2", - prefix: "\x00", - suffix: "kikuri", - }, - } - initMsgs := []msg{ - { - tag: "kessoku", - id: "2", - time: 3, - user: userhash.Hash{1}, - }, - { - tag: "kessoku", - id: "5", - time: 6, - user: userhash.Hash{1}, - }, - { - tag: "sickhack", - id: "2", - time: 3, - user: userhash.Hash{4}, - }, - } - cases := []struct { - name string - user userhash.Hash - know []know - msgs []msg - }{ - { - name: "none", - user: userhash.Hash{100}, - know: initKnow, - msgs: initMsgs, - }, - { - name: "all", - user: userhash.Hash{1}, - know: []know{ - { - tag: "kessoku", - id: "2", - prefix: "kita\x00nijika\x00ryo\x00bocchi\x00\x00", - suffix: "", - deleted: ref("CLEARCHAT"), - }, - { - tag: "kessoku", - id: "2", - prefix: "nijika\x00ryo\x00bocchi\x00\x00", - suffix: "kita", - deleted: ref("CLEARCHAT"), - }, - { - tag: "kessoku", - id: "2", - prefix: "ryo\x00bocchi\x00\x00", - suffix: "nijika", - deleted: ref("CLEARCHAT"), - }, - { - tag: "kessoku", - id: "2", - prefix: "bocchi\x00\x00", - suffix: "ryo", - deleted: ref("CLEARCHAT"), - }, - { - tag: "kessoku", - id: "2", - prefix: "\x00", - suffix: "bocchi", - deleted: ref("CLEARCHAT"), - }, - { - tag: "kessoku", - id: "5", - prefix: "bocchi\x00\x00", - suffix: "", - deleted: ref("CLEARCHAT"), - }, - { - tag: "kessoku", - id: "5", - prefix: "\x00", - suffix: "bocchi", - deleted: ref("CLEARCHAT"), - }, - { - tag: "sickhack", - id: "2", - prefix: "kikuri\x00\x00", - suffix: "", - }, - { - tag: "sickhack", - id: "2", - prefix: "\x00", - suffix: "kikuri", - }, - }, - msgs: []msg{ - { - tag: "kessoku", - id: "2", - time: 3, - user: userhash.Hash{1}, - deleted: ref("CLEARCHAT"), - }, - { - tag: "kessoku", - id: "5", - time: 6, - user: userhash.Hash{1}, - deleted: ref("CLEARCHAT"), - }, - { - tag: "sickhack", - id: "2", - time: 3, - user: userhash.Hash{4}, - }, - }, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - ctx := context.Background() - db := testDB(ctx) - br, err := sqlbrain.Open(ctx, db) - if err != nil { - t.Fatalf("couldn't open brain: %v", err) - } - for _, m := range learn { - err := br.Learn(ctx, m.tag, m.id, m.user, time.Unix(0, m.t), m.tups) - if err != nil { - t.Errorf("failed to learn %v/%v: %v", m.tag, m.id, err) - } - } - conn, err := db.Take(ctx) - defer db.Put(conn) - if err != nil { - t.Fatalf("couldn't get conn to check db state: %v", err) - } - contents(t, conn, initKnow, initMsgs) - if t.Failed() { - t.Fatal("setup failed") - } - if err := br.ForgetUser(ctx, &c.user); err != nil { - t.Errorf("couldn't delete from %x: %v", c.user, err) - } - contents(t, conn, c.know, c.msgs) - }) - } -} diff --git a/command/moderate.go b/command/moderate.go index 0bd9711..3bac19d 100644 --- a/command/moderate.go +++ b/command/moderate.go @@ -20,7 +20,8 @@ func Forget(ctx context.Context, robo *Robot, call *Invocation) { slog.String("tag", call.Channel.Learn), slog.String("id", m.ID), ) - err := robo.Brain.ForgetMessage(ctx, call.Channel.Learn, m.ID) + robo.Metrics.ForgotCount.Observe(1) + err := robo.Brain.Forget(ctx, call.Channel.Learn, m.ID) if err != nil { robo.Log.ErrorContext(ctx, "failed to forget", slog.Any("err", err), diff --git a/tmi.go b/tmi.go index 61fd989..6a2fa9d 100644 --- a/tmi.go +++ b/tmi.go @@ -11,7 +11,6 @@ import ( "github.com/zephyrtronium/robot/brain" "github.com/zephyrtronium/robot/metrics" - "github.com/zephyrtronium/robot/userhash" ) func (robo *Robot) tmiLoop(ctx context.Context, group *errgroup.Group, send chan<- *tmi.Message, recv <-chan *tmi.Message) { @@ -100,9 +99,17 @@ func (robo *Robot) clearchat(ctx context.Context, msg *tmi.Message) { // Delete all recent chat. tag := ch.Learn slog.InfoContext(ctx, "clear all chat", slog.String("channel", msg.To()), slog.String("tag", tag)) - err := robo.brain.ForgetDuring(ctx, tag, msg.Time().Add(-15*time.Minute), msg.Time()) - if err != nil { - slog.ErrorContext(ctx, "failed to forget from all chat", slog.Any("err", err), slog.String("channel", msg.To())) + for m := range ch.History.All() { + slog.DebugContext(ctx, "forget all chat", slog.String("channel", msg.To()), slog.String("id", m.ID)) + robo.Metrics.ForgotCount.Observe(1) + err := robo.brain.Forget(ctx, tag, m.ID) + if err != nil { + slog.ErrorContext(ctx, "failed to forget while clearing all chat", + slog.Any("err", err), + slog.String("channel", msg.To()), + slog.String("id", m.ID), + ) + } } case robo.tmi.userID: // We use the send tag because we are forgetting something we sent. @@ -117,8 +124,9 @@ func (robo *Robot) clearchat(ctx context.Context, msg *tmi.Message) { ) continue } + slog.DebugContext(ctx, "forget from recent trace", slog.String("channel", msg.To()), slog.String("id", id)) robo.Metrics.ForgotCount.Observe(1) - if err := robo.brain.ForgetMessage(ctx, tag, id); err != nil { + if err := robo.brain.Forget(ctx, tag, id); err != nil { slog.ErrorContext(ctx, "failed to forget from recent trace", slog.Any("err", err), slog.String("channel", msg.To()), @@ -129,17 +137,19 @@ func (robo *Robot) clearchat(ctx context.Context, msg *tmi.Message) { } default: // Delete from user. - // We use the user's current and previous userhash, since userhashes - // are time-based. - hr := robo.hashes() - h := hr.Hash(new(userhash.Hash), t, msg.To(), msg.Time()) - if err := robo.brain.ForgetUser(ctx, h); err != nil { - slog.ErrorContext(ctx, "failed to forget recent messages from user", slog.Any("err", err), slog.String("channel", msg.To())) - // Try the previous userhash anyway. - } - h = hr.Hash(h, t, msg.To(), msg.Time().Add(-userhash.TimeQuantum)) - if err := robo.brain.ForgetUser(ctx, h); err != nil { - slog.ErrorContext(ctx, "failed to forget older messages from user", slog.Any("err", err), slog.String("channel", msg.To())) + for m := range ch.History.All() { + if m.Sender != t { + continue + } + slog.DebugContext(ctx, "forget from user", slog.String("channel", msg.To()), slog.String("id", m.ID)) + robo.Metrics.ForgotCount.Observe(1) + if err := robo.brain.Forget(ctx, ch.Learn, m.ID); err != nil { + slog.ErrorContext(ctx, "failed to forget from user", + slog.Any("err", err), + slog.String("channel", msg.To()), + slog.String("id", m.ID), + ) + } } } } @@ -184,7 +194,7 @@ func (robo *Robot) clearmsg(ctx context.Context, msg *tmi.Message) { func forget(ctx context.Context, log *slog.Logger, forgetCount metrics.Observer, brain brain.Brain, tag string, trace ...string) { forgetCount.Observe(1) for _, id := range trace { - err := brain.ForgetMessage(ctx, tag, id) + err := brain.Forget(ctx, tag, id) if err != nil { log.ErrorContext(ctx, "failed to forget message", slog.Any("err", err),