Skip to content

Commit 5c5e903

Browse files
committed
feat: add uwu top yappers
1 parent 109eda8 commit 5c5e903

File tree

8 files changed

+427
-555
lines changed

8 files changed

+427
-555
lines changed

ash.go

Lines changed: 41 additions & 347 deletions
Large diffs are not rendered by default.

ash_test.go

Lines changed: 156 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
// Package main provides unit tests for the ash Matrix bot functionality.
21
package main
32

43
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"strings"
58
"testing"
9+
"time"
10+
11+
_ "github.com/mattn/go-sqlite3"
12+
"maunium.net/go/mautrix/event"
13+
"maunium.net/go/mautrix/id"
614
)
715

8-
// TestExtractLinks tests the URL extraction from text messages.
916
func TestExtractLinks(t *testing.T) {
1017
tests := []struct {
1118
name string
@@ -37,7 +44,6 @@ func TestExtractLinks(t *testing.T) {
3744
}
3845
}
3946

40-
// TestIsBlacklisted tests URL blacklisting functionality.
4147
func TestIsBlacklisted(t *testing.T) {
4248
blacklist, err := LoadBlacklist("blacklist.json")
4349
if err != nil {
@@ -47,7 +53,6 @@ func TestIsBlacklisted(t *testing.T) {
4753
_ = IsBlacklisted("https://example.com", blacklist)
4854
}
4955

50-
// TestExtractJSONPath tests JSON path extraction for bot commands.
5156
func TestExtractJSONPath(t *testing.T) {
5257
root := map[string]interface{}{
5358
"a": map[string]interface{}{
@@ -84,7 +89,6 @@ func TestExtractJSONPath(t *testing.T) {
8489
}
8590
}
8691

87-
// TestFormatPosts tests formatting of post arrays for bot responses.
8892
func TestFormatPosts(t *testing.T) {
8993
posts := []interface{}{
9094
map[string]interface{}{"title": "Post 1", "url": "https://a.com"},
@@ -102,7 +106,6 @@ func TestFormatPosts(t *testing.T) {
102106
}
103107
}
104108

105-
// TestFormatPostsLimit tests that formatPosts limits output to 5 posts.
106109
func TestFormatPostsLimit(t *testing.T) {
107110
// More than 5 posts should be capped
108111
posts := make([]interface{}, 10)
@@ -125,7 +128,6 @@ func TestFormatPostsLimit(t *testing.T) {
125128
}
126129
}
127130

128-
// TestTruncateText tests text truncation for AI prompts.
129131
func TestTruncateText(t *testing.T) {
130132
tests := []struct {
131133
name string
@@ -147,7 +149,6 @@ func TestTruncateText(t *testing.T) {
147149
}
148150
}
149151

150-
// TestStripCommandPrefix tests removal of bot command prefixes.
151152
func TestStripCommandPrefix(t *testing.T) {
152153
tests := []struct {
153154
input string
@@ -170,7 +171,6 @@ func TestStripCommandPrefix(t *testing.T) {
170171
}
171172
}
172173

173-
// TestResolveReplyLabel tests reply label resolution logic.
174174
func TestResolveReplyLabel(t *testing.T) {
175175
tests := []struct {
176176
name string
@@ -194,7 +194,6 @@ func TestResolveReplyLabel(t *testing.T) {
194194
}
195195
}
196196

197-
// TestInSlice tests string slice membership checking.
198197
func TestInSlice(t *testing.T) {
199198
slice := []string{"a", "b", "c"}
200199
if !inSlice(slice, "b") {
@@ -208,7 +207,6 @@ func TestInSlice(t *testing.T) {
208207
}
209208
}
210209

211-
// TestTruncate tests string truncation utility.
212210
func TestTruncate(t *testing.T) {
213211
if truncate("hello", 10) != "hello" {
214212
t.Error("truncate should not truncate short string")
@@ -219,7 +217,6 @@ func TestTruncate(t *testing.T) {
219217
}
220218
}
221219

222-
// TestGenerateHelpMessage tests help message generation for bot commands.
223220
func TestGenerateHelpMessage(t *testing.T) {
224221
botCfg := &BotConfig{
225222
Commands: map[string]BotCommand{
@@ -245,7 +242,6 @@ func TestGenerateHelpMessage(t *testing.T) {
245242
}
246243
}
247244

248-
// TestLoadBotConfig tests loading of bot configuration from file.
249245
func TestLoadBotConfig(t *testing.T) {
250246
cfg, err := LoadBotConfig("bot.json")
251247
if err != nil {
@@ -275,12 +271,10 @@ func TestLoadBotConfig(t *testing.T) {
275271

276272
// helpers
277273

278-
// contains checks if a substring is present in a string.
279274
func contains(s, substr string) bool {
280275
return len(s) >= len(substr) && searchString(s, substr)
281276
}
282277

283-
// searchString performs a simple substring search.
284278
func searchString(s, substr string) bool {
285279
for i := 0; i <= len(s)-len(substr); i++ {
286280
if s[i:i+len(substr)] == substr {
@@ -290,7 +284,6 @@ func searchString(s, substr string) bool {
290284
return false
291285
}
292286

293-
// splitLines splits a string into lines by newline characters.
294287
func splitLines(s string) []string {
295288
var lines []string
296289
start := 0
@@ -305,3 +298,150 @@ func splitLines(s string) []string {
305298
}
306299
return lines
307300
}
301+
302+
func TestUwuify(t *testing.T) {
303+
tests := []struct {
304+
name string
305+
input string
306+
check func(string) bool
307+
desc string
308+
}{
309+
{
310+
"replaces r/l with w",
311+
"really cool",
312+
func(s string) bool { return strings.Contains(s, "w") },
313+
"should replace r and l with w",
314+
},
315+
{
316+
"replaces th with d",
317+
"the weather",
318+
func(s string) bool { return strings.Contains(s, "da") && strings.Contains(s, "wead") },
319+
"should replace 'the ' with 'da ' and 'th' with 'd'",
320+
},
321+
{
322+
"replaces love with wuv",
323+
"I love you",
324+
func(s string) bool { return strings.Contains(s, "wuv") },
325+
"should replace love with wuv",
326+
},
327+
{
328+
"appends kaomoji",
329+
"hello world",
330+
func(s string) bool {
331+
faces := []string{"uwu", "owo", ">w<", "^w^", "◕ᴗ◕✿", "✧w✧", "~nyaa"}
332+
for _, f := range faces {
333+
if strings.HasSuffix(s, f) {
334+
return true
335+
}
336+
}
337+
return false
338+
},
339+
"should end with a kaomoji",
340+
},
341+
}
342+
for _, tt := range tests {
343+
t.Run(tt.name, func(t *testing.T) {
344+
got := uwuify(tt.input)
345+
if !tt.check(got) {
346+
t.Errorf("uwuify(%q) = %q: %s", tt.input, got, tt.desc)
347+
}
348+
})
349+
}
350+
}
351+
352+
func TestQueryTopYappers(t *testing.T) {
353+
db, err := sql.Open("sqlite3", ":memory:")
354+
if err != nil {
355+
t.Fatalf("open db: %v", err)
356+
}
357+
defer db.Close()
358+
359+
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS messages (
360+
id TEXT PRIMARY KEY,
361+
room_id TEXT,
362+
sender TEXT,
363+
ts_ms INTEGER,
364+
body TEXT,
365+
msgtype TEXT,
366+
raw_json TEXT
367+
)`)
368+
if err != nil {
369+
t.Fatalf("create table: %v", err)
370+
}
371+
372+
now := time.Now().UnixMilli()
373+
room := "!testroom:example.com"
374+
375+
// Insert test messages: alice=5, bob=3, carol=1, plus some bot messages that should be excluded.
376+
for i := 0; i < 5; i++ {
377+
_, _ = db.Exec(`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)`,
378+
fmt.Sprintf("alice-%d", i), room, "@alice:example.com", now-int64(i*1000), fmt.Sprintf("hello %d", i), "m.text")
379+
}
380+
for i := 0; i < 3; i++ {
381+
_, _ = db.Exec(`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)`,
382+
fmt.Sprintf("bob-%d", i), room, "@bob:example.com", now-int64(i*1000), fmt.Sprintf("hey %d", i), "m.text")
383+
}
384+
_, _ = db.Exec(`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)`,
385+
"carol-0", room, "@carol:example.com", now, "sup", "m.text")
386+
387+
// Bot messages — should be excluded.
388+
_, _ = db.Exec(`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)`,
389+
"bot-1", room, "@bot:example.com", now, "[BOT] hello", "m.text")
390+
_, _ = db.Exec(`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)`,
391+
"bot-2", room, "@bot:example.com", now, "/bot help", "m.text")
392+
393+
// Old message — should be excluded (>24h ago).
394+
_, _ = db.Exec(`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)`,
395+
"old-1", room, "@old:example.com", now-100000000, "ancient msg", "m.text")
396+
397+
// Different room — should be excluded.
398+
_, _ = db.Exec(`INSERT INTO messages(id, room_id, sender, ts_ms, body, msgtype) VALUES (?, ?, ?, ?, ?, ?)`,
399+
"other-1", "!otherroom:example.com", "@other:example.com", now, "wrong room", "m.text")
400+
401+
ev := &event.Event{
402+
RoomID: id.RoomID(room),
403+
}
404+
405+
ctx := context.Background()
406+
407+
// Test default (top 5).
408+
result, err := queryTopYappers(ctx, db, nil, ev, "")
409+
if err != nil {
410+
t.Fatalf("queryTopYappers: %v", err)
411+
}
412+
if !strings.Contains(result, "alice") {
413+
t.Errorf("expected alice in result, got: %s", result)
414+
}
415+
if !strings.Contains(result, "5 msgs") {
416+
t.Errorf("expected '5 msgs' for alice, got: %s", result)
417+
}
418+
if !strings.Contains(result, "bob") {
419+
t.Errorf("expected bob in result, got: %s", result)
420+
}
421+
// alice should be ranked #1.
422+
if !strings.Contains(result, "1. alice") {
423+
t.Errorf("expected alice at rank 1, got: %s", result)
424+
}
425+
426+
// Test with limit.
427+
result2, err := queryTopYappers(ctx, db, nil, ev, "2")
428+
if err != nil {
429+
t.Fatalf("queryTopYappers with limit: %v", err)
430+
}
431+
lines := strings.Split(strings.TrimSpace(result2), "\n")
432+
// Header line + 2 results.
433+
if len(lines) != 3 {
434+
t.Errorf("expected 3 lines (header + 2 results), got %d: %s", len(lines), result2)
435+
}
436+
437+
// Bot and old messages should not appear.
438+
if strings.Contains(result, "bot") {
439+
t.Errorf("bot messages should be excluded, got: %s", result)
440+
}
441+
if strings.Contains(result, "old") {
442+
t.Errorf("old messages should be excluded, got: %s", result)
443+
}
444+
if strings.Contains(result, "other") {
445+
t.Errorf("messages from other rooms should be excluded, got: %s", result)
446+
}
447+
}

0 commit comments

Comments
 (0)