package bot import ( "context" "database/sql" "encoding/json" "fmt" "strings" "time" ) // ConfessConfig: "channel_id" is where anonymous confessions land. type ConfessConfig struct { ChannelID string `json:"channel_id"` } // ConfessBot watches for /confess messages, deletes the original, reposts anonymously. func ConfessBot(ctx context.Context, db *sql.DB, raw json.RawMessage, send SendMessageFunc) { var cfg ConfessConfig if err := json.Unmarshal(raw, &cfg); err != nil || cfg.ChannelID == "" { return } // Cursor is (created_at, id) in memory so deletes don't break polling. var lastAt time.Time var lastID string seeded := false ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: confessPoll(db, cfg.ChannelID, &seeded, &lastAt, &lastID, send) } } } func confessPoll(db *sql.DB, targetChannel string, seeded *bool, lastAt *time.Time, lastID *string, send SendMessageFunc) { if !*seeded { var at sql.NullTime var id sql.NullString _ = db.QueryRowContext(context.Background(), `SELECT created_at, id::text FROM messages ORDER BY created_at DESC, id DESC LIMIT 1`, ).Scan(&at, &id) if at.Valid { *lastAt = at.Time } if id.Valid { *lastID = id.String } *seeded = true return } rows, err := db.QueryContext(context.Background(), ` SELECT m.id::text, m.created_at, m.content FROM messages m WHERE m.bot_id IS NULL AND lower(m.content) LIKE '/confess%' AND (m.created_at, m.id) > ($1::timestamptz, $2::uuid) ORDER BY m.created_at ASC, m.id ASC LIMIT 20 `, *lastAt, nullUUID(*lastID)) if err != nil { return } defer rows.Close() for rows.Next() { var id string var createdAt time.Time var content string if err := rows.Scan(&id, &createdAt, &content); err != nil { continue } *lastAt = createdAt *lastID = id rest := content if len(rest) >= 8 && strings.EqualFold(rest[:8], "/confess") { rest = rest[8:] } text := strings.TrimSpace(rest) if text == "" { continue } // Delete original so author isn't exposed in chat history. _, _ = db.ExecContext(context.Background(), `DELETE FROM messages WHERE id = $1::uuid`, id) send(targetChannel, fmt.Sprintf("🕵️ **anonymous confession:** %s", text)) } } // nullUUID returns a zero UUID when empty so the first post-seed poll still works. func nullUUID(id string) string { if id == "" { return "00000000-0000-0000-0000-000000000000" } return id }