summary refs log tree commit diff
path: root/rtc/src/util/Permissions.ts
diff options
context:
space:
mode:
Diffstat (limited to 'rtc/src/util/Permissions.ts')
-rw-r--r--rtc/src/util/Permissions.ts262
1 files changed, 262 insertions, 0 deletions
diff --git a/rtc/src/util/Permissions.ts b/rtc/src/util/Permissions.ts
new file mode 100644

index 00000000..445e901f --- /dev/null +++ b/rtc/src/util/Permissions.ts
@@ -0,0 +1,262 @@ +// https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js +// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah +import { MemberDocument, MemberModel } from "../models/Member"; +import { ChannelDocument, ChannelModel } from "../models/Channel"; +import { ChannelPermissionOverwrite } from "../models/Channel"; +import { Role, RoleDocument, RoleModel } from "../models/Role"; +import { BitField } from "./BitField"; +import { GuildDocument, GuildModel } from "../models/Guild"; +// TODO: check role hierarchy permission + +var HTTPError: any; + +try { + HTTPError = require("lambert-server").HTTPError; +} catch (e) { + HTTPError = Error; +} + +export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString; + +type PermissionString = + | "CREATE_INSTANT_INVITE" + | "KICK_MEMBERS" + | "BAN_MEMBERS" + | "ADMINISTRATOR" + | "MANAGE_CHANNELS" + | "MANAGE_GUILD" + | "ADD_REACTIONS" + | "VIEW_AUDIT_LOG" + | "PRIORITY_SPEAKER" + | "STREAM" + | "VIEW_CHANNEL" + | "SEND_MESSAGES" + | "SEND_TTS_MESSAGES" + | "MANAGE_MESSAGES" + | "EMBED_LINKS" + | "ATTACH_FILES" + | "READ_MESSAGE_HISTORY" + | "MENTION_EVERYONE" + | "USE_EXTERNAL_EMOJIS" + | "VIEW_GUILD_INSIGHTS" + | "CONNECT" + | "SPEAK" + | "MUTE_MEMBERS" + | "DEAFEN_MEMBERS" + | "MOVE_MEMBERS" + | "USE_VAD" + | "CHANGE_NICKNAME" + | "MANAGE_NICKNAMES" + | "MANAGE_ROLES" + | "MANAGE_WEBHOOKS" + | "MANAGE_EMOJIS"; + +const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(48); // 16 free custom permission bits, and 16 for discord to add new ones + +export class Permissions extends BitField { + cache: PermissionCache = {}; + + static FLAGS = { + CREATE_INSTANT_INVITE: BigInt(1) << BigInt(0), + KICK_MEMBERS: BigInt(1) << BigInt(1), + BAN_MEMBERS: BigInt(1) << BigInt(2), + ADMINISTRATOR: BigInt(1) << BigInt(3), + MANAGE_CHANNELS: BigInt(1) << BigInt(4), + MANAGE_GUILD: BigInt(1) << BigInt(5), + ADD_REACTIONS: BigInt(1) << BigInt(6), + VIEW_AUDIT_LOG: BigInt(1) << BigInt(7), + PRIORITY_SPEAKER: BigInt(1) << BigInt(8), + STREAM: BigInt(1) << BigInt(9), + VIEW_CHANNEL: BigInt(1) << BigInt(10), + SEND_MESSAGES: BigInt(1) << BigInt(11), + SEND_TTS_MESSAGES: BigInt(1) << BigInt(12), + MANAGE_MESSAGES: BigInt(1) << BigInt(13), + EMBED_LINKS: BigInt(1) << BigInt(14), + ATTACH_FILES: BigInt(1) << BigInt(15), + READ_MESSAGE_HISTORY: BigInt(1) << BigInt(16), + MENTION_EVERYONE: BigInt(1) << BigInt(17), + USE_EXTERNAL_EMOJIS: BigInt(1) << BigInt(18), + VIEW_GUILD_INSIGHTS: BigInt(1) << BigInt(19), + CONNECT: BigInt(1) << BigInt(20), + SPEAK: BigInt(1) << BigInt(21), + MUTE_MEMBERS: BigInt(1) << BigInt(22), + DEAFEN_MEMBERS: BigInt(1) << BigInt(23), + MOVE_MEMBERS: BigInt(1) << BigInt(24), + USE_VAD: BigInt(1) << BigInt(25), + CHANGE_NICKNAME: BigInt(1) << BigInt(26), + MANAGE_NICKNAMES: BigInt(1) << BigInt(27), + MANAGE_ROLES: BigInt(1) << BigInt(28), + MANAGE_WEBHOOKS: BigInt(1) << BigInt(29), + MANAGE_EMOJIS: BigInt(1) << BigInt(30), + /** + * CUSTOM PERMISSIONS ideas: + * - allow user to dm members + * - allow user to pin messages (without MANAGE_MESSAGES) + * - allow user to publish messages (without MANAGE_MESSAGES) + */ + // CUSTOM_PERMISSION: BigInt(1) << BigInt(0) + CUSTOM_PERMISSION_OFFSET + }; + + any(permission: PermissionResolvable, checkAdmin = true) { + return (checkAdmin && super.any(Permissions.FLAGS.ADMINISTRATOR)) || super.any(permission); + } + + /** + * Checks whether the bitfield has a permission, or multiple permissions. + */ + has(permission: PermissionResolvable, checkAdmin = true) { + return (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || super.has(permission); + } + + /** + * Checks whether the bitfield has a permission, or multiple permissions, but throws an Error if user fails to match auth criteria. + */ + hasThrow(permission: PermissionResolvable) { + if (this.has(permission) && this.has("VIEW_CHANNEL")) return true; + // @ts-ignore + throw new HTTPError(`You are missing the following permissions ${permission}`, 403); + } + + overwriteChannel(overwrites: ChannelPermissionOverwrite[]) { + if (!this.cache) throw new Error("permission chache not available"); + overwrites = overwrites.filter((x) => { + if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true; + if (x.type === 1 && x.id == this.cache.user_id) return true; + return false; + }); + return new Permissions(Permissions.channelPermission(overwrites, this.bitfield)); + } + + static channelPermission(overwrites: ChannelPermissionOverwrite[], init?: bigint) { + // TODO: do not deny any permissions if admin + return overwrites.reduce((permission, overwrite) => { + // apply disallowed permission + // * permission: current calculated permission (e.g. 010) + // * deny contains all denied permissions (e.g. 011) + // * allow contains all explicitly allowed permisions (e.g. 100) + return (permission & ~BigInt(overwrite.deny)) | BigInt(overwrite.allow); + // ~ operator inverts deny (e.g. 011 -> 100) + // & operator only allows 1 for both ~deny and permission (e.g. 010 & 100 -> 000) + // | operators adds both together (e.g. 000 + 100 -> 100) + }, init || 0n); + } + + static rolePermission(roles: Role[]) { + // adds all permissions of all roles together (Bit OR) + return roles.reduce((permission, role) => permission | BigInt(role.permissions), 0n); + } + + static finalPermission({ + user, + guild, + channel, + }: { + user: { id: string; roles: string[] }; + guild: { roles: Role[] }; + channel?: { + overwrites?: ChannelPermissionOverwrite[]; + recipient_ids?: string[] | null; + owner_id?: string; + }; + }) { + if (user.id === "0") return new Permissions("ADMINISTRATOR"); // system user id + + let roles = guild.roles.filter((x) => user.roles.includes(x.id)); + let permission = Permissions.rolePermission(roles); + + if (channel?.overwrites) { + let overwrites = channel.overwrites.filter((x) => { + if (x.type === 0 && user.roles.includes(x.id)) return true; + if (x.type === 1 && x.id == user.id) return true; + return false; + }); + permission = Permissions.channelPermission(overwrites, permission); + } + + if (channel?.recipient_ids) { + if (channel?.owner_id === user.id) return new Permissions("ADMINISTRATOR"); + if (channel.recipient_ids.includes(user.id)) { + // Default dm permissions + return new Permissions([ + "VIEW_CHANNEL", + "SEND_MESSAGES", + "STREAM", + "ADD_REACTIONS", + "EMBED_LINKS", + "ATTACH_FILES", + "READ_MESSAGE_HISTORY", + "MENTION_EVERYONE", + "USE_EXTERNAL_EMOJIS", + "CONNECT", + "SPEAK", + "MANAGE_CHANNELS", + ]); + } + + return new Permissions(); + } + + return new Permissions(permission); + } +} + +export type PermissionCache = { + channel?: ChannelDocument | null; + member?: MemberDocument | null; + guild?: GuildDocument | null; + roles?: RoleDocument[] | null; + user_id?: string; +}; + +export async function getPermission( + user_id?: string, + guild_id?: string, + channel_id?: string, + cache: PermissionCache = {} +) { + var { channel, member, guild, roles } = cache; + + if (!user_id) throw new HTTPError("User not found"); + + if (channel_id && !channel) { + channel = await ChannelModel.findOne( + { id: channel_id }, + { permission_overwrites: true, recipient_ids: true, owner_id: true, guild_id: true } + ).exec(); + if (!channel) throw new HTTPError("Channel not found", 404); + if (channel.guild_id) guild_id = channel.guild_id; + } + + if (guild_id) { + if (!guild) guild = await GuildModel.findOne({ id: guild_id }, { owner_id: true }).exec(); + if (!guild) throw new HTTPError("Guild not found"); + if (guild.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR); + + if (!member) member = await MemberModel.findOne({ guild_id, id: user_id }, "roles").exec(); + if (!member) throw new HTTPError("Member not found"); + + if (!roles) roles = await RoleModel.find({ guild_id, id: { $in: member.roles } }).exec(); + } + + var permission = Permissions.finalPermission({ + user: { + id: user_id, + roles: member?.roles || [], + }, + guild: { + roles: roles || [], + }, + channel: { + overwrites: channel?.permission_overwrites, + owner_id: channel?.owner_id, + recipient_ids: channel?.recipient_ids, + }, + }); + + const obj = new Permissions(permission); + + // pass cache to permission for possible future getPermission calls + obj.cache = { guild, member, channel, roles, user_id }; + + return obj; +}