mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-21 18:02:32 +01:00
add some permission utils
This commit is contained in:
parent
761130c2c6
commit
10e312ae5a
@ -30,6 +30,7 @@
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"framer-motion": "^10.16.1",
|
||||
"missing-native-js-functions": "^1.4.3",
|
||||
"mobx": "^6.9.0",
|
||||
"mobx-react-lite": "^3.4.3",
|
||||
"react": "^18.2.0",
|
||||
|
@ -59,6 +59,9 @@ dependencies:
|
||||
framer-motion:
|
||||
specifier: ^10.16.1
|
||||
version: 10.16.1(react-dom@18.2.0)(react@18.2.0)
|
||||
missing-native-js-functions:
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.3
|
||||
mobx:
|
||||
specifier: ^6.9.0
|
||||
version: 6.9.0
|
||||
@ -7885,6 +7888,10 @@ packages:
|
||||
/minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
/missing-native-js-functions@1.4.3:
|
||||
resolution: {integrity: sha512-p+vFgEiNlS8bpJbK3cCJjKlBH7YsYRfQG/q+Lhu4j3kSGPjRMOTTaeWKA4/ipVmptLbOZMMqIdIsKOdKCtwVPw==}
|
||||
dev: false
|
||||
|
||||
/mixin-deep@1.3.2:
|
||||
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -13,16 +13,6 @@ const Wrapper = styled(Container)`
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
function Test() {
|
||||
return (
|
||||
<>
|
||||
<ChannelSidebar />
|
||||
<Chat />
|
||||
<MemberList />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelPage() {
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
|
||||
@ -30,7 +20,9 @@ function ChannelPage() {
|
||||
<Wrapper>
|
||||
{contextMenu.visible && <ContextMenu {...contextMenu} />}
|
||||
<GuildSidebar />
|
||||
<Test />
|
||||
<ChannelSidebar />
|
||||
<Chat />
|
||||
<MemberList />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
@ -300,6 +300,9 @@ export default class GatewayConnectionStore {
|
||||
return;
|
||||
}
|
||||
|
||||
// dont reconnect on "going away"
|
||||
if (code === 1001) return;
|
||||
|
||||
this.logger.debug(
|
||||
`Websocket closed with code ${code}; Will reconnect in ${(RECONNECT_TIMEOUT / 1000).toFixed(2)} seconds.`,
|
||||
);
|
||||
|
@ -16,9 +16,11 @@ import type {
|
||||
import { ChannelType, Routes } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import Logger from "../../utils/Logger";
|
||||
import { Permissions } from "../../utils/Permissions";
|
||||
import { APIError } from "../../utils/interfaces/api";
|
||||
import AppStore from "../AppStore";
|
||||
import MessageStore from "../MessageStore";
|
||||
import Guild from "./Guild";
|
||||
|
||||
export default class Channel {
|
||||
private readonly logger: Logger = new Logger("Channel");
|
||||
@ -230,4 +232,32 @@ export default class Channel {
|
||||
this.type === ChannelType.DM
|
||||
);
|
||||
}
|
||||
|
||||
getPermission(userId: string, guild: Guild) {
|
||||
const member = guild.members.get(userId);
|
||||
let recipient_ids = this.recipients?.map((x) => x.id);
|
||||
if (!recipient_ids?.length) recipient_ids = undefined;
|
||||
|
||||
const permission = Permissions.finalPermission({
|
||||
user: {
|
||||
id: userId,
|
||||
roles: member?.roles.map((x) => x.id) || [],
|
||||
},
|
||||
guild: {
|
||||
roles: member?.roles || [],
|
||||
},
|
||||
channel: {
|
||||
overwrites: this.permissionOverwrites,
|
||||
owner_id: this.ownerId,
|
||||
recipient_ids,
|
||||
},
|
||||
});
|
||||
|
||||
const obj = new Permissions(permission);
|
||||
|
||||
// pass cache to permission for possible future getPermission calls
|
||||
obj.cache = { guild, member, channel: this, roles: member?.roles, user_id: userId };
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Snowflake } from "@spacebarchat/spacebar-api-types/globals";
|
||||
import { APIUser, CDNRoutes, DefaultUserAvatarAssets, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { Permissions } from "../../utils/Permissions";
|
||||
import REST from "../../utils/REST";
|
||||
import Channel from "./Channel";
|
||||
import Guild from "./Guild";
|
||||
|
||||
export default class User {
|
||||
id: Snowflake;
|
||||
@ -62,4 +65,32 @@ export default class User {
|
||||
if (this.avatar) return REST.makeCDNUrl(CDNRoutes.userAvatar(this.id, this.avatar, ImageFormat.PNG));
|
||||
else return this.defaultAvatarUrl;
|
||||
}
|
||||
|
||||
getPermission(guild: Guild, channel: Channel) {
|
||||
const member = guild.members.get(this.id);
|
||||
let recipient_ids = channel?.recipients?.map((x) => x.id);
|
||||
if (!recipient_ids?.length) recipient_ids = undefined;
|
||||
|
||||
const permission = Permissions.finalPermission({
|
||||
user: {
|
||||
id: this.id,
|
||||
roles: member?.roles.map((x) => x.id) || [],
|
||||
},
|
||||
guild: {
|
||||
roles: member?.roles || [],
|
||||
},
|
||||
channel: {
|
||||
overwrites: channel.permissionOverwrites,
|
||||
owner_id: channel?.ownerId,
|
||||
recipient_ids,
|
||||
},
|
||||
});
|
||||
|
||||
const obj = new Permissions(permission);
|
||||
|
||||
// pass cache to permission for possible future getPermission calls
|
||||
obj.cache = { guild, member, channel, roles: member?.roles, user_id: this.id };
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
158
src/utils/BitField.ts
Normal file
158
src/utils/BitField.ts
Normal file
@ -0,0 +1,158 @@
|
||||
"use strict";
|
||||
|
||||
// https://github.com/discordjs/discord.js/blob/master/src/util/BitField.js
|
||||
// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
|
||||
// @fc-license-skip
|
||||
|
||||
export type BitFieldResolvable = number | bigint | BitField | string | BitFieldResolvable[];
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a bitfield.
|
||||
*/
|
||||
export class BitField {
|
||||
public bitfield = BigInt(0);
|
||||
|
||||
public static FLAGS: Record<string, bigint> = {};
|
||||
|
||||
constructor(bits: BitFieldResolvable = 0) {
|
||||
this.bitfield = BitField.resolve.call(this, bits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a bit, or any of multiple bits.
|
||||
*/
|
||||
any(bit: BitFieldResolvable): boolean {
|
||||
return (this.bitfield & BitField.resolve.call(this, bit)) !== BigInt(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this bitfield equals another
|
||||
*/
|
||||
equals(bit: BitFieldResolvable): boolean {
|
||||
return this.bitfield === BitField.resolve.call(this, bit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a bit, or multiple bits.
|
||||
*/
|
||||
has(bit: BitFieldResolvable): boolean {
|
||||
if (Array.isArray(bit)) return bit.every((p) => this.has(p));
|
||||
const BIT = BitField.resolve.call(this, bit);
|
||||
return (this.bitfield & BIT) === BIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all given bits that are missing from the bitfield.
|
||||
*/
|
||||
missing(bits: BitFieldResolvable) {
|
||||
if (!Array.isArray(bits)) bits = new BitField(bits).toArray();
|
||||
return bits.filter((p) => !this.has(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Freezes these bits, making them immutable.
|
||||
*/
|
||||
freeze(): Readonly<BitField> {
|
||||
return Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bits to these ones.
|
||||
* @param {...BitFieldResolvable} [bits] Bits to add
|
||||
* @returns {BitField} These bits or new BitField if the instance is frozen.
|
||||
*/
|
||||
add(...bits: BitFieldResolvable[]): BitField {
|
||||
let total = BigInt(0);
|
||||
for (const bit of bits) {
|
||||
total |= BitField.resolve.call(this, bit);
|
||||
}
|
||||
if (Object.isFrozen(this)) return new BitField(this.bitfield | total);
|
||||
this.bitfield |= total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes bits from these.
|
||||
* @param {...BitFieldResolvable} [bits] Bits to remove
|
||||
*/
|
||||
remove(...bits: BitFieldResolvable[]) {
|
||||
let total = BigInt(0);
|
||||
for (const bit of bits) {
|
||||
total |= BitField.resolve.call(this, bit);
|
||||
}
|
||||
if (Object.isFrozen(this)) return new BitField(this.bitfield & ~total);
|
||||
this.bitfield &= ~total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object mapping field names to a {@link boolean} indicating whether the
|
||||
* bit is available.
|
||||
* @param {...*} hasParams Additional parameters for the has method, if any
|
||||
*/
|
||||
serialize() {
|
||||
const serialized: Record<string, boolean> = {};
|
||||
for (const [flag, bit] of Object.entries(BitField.FLAGS)) serialized[flag] = this.has(bit);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link Array} of bitfield names based on the bits available.
|
||||
*/
|
||||
toArray(): string[] {
|
||||
return Object.keys(BitField.FLAGS).filter((bit) => this.has(bit));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.bitfield;
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.bitfield;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
yield* this.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a bitfield. This can be:
|
||||
* * A bit number (this can be a number literal or a value taken from {@link BitField.FLAGS})
|
||||
* * An instance of BitField
|
||||
* * An Array of BitFieldResolvable
|
||||
* @typedef {number|BitField|BitFieldResolvable[]} BitFieldResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves bitfields to their numeric form.
|
||||
* @param {BitFieldResolvable} [bit=0] - bit(s) to resolve
|
||||
* @returns {number}
|
||||
*/
|
||||
static resolve(bit: BitFieldResolvable = BigInt(0)): bigint {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const FLAGS = this.FLAGS || this.constructor?.FLAGS;
|
||||
|
||||
if (typeof bit === "string") {
|
||||
if (typeof FLAGS[bit] !== "undefined") return FLAGS[bit];
|
||||
else bit = BigInt(bit);
|
||||
}
|
||||
|
||||
if ((typeof bit === "number" || typeof bit === "bigint") && bit >= BigInt(0)) return BigInt(bit);
|
||||
|
||||
if (bit instanceof BitField) return bit.bitfield;
|
||||
|
||||
if (Array.isArray(bit)) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const resolve = this.constructor?.resolve || this.resolve;
|
||||
return bit.map((p) => resolve.call(this, p)).reduce((prev, p) => BigInt(prev) | BigInt(p), BigInt(0));
|
||||
}
|
||||
|
||||
throw new RangeError("BITFIELD_INVALID: " + bit);
|
||||
}
|
||||
}
|
||||
|
||||
export function BitFlag(x: bigint | number) {
|
||||
return BigInt(1) << BigInt(x);
|
||||
}
|
186
src/utils/Permissions.ts
Normal file
186
src/utils/Permissions.ts
Normal file
@ -0,0 +1,186 @@
|
||||
// https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js
|
||||
// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
|
||||
// @fc-license-skip
|
||||
|
||||
import { APIOverwrite } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import "missing-native-js-functions";
|
||||
import Channel from "../stores/objects/Channel";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import GuildMember from "../stores/objects/GuildMember";
|
||||
import Role from "../stores/objects/Role";
|
||||
import { BitField, BitFieldResolvable, BitFlag } from "./BitField";
|
||||
|
||||
export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString;
|
||||
|
||||
type PermissionString = keyof typeof Permissions.FLAGS;
|
||||
|
||||
// BigInt doesn't have a bit limit (https://stackoverflow.com/questions/53335545/whats-the-biggest-bigint-value-in-js-as-per-spec)
|
||||
// const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(64); // 27 permission bits left for discord to add new ones
|
||||
|
||||
export class Permissions extends BitField {
|
||||
cache: PermissionCache = {};
|
||||
|
||||
static FLAGS = {
|
||||
CREATE_INSTANT_INVITE: BitFlag(0),
|
||||
KICK_MEMBERS: BitFlag(1),
|
||||
BAN_MEMBERS: BitFlag(2),
|
||||
ADMINISTRATOR: BitFlag(3),
|
||||
MANAGE_CHANNELS: BitFlag(4),
|
||||
MANAGE_GUILD: BitFlag(5),
|
||||
ADD_REACTIONS: BitFlag(6),
|
||||
VIEW_AUDIT_LOG: BitFlag(7),
|
||||
PRIORITY_SPEAKER: BitFlag(8),
|
||||
STREAM: BitFlag(9),
|
||||
VIEW_CHANNEL: BitFlag(10),
|
||||
SEND_MESSAGES: BitFlag(11),
|
||||
SEND_TTS_MESSAGES: BitFlag(12),
|
||||
MANAGE_MESSAGES: BitFlag(13),
|
||||
EMBED_LINKS: BitFlag(14),
|
||||
ATTACH_FILES: BitFlag(15),
|
||||
READ_MESSAGE_HISTORY: BitFlag(16),
|
||||
MENTION_EVERYONE: BitFlag(17),
|
||||
USE_EXTERNAL_EMOJIS: BitFlag(18),
|
||||
VIEW_GUILD_INSIGHTS: BitFlag(19),
|
||||
CONNECT: BitFlag(20),
|
||||
SPEAK: BitFlag(21),
|
||||
MUTE_MEMBERS: BitFlag(22),
|
||||
DEAFEN_MEMBERS: BitFlag(23),
|
||||
MOVE_MEMBERS: BitFlag(24),
|
||||
USE_VAD: BitFlag(25),
|
||||
CHANGE_NICKNAME: BitFlag(26),
|
||||
MANAGE_NICKNAMES: BitFlag(27),
|
||||
MANAGE_ROLES: BitFlag(28),
|
||||
MANAGE_WEBHOOKS: BitFlag(29),
|
||||
MANAGE_EMOJIS_AND_STICKERS: BitFlag(30),
|
||||
USE_APPLICATION_COMMANDS: BitFlag(31),
|
||||
REQUEST_TO_SPEAK: BitFlag(32),
|
||||
MANAGE_EVENTS: BitFlag(33),
|
||||
MANAGE_THREADS: BitFlag(34),
|
||||
USE_PUBLIC_THREADS: BitFlag(35),
|
||||
USE_PRIVATE_THREADS: BitFlag(36),
|
||||
USE_EXTERNAL_STICKERS: BitFlag(37),
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
|
||||
constructor(bits: BitFieldResolvable = 0) {
|
||||
super(bits);
|
||||
if (this.bitfield & Permissions.FLAGS.ADMINISTRATOR) {
|
||||
this.bitfield = ALL_PERMISSIONS;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
overwriteChannel(overwrites: APIOverwrite[]) {
|
||||
if (!overwrites) return this;
|
||||
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: APIOverwrite[], 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 || BigInt(0));
|
||||
}
|
||||
|
||||
static rolePermission(roles: Role[]) {
|
||||
// adds all permissions of all roles together (Bit OR)
|
||||
return roles.reduce((permission, role) => permission | BigInt(role.permissions), BigInt(0));
|
||||
}
|
||||
|
||||
static finalPermission({
|
||||
user,
|
||||
guild,
|
||||
channel,
|
||||
}: {
|
||||
user: { id: string; roles: string[] };
|
||||
guild: { roles: Role[] };
|
||||
channel?: {
|
||||
overwrites?: APIOverwrite[];
|
||||
recipient_ids?: string[] | null;
|
||||
owner_id?: string;
|
||||
};
|
||||
}) {
|
||||
if (user.id === "0") return new Permissions("ADMINISTRATOR"); // system user id
|
||||
|
||||
const roles = guild.roles.filter((x) => user.roles.includes(x.id));
|
||||
console.log(`Roles`, roles);
|
||||
let permission = Permissions.rolePermission(roles);
|
||||
console.log(`Role permissions`, permission);
|
||||
|
||||
if (channel?.overwrites) {
|
||||
const 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;
|
||||
});
|
||||
console.log(`channel overwrites`, overwrites);
|
||||
permission = Permissions.channelPermission(overwrites, permission);
|
||||
console.log(`channel permission`, 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);
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_PERMISSIONS = Object.values(Permissions.FLAGS).reduce((total, val) => total | val, BigInt(0));
|
||||
|
||||
export type PermissionCache = {
|
||||
channel?: Channel | undefined;
|
||||
member?: GuildMember | undefined;
|
||||
guild?: Guild | undefined;
|
||||
roles?: Role[] | undefined;
|
||||
user_id?: string;
|
||||
};
|
Loading…
Reference in New Issue
Block a user