feat(guilds): приватные комнаты — права доступа поверх серверных

Сервер:
- store: оверрайды всех комнат сервера одним запросом (без N+1) и снятие
  оверрайда с признаком «был ли он»;
- API: PUT/DELETE /guilds/{id}/channels/{cid}/overwrites/{role|user}/{tid}
  (права именами через `|`, как у ролей), step-up на изменение, проверка что
  роль принадлежит серверу, а участник состоит в нём;
- список комнат отдаёт permission_overwrites; после правки сбрасывается кэш
  прав комнаты, пишется аудит (channel.overwrite_set/delete) и уходит событие
  GUILD_CHANNELS_SYNC — видимость комнаты меняется у всех участников.

Клиент:
- редактор «Доступ к комнате»: приватность одним переключателем (запрет
  VIEW_CHANNEL для @everyone), права просмотра/переписки/входа для ролей и
  участников, подтверждение личности по требованию сервера;
- в настройках комнаты теперь и голосовые комнаты (фон и права), вебхуки —
  только у текстовых; событие GUILD_CHANNELS_SYNC перечитывает список комнат.

Тесты: 2 Go-теста (скрытие и открытие комнаты ролями, проверка цели и прав) и
3 web-теста редактора (маски, step-up, скрытие без MANAGE_ROLES).
This commit is contained in:
2026-09-22 19:14:05 +03:00
parent b403f6c894
commit 785af0dc7b
16 changed files with 1569 additions and 219 deletions
+10
View File
@@ -11,6 +11,7 @@ import {
} from '@/api/gateway';
import {
parseChannelDelete,
parseGuildChannelsSyncEvent,
parseChannelPayload,
parseDmChannelEvent,
parseGuildEmojisUpdate,
@@ -334,6 +335,15 @@ export function dispatchGatewayEvent(event: GatewayDispatch): void {
}
return;
}
case 'GUILD_CHANNELS_SYNC': {
// Права комнаты изменились: видимость может поменяться у всех
// участников, поэтому список комнат перечитываем целиком (AGENT.md 11.6).
const sync = parseGuildChannelsSyncEvent(event.d);
if (sync !== null) {
void session.reloadGuildChannels(sync.guild_id);
}
return;
}
case 'DM_CHANNEL_CREATE': {
// В событии только channel_id: список бесед забираем по REST.
const direct = parseDmChannelEvent(event.d);
+12
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import type { GuildEmoji } from '@/api/emojis';
import { fetchChannels } from '@/api/guilds';
import { fetchDirectChannels } from '@/api/friends';
import type { GatewayGuild, GatewaySnapshot, GatewayUser } from '@/api/gateway';
import type { GuildSound, SoundKind } from '@/api/sounds';
@@ -113,6 +114,8 @@ interface SessionState {
reloadDmChannels: () => Promise<void>;
/** CHANNEL_DELETE: удаляет комнату и переносит выбор на первую доступную. */
removeChannel: (guildId: string, channelId: string) => void;
/** GUILD_CHANNELS_SYNC: перечитать видимые комнаты сервера (права изменились). */
reloadGuildChannels: (guildId: string) => Promise<void>;
/** Первая доступная (видимая) комната сервера — для выбора после удалений. */
firstChannelId: (guildId: string) => string | null;
selectGuild: (guildId: string | null) => void;
@@ -351,6 +354,15 @@ export const useSessionStore = create<SessionState>((set, get) => ({
});
},
reloadGuildChannels: async (guildId) => {
try {
const channels = await fetchChannels(guildId);
get().setGuildChannels(guildId, channels);
} catch {
// Сеть подвела: список обновится при следующем изменении прав.
}
},
setGuildChannels: (guildId, channels) => {
set({
guilds: get().guilds.map((guild) => (guild.id === guildId ? { ...guild, channels } : guild)),