fix(bots): intercept /confess so original never hits chat

Root cause of "not anonymous":
1. Confess deleted via raw SQL with no MESSAGE_DELETE broadcast
2. Frontend extractIds only accepted message_id, but deletes send id
   so live clients never removed deleted messages without refresh

Fix:
- Intercept /confess at message create: never store or broadcast the
  original; post only the anonymous bot message
- Accept both id and message_id on MESSAGE_DELETE in the WS store
- Include both fields on delete broadcasts
This commit is contained in:
2026-07-15 20:28:47 -04:00
parent 13bd4478f6
commit 7bf1eaf845
5 changed files with 165 additions and 78 deletions
+101
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"sync"
@@ -228,4 +229,104 @@ func (r *Runner) RegisteredTypes() []string {
return names
}
// TryConfess intercepts "/confess …" at message create time.
// On success the original is never stored or broadcast (true anonymity).
// Returns the anonymous bot message payload for the HTTP response when handled.
func (r *Runner) TryConfess(ctx context.Context, serverID, _authorID, content string) (map[string]interface{}, bool) {
text, ok := parseConfessContent(content)
if !ok {
return nil, false
}
var botID, botName, ownerID string
var rawConfig string
err := r.db.QueryRowContext(ctx, `
SELECT b.id, b.name, b.owner_id, COALESCE(b.config::text, '{}')
FROM bots b
JOIN bot_servers bs ON bs.bot_id = b.id
WHERE bs.server_id = $1 AND b.bot_type = 'confess'
LIMIT 1
`, serverID).Scan(&botID, &botName, &ownerID, &rawConfig)
if err != nil {
return nil, false
}
var cfg ConfessConfig
if err := json.Unmarshal([]byte(rawConfig), &cfg); err != nil || cfg.ChannelID == "" {
return nil, false
}
// Ensure bot can post to the confession channel's server.
r.ensureBotServerFromConfig(botID, json.RawMessage(rawConfig))
anonContent := fmt.Sprintf("🕵️ **anonymous confession:** %s", text)
if len(anonContent) > 4000 {
anonContent = anonContent[:4000]
}
var msgID, createdAt string
err = r.db.QueryRowContext(ctx, `
INSERT INTO messages (channel_id, author_id, content, bot_id)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at::text
`, cfg.ChannelID, ownerID, anonContent, botID).Scan(&msgID, &createdAt)
if err != nil {
r.logger.Error("confess: insert failed", "error", err)
return nil, false
}
targetServerID, err := r.hub.ServerIDForChannel(ctx, cfg.ChannelID)
if err != nil || targetServerID == "" {
targetServerID = serverID
}
payload := map[string]interface{}{
"id": msgID,
"channel_id": cfg.ChannelID,
"author_id": ownerID,
"author_username": botName,
"author_display_name": nil,
"author_bot": true,
"bot_id": botID,
"bot_name": botName,
"content": anonContent,
"reply_to": nil,
"edited_at": nil,
"pinned": false,
"created_at": createdAt,
"embeds": []interface{}{},
"reactions": []interface{}{},
}
r.hub.BroadcastToServer(targetServerID, gateway.Event{
Type: gateway.EventMessageCreate,
Data: payload,
})
return payload, true
}
// DeleteMessage removes a message and broadcasts MESSAGE_DELETE to live clients.
func (r *Runner) DeleteMessage(messageID string) {
var channelID string
err := r.db.QueryRowContext(context.Background(),
`DELETE FROM messages WHERE id = $1::uuid RETURNING channel_id`, messageID,
).Scan(&channelID)
if err != nil {
return
}
serverID, err := r.hub.ServerIDForChannel(context.Background(), channelID)
if err != nil || serverID == "" {
return
}
r.hub.BroadcastToServer(serverID, gateway.Event{
Type: gateway.EventMessageDelete,
Data: map[string]string{
"id": messageID,
"message_id": messageID,
"channel_id": channelID,
},
})
}