4fb3509128
Фаза 4 (AGENT.md 7.17, 7.18): модерация участников.
- миграция 00011: таблица `guild_bans` (бан переживает исключение участника);
- `PUT /guilds/{id}/bans/{user_id}` — бан с причиной и права `BAN_MEMBERS`:
исключает участника, отключает его от голосовой комнаты, пишет системное
сообщение, аудит и события `MEMBER_REMOVE`/`GUILD_DELETE`;
- `DELETE /guilds/{id}/bans/{user_id}` — снятие бана, `GET .../bans` — список
с автором и причиной (только модераторам);
- вход по приглашению для забаненного отклоняется с `guild.banned`;
- тайм-аут уже был в схеме, но не работал: кэш прав комнат не сбрасывался при
изменениях сервера, поэтому участник с тайм-аутом продолжал писать. Ключ
кэша комнат теперь включает сервер, `InvalidateGuild` чистит и его;
- тайм-аут уходит в `MEMBER_UPDATE` (и снимается событием), истёкшие
тайм-ауты не показываются в профиле участника;
- журнал аудита получил фильтры `action`, `actor_id`, `target_id`, `before_id`;
- тесты: полный цикл бана (включая запрет входа и разбан), права и иерархия,
блокировка сообщений тайм-аутом, запрет тайм-аута на себя, фильтры аудита.
160 lines
4.0 KiB
Go
160 lines
4.0 KiB
Go
package permissions
|
|
|
|
import (
|
|
"container/list"
|
|
"sync"
|
|
)
|
|
|
|
// Cache — простой LRU-кэш вычисленных прав с инвалидацией при записи
|
|
// (AGENT.md 9.1). Ключи: сервер+пользователь и комната+пользователь.
|
|
type Cache struct {
|
|
mu sync.Mutex
|
|
capacity int
|
|
items map[guildKey]*list.Element
|
|
channels map[channelKey]*list.Element
|
|
order *list.List
|
|
}
|
|
|
|
type guildKey struct {
|
|
guildID uint64
|
|
userID uint64
|
|
}
|
|
|
|
type channelKey struct {
|
|
// guildID нужен, чтобы сброс сервера (роли, тайм-аут, оверрайды) сбрасывал
|
|
// и кэш его комнат: иначе участник с тайм-аутом продолжал писать.
|
|
guildID uint64
|
|
channelID uint64
|
|
userID uint64
|
|
}
|
|
|
|
type cacheEntry struct {
|
|
guild guildKey
|
|
channel channelKey
|
|
value Resolved
|
|
isChan bool
|
|
}
|
|
|
|
func NewCache(capacity int) *Cache {
|
|
if capacity <= 0 {
|
|
capacity = 1024
|
|
}
|
|
return &Cache{
|
|
capacity: capacity,
|
|
items: make(map[guildKey]*list.Element, capacity),
|
|
channels: make(map[channelKey]*list.Element, capacity),
|
|
order: list.New(),
|
|
}
|
|
}
|
|
|
|
func (c *Cache) GetGuild(guildID, userID uint64) (Resolved, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
element, ok := c.items[guildKey{guildID: guildID, userID: userID}]
|
|
if !ok {
|
|
return Resolved{}, false
|
|
}
|
|
c.order.MoveToFront(element)
|
|
return element.Value.(*cacheEntry).value, true
|
|
}
|
|
|
|
func (c *Cache) PutGuild(guildID, userID uint64, value Resolved) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
key := guildKey{guildID: guildID, userID: userID}
|
|
if element, ok := c.items[key]; ok {
|
|
element.Value.(*cacheEntry).value = value
|
|
c.order.MoveToFront(element)
|
|
return
|
|
}
|
|
entry := &cacheEntry{guild: key, value: value}
|
|
c.items[key] = c.order.PushFront(entry)
|
|
c.evictLocked()
|
|
}
|
|
|
|
func (c *Cache) GetChannel(guildID, channelID, userID uint64) (Resolved, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
element, ok := c.channels[channelKey{guildID: guildID, channelID: channelID, userID: userID}]
|
|
if !ok {
|
|
return Resolved{}, false
|
|
}
|
|
c.order.MoveToFront(element)
|
|
return element.Value.(*cacheEntry).value, true
|
|
}
|
|
|
|
func (c *Cache) PutChannel(guildID, channelID, userID uint64, value Resolved) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
key := channelKey{guildID: guildID, channelID: channelID, userID: userID}
|
|
if element, ok := c.channels[key]; ok {
|
|
element.Value.(*cacheEntry).value = value
|
|
c.order.MoveToFront(element)
|
|
return
|
|
}
|
|
entry := &cacheEntry{channel: key, value: value, isChan: true}
|
|
c.channels[key] = c.order.PushFront(entry)
|
|
c.evictLocked()
|
|
}
|
|
|
|
// InvalidateGuild сбрасывает права всех пользователей сервера, включая кэш
|
|
// его комнат: роли, тайм-аут и оверрайды влияют и на права в комнатах.
|
|
func (c *Cache) InvalidateGuild(guildID uint64) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for key, element := range c.items {
|
|
if key.guildID == guildID {
|
|
c.order.Remove(element)
|
|
delete(c.items, key)
|
|
}
|
|
}
|
|
for key, element := range c.channels {
|
|
if key.guildID == guildID {
|
|
c.order.Remove(element)
|
|
delete(c.channels, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// InvalidateChannel сбрасывает кэш комнаты.
|
|
func (c *Cache) InvalidateChannel(channelID uint64) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for key, element := range c.channels {
|
|
if key.channelID == channelID {
|
|
c.order.Remove(element)
|
|
delete(c.channels, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) Clear() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.items = make(map[guildKey]*list.Element, c.capacity)
|
|
c.channels = make(map[channelKey]*list.Element, c.capacity)
|
|
c.order.Init()
|
|
}
|
|
|
|
func (c *Cache) Len() int {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.order.Len()
|
|
}
|
|
|
|
func (c *Cache) evictLocked() {
|
|
for c.order.Len() > c.capacity {
|
|
oldest := c.order.Back()
|
|
if oldest == nil {
|
|
return
|
|
}
|
|
entry := oldest.Value.(*cacheEntry)
|
|
if entry.isChan {
|
|
delete(c.channels, entry.channel)
|
|
} else {
|
|
delete(c.items, entry.guild)
|
|
}
|
|
c.order.Remove(oldest)
|
|
}
|
|
}
|