package store import ( "context" "database/sql" "time" ) // Webhook — вебхук комнаты (AGENT.md 7.11): отправляет сообщения без сессии. type Webhook struct { ID uint64 GuildID uint64 ChannelID uint64 Name string AvatarFileID *uint64 Token string CreatedBy *uint64 CreatedAt time.Time } // CreateWebhookParams — параметры нового вебхука. type CreateWebhookParams struct { ID uint64 GuildID uint64 ChannelID uint64 Name string AvatarFileID *uint64 Token string CreatedBy uint64 } // UpdateWebhookParams — новое состояние вебхука: имя, аватар и токен // передаются целиком, чтобы пересоздание токена было обычной правкой. type UpdateWebhookParams struct { Name string AvatarFileID *uint64 Token string } const webhookColumns = `id, guild_id, channel_id, name, avatar_file_id, token, created_by, created_at` // CreateWebhook сохраняет вебхук комнаты. func (s *Store) CreateWebhook(ctx context.Context, params CreateWebhookParams) (*Webhook, error) { if params.ID == 0 { params.ID = s.NextID() } _, err := s.writer.ExecContext(ctx, ` INSERT INTO webhooks (id, guild_id, channel_id, name, avatar_file_id, token, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, int64(params.ID), int64(params.GuildID), int64(params.ChannelID), params.Name, nullableID(params.AvatarFileID), params.Token, int64(params.CreatedBy), s.Now()) if err != nil { return nil, mapError(err) } return s.GetWebhook(ctx, params.ID) } func (s *Store) GetWebhook(ctx context.Context, id uint64) (*Webhook, error) { row := s.reader.QueryRowContext(ctx, `SELECT `+webhookColumns+` FROM webhooks WHERE id = ?`, int64(id)) return scanWebhook(row) } // ListChannelWebhooks возвращает вебхуки комнаты: их показывает панель // управления комнатой (AGENT.md 7.11). func (s *Store) ListChannelWebhooks(ctx context.Context, channelID uint64) ([]Webhook, error) { rows, err := s.reader.QueryContext(ctx, `SELECT `+webhookColumns+` FROM webhooks WHERE channel_id = ? ORDER BY id`, int64(channelID)) if err != nil { return nil, err } defer rows.Close() webhooks := make([]Webhook, 0, 8) for rows.Next() { webhook, err := scanWebhook(rows) if err != nil { return nil, err } webhooks = append(webhooks, *webhook) } return webhooks, rows.Err() } // UpdateWebhook заменяет имя, аватар и токен вебхука. func (s *Store) UpdateWebhook(ctx context.Context, id uint64, params UpdateWebhookParams) (*Webhook, error) { result, err := s.writer.ExecContext(ctx, `UPDATE webhooks SET name = ?, avatar_file_id = ?, token = ? WHERE id = ?`, params.Name, nullableID(params.AvatarFileID), params.Token, int64(id)) if err != nil { return nil, mapError(err) } if affected, err := result.RowsAffected(); err == nil && affected == 0 { return nil, ErrNotFound } return s.GetWebhook(ctx, id) } // DeleteWebhook удаляет вебхук; уже отправленные сообщения остаются (в них // хранится снимок имени и аватара). func (s *Store) DeleteWebhook(ctx context.Context, id uint64) error { result, err := s.writer.ExecContext(ctx, `DELETE FROM webhooks WHERE id = ?`, int64(id)) if err != nil { return mapError(err) } if affected, err := result.RowsAffected(); err == nil && affected == 0 { return ErrNotFound } return nil } func scanWebhook(scanner interface{ Scan(...any) error }) (*Webhook, error) { var ( webhook Webhook avatar sql.NullInt64 createdBy sql.NullInt64 createdAt string ) if err := scanner.Scan(&webhook.ID, &webhook.GuildID, &webhook.ChannelID, &webhook.Name, &avatar, &webhook.Token, &createdBy, &createdAt); err != nil { return nil, mapError(err) } webhook.AvatarFileID = optionalID(avatar) webhook.CreatedBy = optionalID(createdBy) webhook.CreatedAt = parseTimestamp(createdAt) return &webhook, nil }