mirror of
https://github.com/stoatchat/javascript-client-sdk.git
synced 2026-07-19 17:13:31 -04:00
feat: port permissions calculations
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"@solid-primitives/set": "^0.4.3",
|
||||
"eventemitter3": "^5.0.0",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"long": "^5.2.1",
|
||||
"revolt-api": "^0.5.17",
|
||||
"solid-js": "^1.7.2",
|
||||
"ulid": "^2.3.0",
|
||||
|
||||
+29
-1
@@ -2,9 +2,11 @@ import { ReactiveMap } from "@solid-primitives/map";
|
||||
import type { Channel as ApiChannel } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { Client, FileArgs } from "../Client";
|
||||
import { Client } from "../Client";
|
||||
import { hydrate } from "../hydration";
|
||||
import { HydratedChannel } from "../hydration/channel";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
@@ -164,6 +166,13 @@ export default (client: Client) =>
|
||||
return Channel.#storage.get(this.id).active;
|
||||
}
|
||||
|
||||
/**
|
||||
* User Ids of recipients of the group
|
||||
*/
|
||||
get recipientIds() {
|
||||
return Channel.#storage.get(this.id).recipientIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recipients of the group
|
||||
*/
|
||||
@@ -313,4 +322,23 @@ export default (client: Client) =>
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission the currently authenticated user has against this channel
|
||||
*/
|
||||
get permission() {
|
||||
return calculatePermission(client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a given permission in a channel
|
||||
* @param permission Permission Names
|
||||
* @returns Whether we have this permission
|
||||
*/
|
||||
havePermission(...permission: (keyof typeof Permission)[]) {
|
||||
return bitwiseAndEq(
|
||||
this.permission,
|
||||
...permission.map((x) => Permission[x])
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@ import { decodeTime } from "ulid";
|
||||
|
||||
import { Channel, Client } from "../Client";
|
||||
import { HydratedServer } from "../hydration/server";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
@@ -113,6 +115,13 @@ export default (client: Client) =>
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner's user ID
|
||||
*/
|
||||
get ownerId() {
|
||||
return Server.#storage.get(this.id).ownerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner
|
||||
*/
|
||||
@@ -322,4 +331,33 @@ export default (client: Client) =>
|
||||
max_side: 256,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Own member object for this server
|
||||
*/
|
||||
get member() {
|
||||
return client.serverMembers.get({
|
||||
server: this.id,
|
||||
user: client.user!.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission the currently authenticated user has against this server
|
||||
*/
|
||||
get permission() {
|
||||
return calculatePermission(client, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a given permission in a server
|
||||
* @param permission Permission Names
|
||||
* @returns Whether we have this permission
|
||||
*/
|
||||
havePermission(...permission: (keyof typeof Permission)[]) {
|
||||
return bitwiseAndEq(
|
||||
this.permission,
|
||||
...permission.map((x) => Permission[x])
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
import type { Member as ApiMember, MemberCompositeKey } from "revolt-api";
|
||||
|
||||
import { Client } from "../Client";
|
||||
import { Channel, Client, Server } from "../Client";
|
||||
import { HydratedServerMember } from "../hydration/serverMember";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
/**
|
||||
@@ -173,4 +175,94 @@ export default (client: Client) =>
|
||||
get timeout() {
|
||||
return ServerMember.#storage.get(key(this.id)).timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered list of roles for this member, from lowest to highest priority.
|
||||
*/
|
||||
get orderedRoles() {
|
||||
const server = this.server!;
|
||||
return (
|
||||
this.roles
|
||||
?.map((id) => ({
|
||||
id,
|
||||
...server.roles?.get(id),
|
||||
}))
|
||||
.sort((a, b) => b.rank! - a.rank!) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's currently hoisted role.
|
||||
*/
|
||||
get hoistedRole() {
|
||||
const roles = this.orderedRoles.filter((x) => x.hoist);
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's current role colour.
|
||||
*/
|
||||
get roleColour() {
|
||||
const roles = this.orderedRoles.filter((x) => x.colour);
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1].colour;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's ranking.
|
||||
* Smaller values are ranked as higher priotity.
|
||||
*/
|
||||
get ranking() {
|
||||
if (this.id.user === this.server?.ownerId) {
|
||||
return -Infinity;
|
||||
}
|
||||
|
||||
const roles = this.orderedRoles;
|
||||
if (roles.length > 0) {
|
||||
return roles[roles.length - 1].rank!;
|
||||
} else {
|
||||
return Infinity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions that this member has against a certain object
|
||||
* @param target Target object to check permissions against
|
||||
* @returns Permissions that this member has
|
||||
*/
|
||||
getPermissions(target: Server | Channel) {
|
||||
return calculatePermission(client, target, { member: this });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a member has a certain permission against a certain object
|
||||
* @param target Target object to check permissions against
|
||||
* @param permission Permission names to check for
|
||||
* @returns Whether the member has this permission
|
||||
*/
|
||||
hasPermission(
|
||||
target: Server | Channel,
|
||||
...permission: (keyof typeof Permission)[]
|
||||
) {
|
||||
return bitwiseAndEq(
|
||||
this.getPermissions(target),
|
||||
...permission.map((x) => Permission[x])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the target member has a higher rank than this member.
|
||||
* @param target The member to compare against
|
||||
* @returns Whether this member is inferior to the target
|
||||
*/
|
||||
inferiorTo(target: ServerMember) {
|
||||
return target.ranking < this.ranking;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { decodeTime } from "ulid";
|
||||
|
||||
import { Client, FileArgs } from "../Client";
|
||||
import { HydratedUser } from "../hydration/user";
|
||||
import { U32_MAX, UserPermission } from "../permissions/definitions";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
@@ -207,4 +208,43 @@ export default (client: Client) =>
|
||||
client.generateFileURL(this.avatar, ...args) ?? this.defaultAvatarURL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions against this user
|
||||
*/
|
||||
get permission() {
|
||||
let permissions = 0;
|
||||
switch (this.relationship) {
|
||||
case "Friend":
|
||||
case "User":
|
||||
return U32_MAX;
|
||||
case "Blocked":
|
||||
case "BlockedOther":
|
||||
return UserPermission.Access;
|
||||
case "Incoming":
|
||||
case "Outgoing":
|
||||
permissions = UserPermission.Access;
|
||||
}
|
||||
|
||||
if (
|
||||
client.channels
|
||||
.toList()
|
||||
.find(
|
||||
(channel) =>
|
||||
(channel.type === "Group" || channel.type === "DirectMessage") &&
|
||||
channel.recipientIds?.includes(client.user!.id)
|
||||
) ||
|
||||
client.serverMembers
|
||||
.toList()
|
||||
.find((member) => member.id.user === client.user!.id)
|
||||
) {
|
||||
if (client.user?.bot || this.bot) {
|
||||
permissions |= UserPermission.SendMessage;
|
||||
}
|
||||
|
||||
permissions |= UserPermission.Access | UserPermission.ViewProfile;
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import Long from "long";
|
||||
|
||||
import { Channel, Client, Server, ServerMember } from "..";
|
||||
|
||||
import {
|
||||
ALLOW_IN_TIMEOUT,
|
||||
DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
DEFAULT_PERMISSION_VIEW_ONLY,
|
||||
Permission,
|
||||
UserPermission,
|
||||
} from "./definitions";
|
||||
|
||||
/**
|
||||
* Check whether `b` is present in `a`
|
||||
* @param a Input A
|
||||
* @param b Inputs (OR'd together)
|
||||
*/
|
||||
export function bitwiseAndEq(a: number, ...b: number[]) {
|
||||
const value = b.reduce((prev, cur) => prev.or(cur), Long.fromNumber(0));
|
||||
return value.and(a).eq(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate permissions against a given object
|
||||
* @param target Target object to check permissions against
|
||||
* @param options Additional options to use when calculating
|
||||
*/
|
||||
export function calculatePermission(
|
||||
client: Client,
|
||||
target: Channel | Server,
|
||||
options?: {
|
||||
/**
|
||||
* Pretend to be another ServerMember
|
||||
*/
|
||||
member?: ServerMember;
|
||||
}
|
||||
): number {
|
||||
const user = options?.member ? options?.member.user : client.user;
|
||||
if (user?.privileged) {
|
||||
return Permission.GrantAllSafe;
|
||||
}
|
||||
|
||||
if (target instanceof client.servers) {
|
||||
// 1. Check if owner.
|
||||
if (target.owner === user?.id) {
|
||||
return Permission.GrantAllSafe;
|
||||
} else {
|
||||
// 2. Get ServerMember.
|
||||
const member = options?.member ??
|
||||
client.serverMembers.get({
|
||||
user: user!.id,
|
||||
server: target.id,
|
||||
}) ?? { roles: null, timeout: null };
|
||||
|
||||
if (!member) return 0;
|
||||
|
||||
// 3. Apply allows from default_permissions.
|
||||
let perm = Long.fromNumber(target.defaultPermissions);
|
||||
|
||||
// 4. If user has roles, iterate in order.
|
||||
if (member.roles && target.roles) {
|
||||
// 5. Apply allows and denies from roles.
|
||||
const permissions = member.orderedRoles.map(
|
||||
(role) => role.permissions ?? { a: 0, d: 0 }
|
||||
);
|
||||
|
||||
for (const permission of permissions) {
|
||||
perm = perm.or(permission.a).and(Long.fromNumber(permission.d).not());
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Revoke permissions if ServerMember is timed out.
|
||||
if (member.timeout && member.timeout > new Date()) {
|
||||
perm = perm.and(ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
return perm.toNumber();
|
||||
}
|
||||
} else {
|
||||
// 1. Check channel type.
|
||||
switch (target.type) {
|
||||
case "SavedMessages":
|
||||
return Permission.GrantAllSafe;
|
||||
case "DirectMessage": {
|
||||
// 2. Determine user permissions.
|
||||
const user_permissions = target.recipient?.permission || 0;
|
||||
|
||||
// 3. Check if the user can send messages.
|
||||
if (user_permissions & UserPermission.SendMessage) {
|
||||
return DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
} else {
|
||||
return DEFAULT_PERMISSION_VIEW_ONLY;
|
||||
}
|
||||
}
|
||||
case "Group": {
|
||||
// 2. Check if user is owner.
|
||||
if (target.ownerId === user!.id) {
|
||||
return DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
} else {
|
||||
// 3. Pull out group permissions.
|
||||
return target.permissions ?? DEFAULT_PERMISSION_DIRECT_MESSAGE;
|
||||
}
|
||||
}
|
||||
case "TextChannel":
|
||||
case "VoiceChannel": {
|
||||
// 2. Get server.
|
||||
const server = target.server;
|
||||
if (typeof server === "undefined") return 0;
|
||||
|
||||
// 3. If server owner, just grant all permissions.
|
||||
if (server.owner === user?.id) {
|
||||
return Permission.GrantAllSafe;
|
||||
} else {
|
||||
// 4. Get ServerMember.
|
||||
const member = options?.member ??
|
||||
client.serverMembers.get({
|
||||
user: user!.id,
|
||||
server: server.id,
|
||||
}) ?? { roles: null, timeout: null };
|
||||
|
||||
if (!member) return 0;
|
||||
|
||||
// 5. Calculate server base permissions.
|
||||
let perm = Long.fromNumber(
|
||||
calculatePermission(client, server, options)
|
||||
);
|
||||
|
||||
// 6. Apply default allows and denies for channel.
|
||||
if (target.defaultPermissions) {
|
||||
perm = perm
|
||||
.or(target.defaultPermissions.a)
|
||||
.and(Long.fromNumber(target.defaultPermissions.d).not());
|
||||
}
|
||||
|
||||
// 7. If user has roles, iterate in order.
|
||||
if (member.roles && target.rolePermissions && server.roles) {
|
||||
// 5. Apply allows and denies from roles.
|
||||
const roles = member.orderedRoles.map(({ id }) => id);
|
||||
|
||||
for (const id of roles) {
|
||||
const override = target.rolePermissions[id];
|
||||
if (override) {
|
||||
perm = perm
|
||||
.or(override.a)
|
||||
.and(Long.fromNumber(override.d).not());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Revoke permissions if ServerMember is timed out.
|
||||
if (member.timeout && member.timeout > new Date()) {
|
||||
perm = perm.and(ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
return perm.toNumber();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Permission against User
|
||||
*/
|
||||
export const UserPermission = {
|
||||
Access: 1 << 0,
|
||||
ViewProfile: 1 << 1,
|
||||
SendMessage: 1 << 2,
|
||||
Invite: 1 << 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* Permission against Server / Channel
|
||||
*/
|
||||
export const Permission = {
|
||||
// * Generic permissions
|
||||
/// Manage the channel or channels on the server
|
||||
ManageChannel: 2 ** 0,
|
||||
/// Manage the server
|
||||
ManageServer: 2 ** 1,
|
||||
/// Manage permissions on servers or channels
|
||||
ManagePermissions: 2 ** 2,
|
||||
/// Manage roles on server
|
||||
ManageRole: 2 ** 3,
|
||||
/// Manage server customisation (includes emoji)
|
||||
ManageCustomisation: 2 ** 4,
|
||||
|
||||
// % 1 bits reserved
|
||||
|
||||
// * Member permissions
|
||||
/// Kick other members below their ranking
|
||||
KickMembers: 2 ** 6,
|
||||
/// Ban other members below their ranking
|
||||
BanMembers: 2 ** 7,
|
||||
/// Timeout other members below their ranking
|
||||
TimeoutMembers: 2 ** 8,
|
||||
/// Assign roles to members below their ranking
|
||||
AssignRoles: 2 ** 9,
|
||||
/// Change own nickname
|
||||
ChangeNickname: 2 ** 10,
|
||||
/// Change or remove other's nicknames below their ranking
|
||||
ManageNicknames: 2 ** 11,
|
||||
/// Change own avatar
|
||||
ChangeAvatar: 2 ** 12,
|
||||
/// Remove other's avatars below their ranking
|
||||
RemoveAvatars: 2 ** 13,
|
||||
|
||||
// % 7 bits reserved
|
||||
|
||||
// * Channel permissions
|
||||
/// View a channel
|
||||
ViewChannel: 2 ** 20,
|
||||
/// Read a channel's past message history
|
||||
ReadMessageHistory: 2 ** 21,
|
||||
/// Send a message in a channel
|
||||
SendMessage: 2 ** 22,
|
||||
/// Delete messages in a channel
|
||||
ManageMessages: 2 ** 23,
|
||||
/// Manage webhook entries on a channel
|
||||
ManageWebhooks: 2 ** 24,
|
||||
/// Create invites to this channel
|
||||
InviteOthers: 2 ** 25,
|
||||
/// Send embedded content in this channel
|
||||
SendEmbeds: 2 ** 26,
|
||||
/// Send attachments and media in this channel
|
||||
UploadFiles: 2 ** 27,
|
||||
/// Masquerade messages using custom nickname and avatar
|
||||
Masquerade: 2 ** 28,
|
||||
/// React to messages with emoji
|
||||
React: 2 ** 29,
|
||||
|
||||
// * Voice permissions
|
||||
/// Connect to a voice channel
|
||||
Connect: 2 ** 30,
|
||||
/// Speak in a voice call
|
||||
Speak: 2 ** 31,
|
||||
/// Share video in a voice call
|
||||
Video: 2 ** 32,
|
||||
/// Mute other members with lower ranking in a voice call
|
||||
MuteMembers: 2 ** 33,
|
||||
/// Deafen other members with lower ranking in a voice call
|
||||
DeafenMembers: 2 ** 34,
|
||||
/// Move members between voice channels
|
||||
MoveMembers: 2 ** 35,
|
||||
|
||||
// * Misc. permissions
|
||||
// % Bits 36 to 52: free area
|
||||
// % Bits 53 to 64: do not use
|
||||
|
||||
// * Grant all permissions
|
||||
/// Safely grant all permissions
|
||||
GrantAllSafe: 0x000f_ffff_ffff_ffff,
|
||||
};
|
||||
|
||||
/**
|
||||
* Maximum safe value
|
||||
*/
|
||||
export const U32_MAX = 2 ** 32 - 1; // 4294967295
|
||||
|
||||
/**
|
||||
* Permissions allowed for a user while in timeout
|
||||
*/
|
||||
export const ALLOW_IN_TIMEOUT =
|
||||
Permission.ViewChannel + Permission.ReadMessageHistory;
|
||||
|
||||
/**
|
||||
* Default permissions if we can only view
|
||||
*/
|
||||
export const DEFAULT_PERMISSION_VIEW_ONLY =
|
||||
Permission.ViewChannel + Permission.ReadMessageHistory;
|
||||
|
||||
/**
|
||||
* Default base permissions for channels
|
||||
*/
|
||||
export const DEFAULT_PERMISSION =
|
||||
DEFAULT_PERMISSION_VIEW_ONLY +
|
||||
Permission.SendMessage +
|
||||
Permission.InviteOthers +
|
||||
Permission.SendEmbeds +
|
||||
Permission.UploadFiles +
|
||||
Permission.Connect +
|
||||
Permission.Speak;
|
||||
|
||||
/**
|
||||
* Permissions in saved messages channel
|
||||
*/
|
||||
export const DEFAULT_PERMISSION_SAVED_MESSAGES = Permission.GrantAllSafe;
|
||||
|
||||
/**
|
||||
* Permissions in direct message channels / default permissions for group DMs
|
||||
*/
|
||||
export const DEFAULT_PERMISSION_DIRECT_MESSAGE =
|
||||
DEFAULT_PERMISSION + Permission.React + Permission.ManageChannel;
|
||||
|
||||
/**
|
||||
* Permissions in server text / voice channel
|
||||
*/
|
||||
export const DEFAULT_PERMISSION_SERVER =
|
||||
DEFAULT_PERMISSION +
|
||||
Permission.React +
|
||||
Permission.ChangeNickname +
|
||||
Permission.ChangeAvatar;
|
||||
Reference in New Issue
Block a user