feat: port permissions calculations

This commit is contained in:
Paul Makles
2023-04-08 13:41:01 +01:00
parent 8ef5806d03
commit e45ff7fcbf
7 changed files with 502 additions and 2 deletions
+1
View File
@@ -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
View File
@@ -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])
);
}
};
+38
View File
@@ -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])
);
}
};
+93 -1
View File
@@ -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;
}
};
+40
View File
@@ -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;
}
};
+160
View File
@@ -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();
}
}
}
}
}
+141
View File
@@ -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;