feat(webhooks): вебхуки комнат — бэкенд, клиент и настройки комнаты

- миграция 00012: таблица webhooks, снимок имени и аватара в messages (AGENT.md 7.11)
- API: список/создание/правка/удаление и пересоздание токена (MANAGE_WEBHOOKS,
  step-up при создании, аудит), загрузка аватара отдельной multipart-ручкой
- исполнение POST /webhooks/{id}/{token} без сессии: content, username,
  avatar_url, файлы создателя вебхука, лимит 30/мин на вебхук
- клиент: пункт настроек «Комната» со списком вебхуков, копированием ссылки,
  пересозданием токена и удалением
- сообщения вебхуков: имя, аватар и значок в ленте вместо «неизвестного автора»
- fix: неполный ответ REST больше не затирает данные READY-снапшота
This commit is contained in:
2026-09-21 00:26:50 +03:00
parent 4ea2965892
commit 2e038a1d3f
32 changed files with 2494 additions and 23 deletions
+25 -5
View File
@@ -21,6 +21,9 @@ const (
MessageAction MessageType = "action"
MessageScream MessageType = "scream"
MessagePrivate MessageType = "private"
// MessageWebhook — сообщение вебхука (AGENT.md 7.11): у него нет автора,
// а имя и аватар берутся из снимка в самом сообщении.
MessageWebhook MessageType = "webhook"
)
// IsPrivate сообщает, что сообщение видно только автору и адресатам.
@@ -39,6 +42,10 @@ type Message struct {
Attachments []Attachment
Mentions []uint64
CreatedAt time.Time
// Поля вебхука (AGENT.md 7.11): идентификатор и снимок имени с аватаром.
WebhookID *uint64
WebhookName string
WebhookAvatar string
}
// Attachment — метаданные вложения: файл регистрируется в таблице files.
@@ -69,7 +76,8 @@ type ReadState struct {
}
const messageColumns = `id, channel_id, author_id, content, reply_to_id, type,
attachments_json, mentions_json, edited_at, pinned, created_at, content_lower`
attachments_json, mentions_json, edited_at, pinned, created_at, content_lower,
webhook_id, webhook_name, webhook_avatar`
// CreateMessageParams — параметры нового сообщения.
type CreateMessageParams struct {
@@ -81,6 +89,10 @@ type CreateMessageParams struct {
Type MessageType
Attachments []Attachment
Mentions []uint64
// Заполняется только вебхуками: автора-пользователя у них нет.
WebhookID *uint64
WebhookName string
WebhookAvatar string
}
// CreateMessage сохраняет сообщение; пустое содержимое без вложений запрещено
@@ -106,11 +118,13 @@ func (s *Store) CreateMessage(ctx context.Context, params CreateMessageParams) (
}
_, err = s.writer.ExecContext(ctx, `
INSERT INTO messages (id, channel_id, author_id, content, reply_to_id, type,
attachments_json, mentions_json, pinned, created_at, content_lower)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`,
attachments_json, mentions_json, pinned, created_at, content_lower,
webhook_id, webhook_name, webhook_avatar)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)`,
int64(params.ID), int64(params.ChannelID), author, params.Content,
nullableID(params.ReplyToID), string(params.Type), string(attachments), string(mentions),
s.Now(), strings.ToLower(params.Content))
s.Now(), strings.ToLower(params.Content), nullableID(params.WebhookID),
params.WebhookName, params.WebhookAvatar)
if err != nil {
return nil, mapError(err)
}
@@ -568,9 +582,11 @@ func scanMessage(scanner interface{ Scan(...any) error }) (*Message, error) {
pinned int
createdAt string
contentLow string
webhookID sql.NullInt64
)
err := scanner.Scan(&message.ID, &message.ChannelID, &authorID, &message.Content, &replyToID,
&message.Type, &attachments, &mentions, &editedAt, &pinned, &createdAt, &contentLow)
&message.Type, &attachments, &mentions, &editedAt, &pinned, &createdAt, &contentLow,
&webhookID, &message.WebhookName, &message.WebhookAvatar)
if err != nil {
return nil, mapError(err)
}
@@ -582,6 +598,10 @@ func scanMessage(scanner interface{ Scan(...any) error }) (*Message, error) {
value := uint64(replyToID.Int64)
message.ReplyToID = &value
}
if webhookID.Valid {
value := uint64(webhookID.Int64)
message.WebhookID = &value
}
if err := json.Unmarshal([]byte(attachments), &message.Attachments); err != nil {
message.Attachments = nil
}