import { create } from 'zustand'; import { api } from '../lib/api.ts'; // ponytail: mirror backend permission bitflags exactly. export const PERMS = { VIEW_CHANNEL: 1, SEND_MESSAGES: 2, MANAGE_MESSAGES: 4, KICK_MEMBERS: 8, BAN_MEMBERS: 16, MANAGE_SERVER: 32, MANAGE_CHANNELS: 64, ADMINISTRATOR: 128, CONNECT_VOICE: 256, SPEAK_VOICE: 512, SHARE_SCREEN: 1024, MUTE_MEMBERS: 2048, CREATE_INSTANT_INVITE: 4096, CHANGE_NICKNAME: 8192, MANAGE_NICKNAMES: 16384, MANAGE_ROLES: 32768, MANAGE_WEBHOOKS: 65536, EMBED_LINKS: 131072, ATTACH_FILES: 262144, ADD_REACTIONS: 524288, USE_EXTERNAL_EMOJIS: 1048576, MENTION_EVERYONE: 2097152, } as const; export type PermissionKey = keyof typeof PERMS; export const PERMISSION_LABELS: Record = { VIEW_CHANNEL: 'View Channel', SEND_MESSAGES: 'Send Messages', MANAGE_MESSAGES: 'Manage Messages', KICK_MEMBERS: 'Kick Members', BAN_MEMBERS: 'Ban Members', MANAGE_SERVER: 'Manage Server', MANAGE_CHANNELS: 'Manage Channels', ADMINISTRATOR: 'Administrator', CONNECT_VOICE: 'Connect Voice', SPEAK_VOICE: 'Speak Voice', SHARE_SCREEN: 'Share Screen', MUTE_MEMBERS: 'Mute Members', CREATE_INSTANT_INVITE: 'Create Instant Invite', CHANGE_NICKNAME: 'Change Nickname', MANAGE_NICKNAMES: 'Manage Nicknames', MANAGE_ROLES: 'Manage Roles', MANAGE_WEBHOOKS: 'Manage Webhooks', EMBED_LINKS: 'Embed Links', ATTACH_FILES: 'Attach Files', ADD_REACTIONS: 'Add Reactions', USE_EXTERNAL_EMOJIS: 'Use External Emojis', MENTION_EVERYONE: 'Mention @everyone', }; /** Grouped for role/channel permission UIs. ADMINISTRATOR is special-cased in editors. */ export const PERM_CATEGORIES: { name: string; keys: PermissionKey[] }[] = [ { name: 'General', keys: [ 'VIEW_CHANNEL', 'SEND_MESSAGES', 'MANAGE_MESSAGES', 'ADD_REACTIONS', 'EMBED_LINKS', 'ATTACH_FILES', 'MENTION_EVERYONE', 'USE_EXTERNAL_EMOJIS', 'CREATE_INSTANT_INVITE', 'CHANGE_NICKNAME', ], }, { name: 'Moderation', keys: ['KICK_MEMBERS', 'BAN_MEMBERS', 'MUTE_MEMBERS', 'MANAGE_NICKNAMES'], }, { name: 'Server', keys: ['MANAGE_SERVER', 'MANAGE_CHANNELS', 'MANAGE_ROLES', 'MANAGE_WEBHOOKS'], }, { name: 'Voice', keys: ['CONNECT_VOICE', 'SPEAK_VOICE', 'SHARE_SCREEN'], }, ]; export function hasPermission(perms: number, flag: number): boolean { // Match backend permissions.Has: all required bits must be present. return (perms & flag) === flag; } export interface ChannelOverride { id: string; channel_id: string; target_type: 'role' | 'user'; target_id: string; allow_bitflags: number; deny_bitflags: number; } interface PermissionState { overridesByChannel: Record; fetchOverrides: (channelId: string) => Promise; setOverride: (channelId: string, targetType: 'role' | 'user', targetId: string, allow: number, deny: number) => Promise; deleteOverride: (channelId: string, targetType: 'role' | 'user', targetId: string) => Promise; } export const usePermissionStore = create((set) => ({ overridesByChannel: {}, fetchOverrides: async (channelId) => { const overrides = await api.get(`/servers/_/channels/${channelId}/permissions`); set((state) => ({ overridesByChannel: { ...state.overridesByChannel, [channelId]: Array.isArray(overrides) ? overrides : [] }, })); }, setOverride: async (channelId, targetType, targetId, allow, deny) => { const override = await api.put(`/servers/_/channels/${channelId}/permissions/${targetType}/${targetId}`, { allow, deny }); set((state) => { const list = state.overridesByChannel[channelId] || []; const filtered = list.filter((o) => !(o.target_type === targetType && o.target_id === targetId)); return { overridesByChannel: { ...state.overridesByChannel, [channelId]: [...filtered, override] } }; }); return override; }, deleteOverride: async (channelId, targetType, targetId) => { await api.delete(`/servers/_/channels/${channelId}/permissions/${targetType}/${targetId}`); set((state) => { const list = state.overridesByChannel[channelId] || []; return { overridesByChannel: { ...state.overridesByChannel, [channelId]: list.filter((o) => !(o.target_type === targetType && o.target_id === targetId)), }, }; }); }, }));