1
0
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:
Puyodead1 2023-08-31 18:50:26 -04:00
parent 761130c2c6
commit 10e312ae5a
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
8 changed files with 419 additions and 11 deletions

View File

@ -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",

View File

@ -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'}

View File

@ -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>
);
}

View File

@ -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.`,
);

View File

@ -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;
}
}

View File

@ -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
View 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
View 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;
};