refactor: conventional design for classes

This commit is contained in:
Paul Makles
2023-04-08 17:08:02 +01:00
parent 2c2846386f
commit 35b564e350
12 changed files with 1626 additions and 1652 deletions
+12 -43
View File
@@ -3,12 +3,7 @@ import { Accessor, Setter, createSignal } from "solid-js";
import { API, Metadata } from "revolt-api";
import type { DataLogin, RevoltConfig } from "revolt-api";
import channelClassFactory from "./classes/Channel";
import emojiClassFactory from "./classes/Emoji";
import messageClassFactory from "./classes/Message";
import serverClassFactory from "./classes/Server";
import serverMemberClassFactory from "./classes/ServerMember";
import userClassFactory from "./classes/User";
import { User } from "./classes";
import {
ChannelCollection,
EmojiCollection,
@@ -19,28 +14,12 @@ import {
} from "./collections";
import { EventClient, createEventClient } from "./events/client";
// eslint-disable-next-line
type O<T extends (...args: any) => any> = InstanceType<ReturnType<T>>;
export type Channel = O<typeof channelClassFactory>;
export type Emoji = O<typeof emojiClassFactory>;
export type Message = O<typeof messageClassFactory>;
export type Server = O<typeof serverClassFactory>;
export type ServerMember = O<typeof serverMemberClassFactory>;
export type User = O<typeof userClassFactory>;
export type Session = { token: string; user_id: string } | string;
/**
* Revolt.js Client
*/
export class Client {
readonly Channel: ReturnType<typeof channelClassFactory>;
readonly Emoji: ReturnType<typeof emojiClassFactory>;
readonly Message: ReturnType<typeof messageClassFactory>;
readonly User: ReturnType<typeof userClassFactory>;
readonly Server: ReturnType<typeof serverClassFactory>;
readonly ServerMember: ReturnType<typeof serverMemberClassFactory>;
readonly channels;
readonly emojis;
readonly messages;
@@ -74,22 +53,12 @@ export class Client {
this.ready = ready;
this.#setReady = setReady;
this.channels = new ChannelCollection();
this.emojis = new EmojiCollection();
this.messages = new MessageCollection();
this.users = new UserCollection();
this.servers = new ServerCollection();
this.serverMembers = new ServerMemberCollection();
this.Channel = channelClassFactory(this, this.channels as never);
this.Emoji = emojiClassFactory(this, this.emojis as never);
this.Message = messageClassFactory(this, this.messages as never);
this.User = userClassFactory(this, this.users as never);
this.Server = serverClassFactory(this, this.servers as never);
this.ServerMember = serverMemberClassFactory(
this,
this.serverMembers as never
);
this.channels = new ChannelCollection(this);
this.emojis = new EmojiCollection(this);
this.messages = new MessageCollection(this);
this.users = new UserCollection(this);
this.servers = new ServerCollection(this);
this.serverMembers = new ServerMemberCollection(this);
}
/**
@@ -101,7 +70,7 @@ export class Client {
if (event.type === "Ready") {
console.time("load users");
for (const user of event.users) {
const u = new this.User(user._id, user);
const u = this.users.getOrCreate(user._id, user);
if (u.relationship === "User") {
this.user = u;
@@ -110,22 +79,22 @@ export class Client {
console.timeEnd("load users");
console.time("load servers");
for (const server of event.servers) {
new this.Server(server._id, server);
this.servers.getOrCreate(server._id, server);
}
console.timeEnd("load servers");
console.time("load memberships");
for (const member of event.members) {
new this.ServerMember(member._id, member);
this.serverMembers.getOrCreate(member._id, member);
}
console.timeEnd("load memberships");
console.time("load channels");
for (const channel of event.channels) {
new this.Channel(channel._id, channel);
this.channels.getOrCreate(channel._id, channel);
}
console.timeEnd("load channels");
console.time("load emojis");
for (const emoji of event.emojis) {
new this.Emoji(emoji._id, emoji);
this.emojis.getOrCreate(emoji._id, emoji);
}
console.timeEnd("load emojis");
+491 -511
View File
File diff suppressed because it is too large Load Diff
+54 -93
View File
@@ -1,105 +1,66 @@
import { SetStoreFunction } from "solid-js/store";
import type { Emoji as ApiEmoji } from "revolt-api";
import { decodeTime } from "ulid";
import { Client } from "../Client";
import { StoreCollection } from "../collections/Collection";
import { HydratedEmoji } from "../hydration/emoji";
import { EmojiCollection } from "../collections";
/**
* Emoji Class
*/
export class Emoji {
readonly collection: EmojiCollection;
readonly id: string;
export default (
client: Client,
collection: StoreCollection<unknown, unknown>
) =>
/**
* Emoji Class
* Construct Emoji
* @param collection Collection
* @param id Emoji Id
*/
class Emoji {
static #collection: StoreCollection<
InstanceType<typeof this>,
HydratedEmoji
>;
static #set: SetStoreFunction<Record<string, HydratedEmoji>>;
static #get: (id: string) => HydratedEmoji;
constructor(collection: EmojiCollection, id: string, data?: ApiEmoji) {
this.collection = collection;
this.id = id;
}
static {
Emoji.#collection = collection as never;
Emoji.#set = collection.updateUnderlyingObject as never;
Emoji.#get = collection.getUnderlyingObject as never;
}
/**
* Time when this emoji was created
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Fetch emoji by ID
* @param id ID
* @returns Emoji
*/
static async fetch(id: string): Promise<Emoji | undefined> {
const emoji = Emoji.#collection.get(id);
if (emoji) return emoji;
/**
* Information about the parent of this emoji
*/
get parent() {
return this.collection.getUnderlyingObject(this.id).parent;
}
const data = await client.api.get(`/custom/emoji/${id as ""}`);
return new Emoji(id, data);
}
/**
* Creator of the emoji
*/
get creator() {
return this.collection.client.users.get(
this.collection.getUnderlyingObject(this.id).creatorId
);
}
readonly id: string;
/**
* Name
*/
get name() {
return this.collection.getUnderlyingObject(this.id).name;
}
/**
* Construct Emoji
* @param id Emoji Id
*/
constructor(id: string, data?: ApiEmoji) {
this.id = id;
Emoji.#collection.create(id, "emoji", this, data);
}
/**
* Whether the emoji is animated
*/
get animated() {
return this.collection.getUnderlyingObject(this.id).animated;
}
/**
* Get or create
* @param id Id
* @param data Data
*/
static new(id: string, data?: ApiEmoji) {
return client.emojis.get(id) ?? new Emoji(id, data);
}
/**
* Time when this emoji was created
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Information about the parent of this emoji
*/
get parent() {
return Emoji.#get(this.id).parent;
}
/**
* Creator of the emoji
*/
get creator() {
return client.users.get(Emoji.#get(this.id).creatorId);
}
/**
* Name
*/
get name() {
return Emoji.#get(this.id).name;
}
/**
* Whether the emoji is animated
*/
get animated() {
return Emoji.#get(this.id).animated;
}
/**
* Whether the emoji is marked as mature
*/
get mature() {
return Emoji.#get(this.id).nsfw;
}
};
/**
* Whether the emoji is marked as mature
*/
get mature() {
return this.collection.getUnderlyingObject(this.id).nsfw;
}
}
+243 -287
View File
@@ -1,294 +1,250 @@
import { SetStoreFunction } from "solid-js/store";
import type { Message as ApiMessage } from "revolt-api";
import { decodeTime } from "ulid";
import { Client } from "../Client";
import { StoreCollection } from "../collections/Collection";
import { HydratedMessage } from "../hydration/message";
import { MessageCollection } from "../collections";
/**
* Message Class
*/
export class Message {
readonly collection: MessageCollection;
readonly id: string;
export default (
client: Client,
collection: StoreCollection<unknown, unknown>
) =>
/**
* Message Class
* Construct Message
* @param collection Collection
* @param id Message Id
*/
class Message {
static #collection: StoreCollection<
InstanceType<typeof this>,
HydratedMessage
>;
static #set: SetStoreFunction<Record<string, HydratedMessage>>;
static #get: (id: string) => HydratedMessage;
constructor(collection: MessageCollection, id: string) {
this.collection = collection;
this.id = id;
}
static {
Message.#collection = collection as never;
Message.#set = collection.updateUnderlyingObject as never;
Message.#get = collection.getUnderlyingObject as never;
/**
* Time when this message was posted
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Absolute pathname to this message in the client
*/
get path() {
return `${this.channel!.path}/${this.id}`;
}
/**
* URL to this message
*/
get url() {
return this.collection.client.configuration?.app + this.path;
}
/**
* Nonce value
*/
get nonce() {
return this.collection.getUnderlyingObject(this.id).nonce;
}
/**
* Id of channel this message was sent in
*/
get channelId() {
return this.collection.getUnderlyingObject(this.id).channelId;
}
/**
* Channel this message was sent in
*/
get channel() {
return this.collection.client.channels.get(
this.collection.getUnderlyingObject(this.id).channelId
);
}
/**
* Server this message was sent in
*/
get server() {
return this.channel!.server;
}
/**
* Member this message was sent by
*/
get member() {
return this.collection.client.serverMembers.getByKey({
server: this.channel!.serverId,
user: this.authorId!,
});
}
/**
* Id of user this message was sent by
*/
get authorId() {
return this.collection.getUnderlyingObject(this.id).authorId;
}
/**
* User this message was sent by
*/
get author() {
return this.collection.client.users.get(
this.collection.getUnderlyingObject(this.id).authorId!
);
}
/**
* Content
*/
get content() {
return this.collection.getUnderlyingObject(this.id).content;
}
/**
* System message content
*/
get systemMessage() {
return this.collection.getUnderlyingObject(this.id).systemMessage;
}
/**
* Attachments
*/
get attachments() {
return this.collection.getUnderlyingObject(this.id).attachments;
}
/**
* Time at which this message was edited
*/
get editedAt() {
return this.collection.getUnderlyingObject(this.id).editedAt;
}
/**
* Embeds
*/
get embeds() {
return this.collection.getUnderlyingObject(this.id).embeds;
}
/**
* IDs of users this message mentions
*/
get mentionIds() {
return this.collection.getUnderlyingObject(this.id).mentionIds;
}
/**
* IDs of messages this message replies to
*/
get replyIds() {
return this.collection.getUnderlyingObject(this.id).replyIds;
}
/**
* Reactions
*/
get reactions() {
return this.collection.getUnderlyingObject(this.id).reactions;
}
/**
* Interactions
*/
get interactions() {
return this.collection.getUnderlyingObject(this.id).interactions;
}
/**
* Masquerade
*/
get masquerade() {
return this.collection.getUnderlyingObject(this.id).masquerade;
}
/**
* Get the username for this message
*/
get username() {
return (
this.masquerade?.name ?? this.member?.nickname ?? this.author?.username
);
}
/**
* Get the role colour for this message
*/
get roleColour() {
return this.masquerade?.colour ?? this.member?.roleColour;
}
/**
* Get the avatar URL for this message
*/
get avatarURL() {
return (
this.masqueradeAvatarURL ??
this.member?.avatarURL ??
this.author?.avatarURL
);
}
/**
* Get the animated avatar URL for this message
*/
get animatedAvatarURL() {
return (
this.masqueradeAvatarURL ??
(this.member
? this.member?.animatedAvatarURL
: this.author?.animatedAvatarURL)
);
}
/**
* Avatar URL from the masquerade
*/
get masqueradeAvatarURL() {
const avatar = this.masquerade?.avatar;
return avatar ? this.collection.client.proxyFile(avatar) : undefined;
}
/**
* Populated system message
*/
get populatedSystemMessage() {
const system = this.systemMessage;
if (!system) return { type: "none" };
const { type } = system;
const get = (id: string) => this.collection.client.users.get(id);
switch (system.type) {
case "text":
return system;
case "user_added":
return { type, user: get(system.id), by: get(system.by) };
case "user_remove":
return { type, user: get(system.id), by: get(system.by) };
case "user_joined":
return { type, user: get(system.id) };
case "user_left":
return { type, user: get(system.id) };
case "user_kicked":
return { type, user: get(system.id) };
case "user_banned":
return { type, user: get(system.id) };
case "channel_renamed":
return { type, name: system.name, by: get(system.by) };
case "channel_description_changed":
return { type, by: get(system.by) };
case "channel_icon_changed":
return { type, by: get(system.by) };
case "channel_ownership_changed":
return { type, from: get(system.from), to: get(system.to) };
}
/**
* Fetch message by ID
* @param channelId Channel ID
* @param messageId Message ID
* @returns Message
*/
static async fetch(
channelId: string,
messageId: string
): Promise<Message | undefined> {
const message = Message.#collection.get(messageId);
if (message) return message;
const data = await client.api.get(
`/channels/${channelId as ""}/messages/${messageId as ""}`
);
return new Message(messageId, data);
}
readonly id: string;
/**
* Construct Message
* @param id Message Id
*/
constructor(id: string, data?: ApiMessage) {
this.id = id;
Message.#collection.create(id, "message", this, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
static new(id: string, data?: ApiMessage) {
return client.messages.get(id) ?? new Message(id, data);
}
/**
* Time when this message was posted
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Absolute pathname to this message in the client
*/
get path() {
return `${this.channel!.path}/${this.id}`;
}
/**
* URL to this message
*/
get url() {
return client.configuration?.app + this.path;
}
/**
* Nonce value
*/
get nonce() {
return Message.#get(this.id).nonce;
}
/**
* Id of channel this message was sent in
*/
get channelId() {
return Message.#get(this.id).channelId;
}
/**
* Channel this message was sent in
*/
get channel() {
return client.channels.get(Message.#get(this.id).channelId);
}
/**
* Server this message was sent in
*/
get server() {
return this.channel!.server;
}
/**
* Member this message was sent by
*/
get member() {
return client.serverMembers.getByKey({
server: this.channel!.serverId,
user: this.authorId!,
});
}
/**
* Id of user this message was sent by
*/
get authorId() {
return Message.#get(this.id).authorId;
}
/**
* User this message was sent by
*/
get author() {
return client.users.get(Message.#get(this.id).authorId!);
}
/**
* Content
*/
get content() {
return Message.#get(this.id).content;
}
/**
* System message content
*/
get systemMessage() {
return Message.#get(this.id).systemMessage;
}
/**
* Attachments
*/
get attachments() {
return Message.#get(this.id).attachments;
}
/**
* Time at which this message was edited
*/
get editedAt() {
return Message.#get(this.id).editedAt;
}
/**
* Embeds
*/
get embeds() {
return Message.#get(this.id).embeds;
}
/**
* IDs of users this message mentions
*/
get mentionIds() {
return Message.#get(this.id).mentionIds;
}
/**
* IDs of messages this message replies to
*/
get replyIds() {
return Message.#get(this.id).replyIds;
}
/**
* Reactions
*/
get reactions() {
return Message.#get(this.id).reactions;
}
/**
* Interactions
*/
get interactions() {
return Message.#get(this.id).interactions;
}
/**
* Masquerade
*/
get masquerade() {
return Message.#get(this.id).masquerade;
}
/**
* Get the username for this message
*/
get username() {
return (
this.masquerade?.name ?? this.member?.nickname ?? this.author?.username
);
}
/**
* Get the role colour for this message
*/
get roleColour() {
return this.masquerade?.colour ?? this.member?.roleColour;
}
/**
* Get the avatar URL for this message
*/
get avatarURL() {
return (
this.masqueradeAvatarURL ??
this.member?.avatarURL ??
this.author?.avatarURL
);
}
/**
* Get the animated avatar URL for this message
*/
get animatedAvatarURL() {
return (
this.masqueradeAvatarURL ??
(this.member
? this.member?.animatedAvatarURL
: this.author?.animatedAvatarURL)
);
}
/**
* Avatar URL from the masquerade
*/
get masqueradeAvatarURL() {
const avatar = this.masquerade?.avatar;
return avatar ? client.proxyFile(avatar) : undefined;
}
/**
* Populated system message
*/
get populatedSystemMessage() {
const system = this.systemMessage;
if (!system) return { type: "none" };
const { type } = system;
const get = (id: string) => client.users.get(id);
switch (system.type) {
case "text":
return system;
case "user_added":
return { type, user: get(system.id), by: get(system.by) };
case "user_remove":
return { type, user: get(system.id), by: get(system.by) };
case "user_joined":
return { type, user: get(system.id) };
case "user_left":
return { type, user: get(system.id) };
case "user_kicked":
return { type, user: get(system.id) };
case "user_banned":
return { type, user: get(system.id) };
case "channel_renamed":
return { type, name: system.name, by: get(system.by) };
case "channel_description_changed":
return { type, by: get(system.by) };
case "channel_icon_changed":
return { type, by: get(system.by) };
case "channel_ownership_changed":
return { type, from: get(system.from), to: get(system.to) };
}
}
};
}
}
+253 -288
View File
@@ -1,326 +1,291 @@
import { SetStoreFunction } from "solid-js/store";
import type { Server as ApiServer, Category } from "revolt-api";
import { decodeTime } from "ulid";
import { Channel, Client } from "../Client";
import { StoreCollection } from "../collections/Collection";
import { HydratedServer } from "../hydration/server";
import { ServerCollection } from "../collections";
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
import { Permission } from "../permissions/definitions";
export default (
client: Client,
collection: StoreCollection<unknown, unknown>
) =>
import { Channel } from "./Channel";
/**
* Server Class
*/
export class Server {
readonly collection: ServerCollection;
readonly id: string;
/**
* Server Class
* Construct Server
* @param collection Collection
* @param id Id
*/
class Server {
static #collection: StoreCollection<
InstanceType<typeof this>,
HydratedServer
>;
static #set: SetStoreFunction<Record<string, HydratedServer>>;
static #get: (id: string) => HydratedServer;
constructor(collection: ServerCollection, id: string) {
this.collection = collection;
this.id = id;
}
static {
Server.#collection = collection as never;
Server.#set = collection.updateUnderlyingObject as never;
Server.#get = collection.getUnderlyingObject as never;
}
/**
* Time when this server was created
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Fetch server by ID
* @param id ID
* @returns Server
*/
static async fetch(id: string): Promise<Server | undefined> {
const server = Server.#collection.get(id);
if (server) return server;
/**
* Owner's user ID
*/
get ownerId() {
return this.collection.getUnderlyingObject(this.id).ownerId;
}
const data = await client.api.get(`/servers/${id as ""}`);
return new Server(id, data);
}
/**
* Owner
*/
get owner() {
return this.collection.client.users.get(
this.collection.getUnderlyingObject(this.id).ownerId
);
}
readonly id: string;
/**
* Name
*/
get name() {
return this.collection.getUnderlyingObject(this.id).name;
}
/**
* Construct
* @param id Id
* @param data Data
*/
constructor(id: string, data?: ApiServer) {
this.id = id;
Server.#collection.create(id, "server", this, data);
}
/**
* Description
*/
get description() {
return this.collection.getUnderlyingObject(this.id).description;
}
/**
* Get or create
* @param id Id
* @param data Data
*/
static new(id: string, data?: ApiServer) {
return client.servers.get(id) ?? new Server(id, data);
}
/**
* Icon
*/
get icon() {
return this.collection.getUnderlyingObject(this.id).icon;
}
/**
* Time when this server was created
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Banner
*/
get banner() {
return this.collection.getUnderlyingObject(this.id).banner;
}
/**
* Owner's user ID
*/
get ownerId() {
return Server.#get(this.id).ownerId;
}
/**
* Channel IDs
*/
get channelIds() {
return this.collection.getUnderlyingObject(this.id).channelIds;
}
/**
* Owner
*/
get owner() {
return client.users.get(Server.#get(this.id).ownerId);
}
/**
* Channels
*/
get channels() {
return [...this.collection.getUnderlyingObject(this.id).channelIds.values()]
.map((id) => this.collection.client.channels.get(id)!)
.filter((x) => x);
}
/**
* Name
*/
get name() {
return Server.#get(this.id).name;
}
/**
* Categories
*/
get categories() {
return this.collection.getUnderlyingObject(this.id).categories;
}
/**
* Description
*/
get description() {
return Server.#get(this.id).description;
}
/**
* System message channels
*/
get systemMessages() {
return this.collection.getUnderlyingObject(this.id).systemMessages;
}
/**
* Icon
*/
get icon() {
return Server.#get(this.id).icon;
}
/**
* Roles
*/
get roles() {
return this.collection.getUnderlyingObject(this.id).roles;
}
/**
* Banner
*/
get banner() {
return Server.#get(this.id).banner;
}
/**
* Default permissions
*/
get defaultPermissions() {
return this.collection.getUnderlyingObject(this.id).defaultPermissions;
}
/**
* Channel IDs
*/
get channelIds() {
return Server.#get(this.id).channelIds;
}
/**
* Server flags
*/
get flags() {
return this.collection.getUnderlyingObject(this.id).flags;
}
/**
* Channels
*/
get channels() {
return [...Server.#get(this.id).channelIds.values()]
.map((id) => client.channels.get(id)!)
.filter((x) => x);
}
/**
* Whether analytics are enabled for this server
*/
get analytics() {
return this.collection.getUnderlyingObject(this.id).analytics;
}
/**
* Categories
*/
get categories() {
return Server.#get(this.id).categories;
}
/**
* Whether this server is publicly discoverable
*/
get discoverable() {
return this.collection.getUnderlyingObject(this.id).discoverable;
}
/**
* System message channels
*/
get systemMessages() {
return Server.#get(this.id).systemMessages;
}
/**
* Whether this server is marked as mature
*/
get mature() {
return this.collection.getUnderlyingObject(this.id).nsfw;
}
/**
* Roles
*/
get roles() {
return Server.#get(this.id).roles;
}
/**
* Get an array of ordered categories with their respective channels.
* Uncategorised channels are returned in `id="default"` category.
*/
get orderedChannels(): (Omit<Category, "channels"> & {
channels: Channel[];
})[] {
const uncategorised = new Set(this.channels.map((channel) => channel.id));
/**
* Default permissions
*/
get defaultPermissions() {
return Server.#get(this.id).defaultPermissions;
}
const elements = [];
let defaultCategory;
/**
* Server flags
*/
get flags() {
return Server.#get(this.id).flags;
}
/**
* Whether analytics are enabled for this server
*/
get analytics() {
return Server.#get(this.id).analytics;
}
/**
* Whether this server is publicly discoverable
*/
get discoverable() {
return Server.#get(this.id).discoverable;
}
/**
* Whether this server is marked as mature
*/
get mature() {
return Server.#get(this.id).nsfw;
}
/**
* Get an array of ordered categories with their respective channels.
* Uncategorised channels are returned in `id="default"` category.
*/
get orderedChannels(): (Omit<Category, "channels"> & {
channels: Channel[];
})[] {
const uncategorised = new Set(this.channels.map((channel) => channel.id));
const elements = [];
let defaultCategory;
const categories = this.categories;
if (categories) {
for (const category of categories) {
const channels = [];
for (const key of category.channels) {
if (uncategorised.delete(key)) {
channels.push(client.channels.get(key)!);
}
const categories = this.categories;
if (categories) {
for (const category of categories) {
const channels = [];
for (const key of category.channels) {
if (uncategorised.delete(key)) {
channels.push(this.collection.client.channels.get(key)!);
}
const cat = {
...category,
channels,
};
if (cat.id === "default") {
if (channels.length === 0) continue;
defaultCategory = cat;
}
elements.push(cat);
}
}
if (uncategorised.size > 0) {
const channels = [...uncategorised].map(
(key) => client.channels.get(key)!
);
const cat = {
...category,
channels,
};
if (defaultCategory) {
defaultCategory.channels = [...defaultCategory.channels, ...channels];
} else {
elements.unshift({
id: "default",
title: "Default",
channels,
});
if (cat.id === "default") {
if (channels.length === 0) continue;
defaultCategory = cat;
}
elements.push(cat);
}
return elements;
}
/**
* Default channel for this server
*/
get defaultChannel(): Channel | undefined {
return this.orderedChannels.find((cat) => cat.channels.length)
?.channels[0];
}
/**
* Get an ordered array of roles with their IDs attached.
* The highest ranking roles will be first followed by lower
* ranking roles. This is dictated by the "rank" property
* which is smaller for higher priority roles.
*/
get orderedRoles() {
const roles = this.roles;
return roles
? [...roles.entries()]
.map(([id, role]) => ({ id, ...role }))
.sort((a, b) => (a.rank || 0) - (b.rank || 0))
: [];
}
get unread() {
return false;
}
get mentions() {
return [];
}
/**
* URL to the server's icon
*/
get iconURL() {
return client.generateFileURL(this.icon, { max_side: 256 });
}
/**
* URL to the server's animated icon
*/
get animatedIconURL() {
return client.generateFileURL(this.icon, { max_side: 256 }, true);
}
/**
* URL to the server's banner
*/
get bannerURL() {
return client.generateFileURL(this.banner, {
max_side: 256,
});
}
/**
* Own member object for this server
*/
get member() {
return client.serverMembers.getByKey({
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])
if (uncategorised.size > 0) {
const channels = [...uncategorised].map(
(key) => this.collection.client.channels.get(key)!
);
if (defaultCategory) {
defaultCategory.channels = [...defaultCategory.channels, ...channels];
} else {
elements.unshift({
id: "default",
title: "Default",
channels,
});
}
}
};
return elements;
}
/**
* Default channel for this server
*/
get defaultChannel(): Channel | undefined {
return this.orderedChannels.find((cat) => cat.channels.length)?.channels[0];
}
/**
* Get an ordered array of roles with their IDs attached.
* The highest ranking roles will be first followed by lower
* ranking roles. This is dictated by the "rank" property
* which is smaller for higher priority roles.
*/
get orderedRoles() {
const roles = this.roles;
return roles
? [...roles.entries()]
.map(([id, role]) => ({ id, ...role }))
.sort((a, b) => (a.rank || 0) - (b.rank || 0))
: [];
}
get unread() {
return false;
}
get mentions() {
return [];
}
/**
* URL to the server's icon
*/
get iconURL() {
return this.collection.client.generateFileURL(this.icon, { max_side: 256 });
}
/**
* URL to the server's animated icon
*/
get animatedIconURL() {
return this.collection.client.generateFileURL(
this.icon,
{ max_side: 256 },
true
);
}
/**
* URL to the server's banner
*/
get bannerURL() {
return this.collection.client.generateFileURL(this.banner, {
max_side: 256,
});
}
/**
* Own member object for this server
*/
get member() {
return this.collection.client.serverMembers.getByKey({
server: this.id,
user: this.collection.client.user!.id,
});
}
/**
* Permission the currently authenticated user has against this server
*/
get permission() {
return calculatePermission(this.collection.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])
);
}
}
+175 -213
View File
@@ -1,13 +1,12 @@
import { SetStoreFunction } from "solid-js/store";
import type { MemberCompositeKey } from "revolt-api";
import type { Member as ApiMember, MemberCompositeKey } from "revolt-api";
import { Channel, Client, FileArgs, Server } from "../Client";
import { StoreCollection } from "../collections/Collection";
import { HydratedServerMember } from "../hydration/serverMember";
import { ServerMemberCollection } from "../collections";
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
import { Permission } from "../permissions/definitions";
import { Channel } from "./Channel";
import { Server } from "./Server";
/**
* Deterministic conversion of member composite key to string ID
* @param key Key
@@ -17,221 +16,184 @@ function key(key: MemberCompositeKey) {
return key.server + key.user;
}
export default (
client: Client,
collection: StoreCollection<unknown, unknown>
) =>
/**
* Server Member Class
*/
export class ServerMember {
readonly collection: ServerMemberCollection;
readonly id: MemberCompositeKey;
/**
* Server Member Class
* Construct Server Member
* @param collection Collection
* @param id Id
*/
class ServerMember {
static #collection: StoreCollection<
InstanceType<typeof this>,
HydratedServerMember
>;
static #set: SetStoreFunction<Record<string, HydratedServerMember>>;
static #get: (id: string) => HydratedServerMember;
constructor(collection: ServerMemberCollection, id: MemberCompositeKey) {
this.collection = collection;
this.id = id;
}
static {
ServerMember.#collection = collection as never;
ServerMember.#set = collection.updateUnderlyingObject as never;
ServerMember.#get = collection.getUnderlyingObject as never;
/**
* Server this member belongs to
*/
get server() {
return this.collection.client.servers.get(this.id.server);
}
/**
* User corresponding to this member
*/
get user() {
return this.collection.client.users.get(this.id.user);
}
/**
* When this user joined the server
*/
get joinedAt() {
return this.collection.getUnderlyingObject(key(this.id)).joinedAt;
}
/**
* Nickname
*/
get nickname() {
return this.collection.getUnderlyingObject(key(this.id)).nickname;
}
/**
* Avatar
*/
get avatar() {
return this.collection.getUnderlyingObject(key(this.id)).avatar;
}
/**
* List of role IDs
*/
get roles() {
return this.collection.getUnderlyingObject(key(this.id)).roles;
}
/**
* Time at which timeout expires
*/
get timeout() {
return this.collection.getUnderlyingObject(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;
}
/**
* Fetch member by ID
* @param id Member ID
* @returns Member
*/
static async fetch(
id: MemberCompositeKey
): Promise<ServerMember | undefined> {
const channel = ServerMember.#collection.get(key(id));
if (channel) return channel;
const data = await client.api.get(
`/servers/${id.server as ""}/members/${id.user as ""}`
);
return new ServerMember(id, data);
const roles = this.orderedRoles;
if (roles.length > 0) {
return roles[roles.length - 1].rank!;
} else {
return Infinity;
}
}
readonly id: MemberCompositeKey;
/**
* 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(this.collection.client, target, {
member: this,
});
}
/**
* Construct
* @param id Id
* @param data Data
*/
constructor(id: MemberCompositeKey, data?: ApiMember) {
this.id = id;
ServerMember.#collection.create(key(id), "serverMember", this, data);
}
/**
* 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])
);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
static new(id: MemberCompositeKey, data?: ApiMember) {
return client.serverMembers.get(key(id)) ?? new ServerMember(id, data);
}
/**
* 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;
}
/**
* Server this member belongs to
*/
get server() {
return client.servers.get(this.id.server);
}
/**
* URL to the member's avatar
*/
get avatarURL() {
return (
this.collection.client.generateFileURL(this.avatar, { max_side: 256 }) ??
this.user?.avatarURL
);
}
/**
* User corresponding to this member
*/
get user() {
return client.users.get(this.id.user);
}
/**
* When this user joined the server
*/
get joinedAt() {
return ServerMember.#get(key(this.id)).joinedAt;
}
/**
* Nickname
*/
get nickname() {
return ServerMember.#get(key(this.id)).nickname;
}
/**
* Avatar
*/
get avatar() {
return ServerMember.#get(key(this.id)).avatar;
}
/**
* List of role IDs
*/
get roles() {
return ServerMember.#get(key(this.id)).roles;
}
/**
* Time at which timeout expires
*/
get timeout() {
return ServerMember.#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;
}
/**
* URL to the member's avatar
*/
get avatarURL() {
return (
client.generateFileURL(this.avatar, { max_side: 256 }) ??
this.user?.avatarURL
);
}
/**
* URL to the member's animated avatar
*/
get animatedAvatarURL() {
return (
client.generateFileURL(this.avatar, { max_side: 256 }, true) ??
this.user?.animatedAvatarURL
);
}
};
/**
* URL to the member's animated avatar
*/
get animatedAvatarURL() {
return (
this.collection.client.generateFileURL(
this.avatar,
{ max_side: 256 },
true
) ?? this.user?.animatedAvatarURL
);
}
}
+149 -189
View File
@@ -1,201 +1,161 @@
import { SetStoreFunction } from "solid-js/store";
import type { User as ApiUser } from "revolt-api";
import { decodeTime } from "ulid";
import { Client, FileArgs } from "../Client";
import { StoreCollection } from "../collections/Collection";
import { HydratedUser } from "../hydration/user";
import { UserCollection } from "../collections";
import { U32_MAX, UserPermission } from "../permissions/definitions";
export default (
client: Client,
collection: StoreCollection<unknown, unknown>
) =>
/**
* User Class
*/
export class User {
readonly collection: UserCollection;
readonly id: string;
/**
* User Class
* Construct User
* @param collection Collection
* @param id Id
*/
class User {
static #collection: StoreCollection<
InstanceType<typeof this>,
HydratedUser
>;
static #set: SetStoreFunction<Record<string, HydratedUser>>;
static #get: (id: string) => HydratedUser;
constructor(collection: UserCollection, id: string) {
this.collection = collection;
this.id = id;
}
static {
User.#collection = collection as never;
User.#set = collection.updateUnderlyingObject as never;
User.#get = collection.getUnderlyingObject as never;
/**
* Time when this user created their account
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Username
*/
get username() {
return this.collection.getUnderlyingObject(this.id).username;
}
/**
* Avatar
*/
get avatar() {
return this.collection.getUnderlyingObject(this.id).avatar;
}
/**
* Badges
*/
get badges() {
return this.collection.getUnderlyingObject(this.id).badges;
}
/**
* User Status
*/
get status() {
return this.collection.getUnderlyingObject(this.id).status;
}
/**
* Relationship with user
*/
get relationship() {
return this.collection.getUnderlyingObject(this.id).relationship;
}
/**
* Whether the user is online
*/
get online() {
return this.collection.getUnderlyingObject(this.id).online;
}
/**
* Whether the user is privileged
*/
get privileged() {
return this.collection.getUnderlyingObject(this.id).privileged;
}
/**
* Flags
*/
get flags() {
return this.collection.getUnderlyingObject(this.id).flags;
}
/**
* Bot information
*/
get bot() {
return this.collection.getUnderlyingObject(this.id).bot;
}
/**
* URL to the user's default avatar
*/
get defaultAvatarURL() {
return `${this.collection.client.baseURL}/users/${this.id}/default_avatar`;
}
/**
* URL to the user's avatar
*/
get avatarURL() {
return (
this.collection.client.generateFileURL(this.avatar, { max_side: 256 }) ??
this.defaultAvatarURL
);
}
/**
* URL to the user's animated avatar
*/
get animatedAvatarURL() {
return (
this.collection.client.generateFileURL(
this.avatar,
{ max_side: 256 },
true
) ?? 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;
}
/**
* Fetch user by ID
* @param id ID
* @returns User
*/
static async fetch(id: string): Promise<User | undefined> {
const user = this.#collection.get(id);
if (user) return user;
const data = await client.api.get(`/users/${id as ""}`);
return new User(id, data);
}
readonly id: string;
/**
* Construct
* @param id Id
* @param data Data
*/
constructor(id: string, data?: ApiUser) {
this.id = id;
User.#collection.create(id, "user", this, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
static new(id: string, data?: ApiUser) {
return client.users.get(id) ?? new User(id, data);
}
/**
* Time when this user created their account
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Username
*/
get username() {
return User.#get(this.id).username;
}
/**
* Avatar
*/
get avatar() {
return User.#get(this.id).avatar;
}
/**
* Badges
*/
get badges() {
return User.#get(this.id).badges;
}
/**
* User Status
*/
get status() {
return User.#get(this.id).status;
}
/**
* Relationship with user
*/
get relationship() {
return User.#get(this.id).relationship;
}
/**
* Whether the user is online
*/
get online() {
return User.#get(this.id).online;
}
/**
* Whether the user is privileged
*/
get privileged() {
return User.#get(this.id).privileged;
}
/**
* Flags
*/
get flags() {
return User.#get(this.id).flags;
}
/**
* Bot information
*/
get bot() {
return User.#get(this.id).bot;
}
/**
* URL to the user's default avatar
*/
get defaultAvatarURL() {
return `${client.baseURL}/users/${this.id}/default_avatar`;
}
/**
* URL to the user's avatar
*/
get avatarURL() {
return (
client.generateFileURL(this.avatar, { max_side: 256 }) ??
this.defaultAvatarURL
);
}
/**
* URL to the user's animated avatar
*/
get animatedAvatarURL() {
return (
client.generateFileURL(this.avatar, { max_side: 256 }, true) ??
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 (
this.collection.client.channels
.toList()
.find(
(channel) =>
(channel.type === "Group" || channel.type === "DirectMessage") &&
channel.recipientIds?.includes(this.collection.client.user!.id)
) ||
this.collection.client.serverMembers
.toList()
.find((member) => member.id.user === this.collection.client.user!.id)
) {
if (this.collection.client.user?.bot || this.bot) {
permissions |= UserPermission.SendMessage;
}
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;
permissions |= UserPermission.Access | UserPermission.ViewProfile;
}
};
return permissions;
}
}
+6
View File
@@ -0,0 +1,6 @@
export { Channel } from "./Channel";
export { Emoji } from "./Emoji";
export { Message } from "./Message";
export { Server } from "./Server";
export { ServerMember } from "./ServerMember";
export { User } from "./User";
+233 -17
View File
@@ -1,31 +1,247 @@
import { MemberCompositeKey } from "revolt-api";
import type * as API from "revolt-api";
import { Channel, Emoji, Message, Server, ServerMember, User } from "..";
import { HydratedChannel } from "../hydration/channel";
import { HydratedEmoji } from "../hydration/emoji";
import { HydratedMessage } from "../hydration/message";
import { HydratedServer } from "../hydration/server";
import { HydratedServerMember } from "../hydration/serverMember";
import { HydratedUser } from "../hydration/user";
import { Client } from "..";
import {
Channel,
Emoji,
Message,
Server,
ServerMember,
User,
} from "../classes";
import {
HydratedChannel,
HydratedEmoji,
HydratedMessage,
HydratedServer,
HydratedServerMember,
HydratedUser,
} from "../hydration";
import { StoreCollection } from "./Collection";
export class ChannelCollection extends StoreCollection<
class ClassCollection<T, V> extends StoreCollection<T, V> {
readonly client: Client;
constructor(client: Client) {
super();
this.client = client;
}
}
export class ChannelCollection extends ClassCollection<
Channel,
HydratedChannel
> {}
export class EmojiCollection extends StoreCollection<Emoji, HydratedEmoji> {}
export class MessageCollection extends StoreCollection<
> {
/**
* Fetch channel by ID
* @param id Id
* @returns Channel
*/
async fetch(id: string): Promise<Channel | undefined> {
const channel = this.get(id);
if (channel) return channel;
const data = await this.client.api.get(`/channels/${id as ""}`);
return this.getOrCreate(data._id, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
getOrCreate(id: string, data: API.Channel) {
if (this.has(id)) {
return this.get(id)!;
} else {
const instance = new Channel(this, id);
this.create(id, "channel", instance, data);
return instance;
}
}
}
export class EmojiCollection extends ClassCollection<Emoji, HydratedEmoji> {
/**
* Fetch emoji by ID
* @param id Id
* @returns Emoji
*/
async fetch(id: string): Promise<Emoji | undefined> {
const emoji = this.get(id);
if (emoji) return emoji;
const data = await this.client.api.get(`/custom/emoji/${id as ""}`);
return this.getOrCreate(data._id, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
getOrCreate(id: string, data: API.Emoji) {
if (this.has(id)) {
return this.get(id)!;
} else {
const instance = new Emoji(this, id);
this.create(id, "emoji", instance, data);
return instance;
}
}
}
export class MessageCollection extends ClassCollection<
Message,
HydratedMessage
> {}
export class ServerCollection extends StoreCollection<Server, HydratedServer> {}
export class UserCollection extends StoreCollection<User, HydratedUser> {}
export class ServerMemberCollection extends StoreCollection<
> {
/**
* Fetch message by Id
* @param channelId Channel Id
* @param messageId Message Id
* @returns Message
*/
async fetch(
channelId: string,
messageId: string
): Promise<Message | undefined> {
const message = this.get(messageId);
if (message) return message;
const data = await this.client.api.get(
`/channels/${channelId as ""}/messages/${messageId as ""}`
);
return this.getOrCreate(data._id, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
getOrCreate(id: string, data: API.Message) {
if (this.has(id)) {
return this.get(id)!;
} else {
const instance = new Message(this, id);
this.create(id, "message", instance, data);
return instance;
}
}
}
export class ServerCollection extends ClassCollection<Server, HydratedServer> {
/**
* Fetch server by ID
* @param id Id
* @returns Server
*/
async fetch(id: string): Promise<Server | undefined> {
const server = this.get(id);
if (server) return server;
const data = await this.client.api.get(`/servers/${id as ""}`);
return this.getOrCreate(data._id, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
getOrCreate(id: string, data: API.Server) {
if (this.has(id)) {
return this.get(id)!;
} else {
const instance = new Server(this, id);
this.create(id, "server", instance, data);
return instance;
}
}
}
export class UserCollection extends ClassCollection<User, HydratedUser> {
/**
* Fetch user by ID
* @param id Id
* @returns User
*/
async fetch(id: string): Promise<User | undefined> {
const user = this.get(id);
if (user) return user;
const data = await this.client.api.get(`/users/${id as ""}`);
return this.getOrCreate(data._id, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
getOrCreate(id: string, data: API.User) {
if (this.has(id)) {
return this.get(id)!;
} else {
const instance = new User(this, id);
this.create(id, "user", instance, data);
return instance;
}
}
}
export class ServerMemberCollection extends ClassCollection<
ServerMember,
HydratedServerMember
> {
getByKey(id: MemberCompositeKey) {
/**
* Check if member exists by composite key
* @param id Id
* @returns Whether it exists
*/
hasByKey(id: API.MemberCompositeKey) {
return super.has(id.server + id.user);
}
/**
* Get member by composite key
* @param id Id
* @returns Member
*/
getByKey(id: API.MemberCompositeKey) {
return super.get(id.server + id.user);
}
/**
* Fetch server member by Id
* @param serverId Server Id
* @param userId User Id
* @returns Message
*/
async fetch(
serverId: string,
userId: string
): Promise<ServerMember | undefined> {
const member = this.get(userId);
if (member) return member;
const data = await this.client.api.get(
`/servers/${serverId as ""}/members/${userId as ""}`
);
return this.getOrCreate(data._id, data);
}
/**
* Get or create
* @param id Id
* @param data Data
*/
getOrCreate(id: API.MemberCompositeKey, data: API.Member) {
if (this.hasByKey(id)) {
return this.getByKey(id)!;
} else {
const instance = new ServerMember(this, id);
this.create(id.server + id.user, "serverMember", instance, data);
return instance;
}
}
}
+7
View File
@@ -5,6 +5,13 @@ import { serverHydration } from "./server";
import { serverMemberHydration } from "./serverMember";
import { userHydration } from "./user";
export type { HydratedChannel } from "./channel";
export type { HydratedEmoji } from "./emoji";
export type { HydratedMessage } from "./message";
export type { HydratedServer } from "./server";
export type { HydratedServerMember } from "./serverMember";
export type { HydratedUser } from "./user";
/**
* Functions to map from one object to another
*/
+2 -10
View File
@@ -1,14 +1,6 @@
export { Client } from "./Client";
export type { Session } from "./Client";
export type {
Emoji,
Channel,
Server,
ServerMember,
User,
Session,
Message,
} from "./Client";
export * from "./classes";
export * from "./lib/regex";
export * as API from "revolt-api";
+1 -1
View File
@@ -40,7 +40,7 @@ export function calculatePermission(
return Permission.GrantAllSafe;
}
if (target instanceof client.Server) {
if (target instanceof Server) {
// 1. Check if owner.
if (target.owner === user?.id) {
return Permission.GrantAllSafe;