spacebar/util/util/Permissions.ts
Flam3rboy f711a0411c 🚧 typeorm
2021-08-21 16:47:22 +02:00

263 lines
8.2 KiB
TypeScript

// 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_AND_STICKERS";
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_AND_STICKERS: 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;
}