feat(cosmetics): оформление профиля — галерея, роли и личные стили
- миграция 00013: guild_cosmetics (рамки и иконки) и user_styles («для друзей» и пер-серверные стили) (AGENT.md 7.2, 7.4) - store: CRUD галереи, личные стили и вычисление оформления поэлементно (роль → пер-серверный стиль → «для друзей» → дефолт) с откатом, когда роль больше не выдаёт элемент - API: галерея сервера под MANAGE_ROLES, оформление роли в create/update, GET/PUT/DELETE /users/@me/styles с доступными элементами по областям - клиент: редактор личного стиля с предпросмотром, галерея оформления в настройках сервера, оформление роли в разделе «Роли» - рендер: рамка под аватаром, иконка рядом с ником, цвет и встроенные эффекты ника (CSS, при reduced-motion статично) в ленте и списке участников
This commit is contained in:
@@ -41,6 +41,11 @@ type rolePayload struct {
|
||||
IsDefault bool `json:"is_default"`
|
||||
Hoist bool `json:"hoist"`
|
||||
Mentionable bool `json:"mentionable"`
|
||||
// Оформление роли (AGENT.md 7.4): рамка, иконка, цвет и эффект ника.
|
||||
CosmeticFrameID string `json:"cosmetic_frame_id,omitempty"`
|
||||
CosmeticBadgeID string `json:"cosmetic_badge_id,omitempty"`
|
||||
NickColor *int64 `json:"nick_color,omitempty"`
|
||||
NickEffect string `json:"nick_effect,omitempty"`
|
||||
}
|
||||
|
||||
type memberPayload struct {
|
||||
@@ -55,6 +60,8 @@ type memberPayload struct {
|
||||
JoinedAt string `json:"joined_at"`
|
||||
TimeoutUntil string `json:"timeout_until,omitempty"`
|
||||
RoleIDs []string `json:"role_ids"`
|
||||
// Вычисленное оформление участника на этом сервере (AGENT.md 7.2).
|
||||
Cosmetics *cosmeticsPayload `json:"cosmetics,omitempty"`
|
||||
}
|
||||
|
||||
type guildDetailPayload struct {
|
||||
@@ -99,7 +106,7 @@ func channelFromStore(channel store.Channel, resolved *permissions.Resolved) cha
|
||||
}
|
||||
|
||||
func roleFromStore(role store.Role) rolePayload {
|
||||
return rolePayload{
|
||||
payload := rolePayload{
|
||||
ID: formatSnowflake(role.ID),
|
||||
Name: role.Name,
|
||||
Color: role.Color,
|
||||
@@ -108,7 +115,18 @@ func roleFromStore(role store.Role) rolePayload {
|
||||
IsDefault: role.IsDefault,
|
||||
Hoist: role.Hoist,
|
||||
Mentionable: role.Mentionable,
|
||||
NickColor: role.NickColor,
|
||||
}
|
||||
if role.CosmeticFrameID != nil {
|
||||
payload.CosmeticFrameID = formatSnowflake(*role.CosmeticFrameID)
|
||||
}
|
||||
if role.CosmeticBadgeID != nil {
|
||||
payload.CosmeticBadgeID = formatSnowflake(*role.CosmeticBadgeID)
|
||||
}
|
||||
if role.NickEffect != "" && role.NickEffect != string(store.NickEffectNone) {
|
||||
payload.NickEffect = role.NickEffect
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
type guildOutput struct {
|
||||
@@ -743,13 +761,20 @@ func (s *Server) registerMemberRoutes(api huma.API) {
|
||||
if err != nil {
|
||||
return nil, humaError(err)
|
||||
}
|
||||
// Оформление считается поэлементно для всего сервера (AGENT.md 7.2).
|
||||
cosmetics, err := s.store.EffectiveCosmeticsForGuild(ctx, guildID)
|
||||
if err != nil {
|
||||
return nil, humaError(err)
|
||||
}
|
||||
output := &memberListOutput{}
|
||||
output.Body.Members = make([]memberPayload, 0, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
if len(output.Body.Members) >= input.Limit {
|
||||
break
|
||||
}
|
||||
output.Body.Members = append(output.Body.Members, memberFromProfile(profile))
|
||||
payload := memberFromProfile(profile)
|
||||
payload.Cosmetics = cosmeticsFromEffective(cosmetics[profile.UserID])
|
||||
output.Body.Members = append(output.Body.Members, payload)
|
||||
}
|
||||
return output, nil
|
||||
})
|
||||
@@ -1098,11 +1123,15 @@ func (s *Server) registerRoleRoutes(api huma.API) {
|
||||
}, func(ctx context.Context, input *struct {
|
||||
GuildID string `path:"guild_id"`
|
||||
Body struct {
|
||||
Name string `json:"name" minLength:"1" maxLength:"64"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
Color int64 `json:"color,omitempty" minimum:"0" maximum:"16777215"`
|
||||
Hoist bool `json:"hoist,omitempty"`
|
||||
Mentionable bool `json:"mentionable,omitempty"`
|
||||
Name string `json:"name" minLength:"1" maxLength:"64"`
|
||||
Permissions string `json:"permissions,omitempty"`
|
||||
Color int64 `json:"color,omitempty" minimum:"0" maximum:"16777215"`
|
||||
Hoist bool `json:"hoist,omitempty"`
|
||||
Mentionable bool `json:"mentionable,omitempty"`
|
||||
CosmeticFrameID string `json:"cosmetic_frame_id,omitempty"`
|
||||
CosmeticBadgeID string `json:"cosmetic_badge_id,omitempty"`
|
||||
NickColor *int64 `json:"nick_color,omitempty"`
|
||||
NickEffect string `json:"nick_effect,omitempty"`
|
||||
}
|
||||
},
|
||||
) (*roleOutput, error) {
|
||||
@@ -1136,6 +1165,19 @@ func (s *Server) registerRoleRoutes(api huma.API) {
|
||||
if err != nil {
|
||||
return nil, humaError(err)
|
||||
}
|
||||
// Оформление роли (AGENT.md 7.4) сохраняем тем же обработчиком правки.
|
||||
if role, err = s.applyRoleCosmetics(ctx, guildID, role, roleCosmeticsInput{
|
||||
FrameID: input.Body.CosmeticFrameID,
|
||||
BadgeID: input.Body.CosmeticBadgeID,
|
||||
NickColor: input.Body.NickColor,
|
||||
NickEffect: input.Body.NickEffect,
|
||||
FrameSet: input.Body.CosmeticFrameID != "",
|
||||
BadgeSet: input.Body.CosmeticBadgeID != "",
|
||||
ColorSet: input.Body.NickColor != nil,
|
||||
EffectSet: input.Body.NickEffect != "",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.invalidateGuild(guildID)
|
||||
s.recordAudit(ctx, user, guildID, "role.create", "role", &role.ID, "")
|
||||
s.dispatchGuildEvent(guildID, "ROLE_CREATE", roleFromStore(*role))
|
||||
@@ -1161,6 +1203,11 @@ func (s *Server) registerRoleRoutes(api huma.API) {
|
||||
Position *int `json:"position,omitempty" minimum:"0" maximum:"1000"`
|
||||
Hoist *bool `json:"hoist,omitempty"`
|
||||
Mentionable *bool `json:"mentionable,omitempty"`
|
||||
// Оформление роли (AGENT.md 7.4): пустая строка снимает элемент.
|
||||
CosmeticFrameID *string `json:"cosmetic_frame_id,omitempty"`
|
||||
CosmeticBadgeID *string `json:"cosmetic_badge_id,omitempty"`
|
||||
NickColor *int64 `json:"nick_color,omitempty"`
|
||||
NickEffect *string `json:"nick_effect,omitempty"`
|
||||
}
|
||||
},
|
||||
) (*roleOutput, error) {
|
||||
@@ -1208,6 +1255,19 @@ func (s *Server) registerRoleRoutes(api huma.API) {
|
||||
if err != nil {
|
||||
return nil, humaError(err)
|
||||
}
|
||||
// Оформление роли (AGENT.md 7.4): рамка, иконка, цвет и эффект ника.
|
||||
if updated, err = s.applyRoleCosmetics(ctx, guildID, updated, roleCosmeticsInput{
|
||||
FrameID: optionalString(input.Body.CosmeticFrameID),
|
||||
BadgeID: optionalString(input.Body.CosmeticBadgeID),
|
||||
NickColor: input.Body.NickColor,
|
||||
NickEffect: optionalString(input.Body.NickEffect),
|
||||
FrameSet: input.Body.CosmeticFrameID != nil,
|
||||
BadgeSet: input.Body.CosmeticBadgeID != nil,
|
||||
ColorSet: input.Body.NickColor != nil,
|
||||
EffectSet: input.Body.NickEffect != nil,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.invalidateGuild(guildID)
|
||||
s.recordAudit(ctx, user, guildID, "role.update", "role", &roleID, "")
|
||||
s.dispatchGuildEvent(guildID, "ROLE_UPDATE", roleFromStore(*updated))
|
||||
|
||||
Reference in New Issue
Block a user