feat(dm): иконка групповой беседы в хранилище и API (Фаза 7)
D-070: у групповой беседы не было своей иконки — показывались имя и первая
буква аватара, как у 1:1 без аватара. Колонки под файл у канала не было.
Миграция 00024 добавляет `channels.icon_file_id` (REFERENCES files ON DELETE
SET NULL): NULL у 1:1 и комнат сервера, файл — у группы. Store читает и пишет
поле через `Channel`/`UpdateChannelParams` (`IconFileID`/`ClearIcon`), отдаёт
его в `ListDMChannels` и в READY, а уборка сирот больше не считает иконку
беседы мусором. Когда в группе остаётся два участника, беседа снова обычная
личная — иконка сбрасывается вместе с именем и владельцем.
Ручки `POST/DELETE /channels/{id}/icon` принимают multipart (назначение файла
`dm_icon`, аватарный лимит, проверка `image/`) и доступны только владельцу
беседы: участнику-не-владельцу 403 `perm.denied`, посторонним 404 (существование
чужой беседы не подтверждаем), у 1:1 своей иконки нет — 422 `dm.not_group`.
Файл иконки отдаётся только участникам беседы. После изменения участникам
уходит `DM_CHANNEL_UPDATE`, поэтому иконка меняется без перезагрузки
(AGENT.md 7.7, 7.8, 8.3, 11.6; D-042, D-070).
This commit is contained in:
@@ -34,7 +34,11 @@ type Channel struct {
|
||||
// DMOwnerID — владелец групповой личной беседы (AGENT.md 7.8): только он
|
||||
// добавляет и удаляет участников и переименовывает беседу. У 1:1 — NULL.
|
||||
DMOwnerID *uint64
|
||||
CreatedAt time.Time
|
||||
// IconFileID — иконка групповой беседы (AGENT.md 7.8, D-070): обычная
|
||||
// загрузка, менять её может только владелец. У 1:1 иконка — аватар
|
||||
// собеседника, поэтому колонка пустая.
|
||||
IconFileID *uint64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ChannelOverride struct {
|
||||
@@ -59,7 +63,8 @@ type CreateChannelParams struct {
|
||||
}
|
||||
|
||||
const channelColumns = `id, guild_id, type, name, description, position, parent_id,
|
||||
slowmode_seconds, nsfw, background, background_file_id, voice_status, user_limit, dm_owner_id, created_at`
|
||||
slowmode_seconds, nsfw, background, background_file_id, voice_status, user_limit, dm_owner_id,
|
||||
created_at, icon_file_id`
|
||||
|
||||
func (s *Store) CreateChannel(ctx context.Context, params CreateChannelParams) (*Channel, error) {
|
||||
if params.ID == 0 {
|
||||
@@ -105,6 +110,9 @@ type UpdateChannelParams struct {
|
||||
ClearBackground bool
|
||||
VoiceStatus *string
|
||||
UserLimit *int
|
||||
// IconFileID задаёт иконку групповой беседы, ClearIcon снимает её (D-070).
|
||||
IconFileID *uint64
|
||||
ClearIcon bool
|
||||
}
|
||||
|
||||
func (s *Store) UpdateChannel(ctx context.Context, id uint64, params UpdateChannelParams) (*Channel, error) {
|
||||
@@ -156,6 +164,13 @@ func (s *Store) UpdateChannel(ctx context.Context, id uint64, params UpdateChann
|
||||
sets = append(sets, "user_limit = ?")
|
||||
args = append(args, *params.UserLimit)
|
||||
}
|
||||
if params.IconFileID != nil {
|
||||
sets = append(sets, "icon_file_id = ?")
|
||||
args = append(args, int64(*params.IconFileID))
|
||||
}
|
||||
if params.ClearIcon {
|
||||
sets = append(sets, "icon_file_id = NULL")
|
||||
}
|
||||
if len(sets) == 0 {
|
||||
return s.GetChannel(ctx, id)
|
||||
}
|
||||
@@ -274,10 +289,11 @@ func scanChannel(scanner interface{ Scan(...any) error }) (*Channel, error) {
|
||||
backgroundID sql.NullInt64
|
||||
dmOwnerID sql.NullInt64
|
||||
createdAt string
|
||||
iconID sql.NullInt64
|
||||
)
|
||||
err := scanner.Scan(&channel.ID, &guildID, &channel.Type, &channel.Name, &channel.Description,
|
||||
&channel.Position, &parentID, &channel.SlowmodeSeconds, &nsfw, &background,
|
||||
&backgroundID, &channel.VoiceStatus, &channel.UserLimit, &dmOwnerID, &createdAt)
|
||||
&backgroundID, &channel.VoiceStatus, &channel.UserLimit, &dmOwnerID, &createdAt, &iconID)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
@@ -289,6 +305,7 @@ func scanChannel(scanner interface{ Scan(...any) error }) (*Channel, error) {
|
||||
}
|
||||
channel.BackgroundFileID = optionalID(backgroundID)
|
||||
channel.DMOwnerID = optionalID(dmOwnerID)
|
||||
channel.IconFileID = optionalID(iconID)
|
||||
channel.CreatedAt = parseTimestamp(createdAt)
|
||||
return &channel, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user