From 2c2846386fadd69925cb31f0cca61fd67cd3780f Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sat, 8 Apr 2023 16:14:00 +0100 Subject: [PATCH] feat: further auxiliary method implementations --- src/Client.ts | 13 ++++ src/classes/Channel.ts | 132 ++++++++++++++++++++++++++++++++++++ src/classes/Message.ts | 113 ++++++++++++++++++++++++++++++ src/classes/ServerMember.ts | 22 +++++- src/classes/User.ts | 17 ++--- 5 files changed, 285 insertions(+), 12 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index 96eb3589..55fbac3b 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -265,6 +265,19 @@ export class Client { return `${autumn.url}/${tag}/${_id}${query}`; } + + /** + * Proxy a file through January. + * @param url URL to proxy + * @returns Proxied media URL + */ + proxyFile(url: string): string | undefined { + if (this.configuration?.features.january.enabled) { + return `${ + this.configuration.features.january.url + }/proxy?url=${encodeURIComponent(url)}`; + } + } } export type FileArgs = [ diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index 58b2a415..1f8129da 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -6,6 +6,8 @@ import type { DataMessageSend, Member, Message, + OptionsMessageSearch, + Override, User, } from "revolt-api"; import { APIRoutes } from "revolt-api/dist/routes"; @@ -95,6 +97,24 @@ export default ( return Channel.#get(this.id).channelType; } + /** + * Absolute pathname to this channel in the client + */ + get path() { + if (this.serverId) { + return `/server/${this.serverId}/channel/${this.id}`; + } else { + return `/channel/${this.id}`; + } + } + + /** + * URL to this channel + */ + get url() { + return client.configuration?.app + this.path; + } + /** * Channel name */ @@ -457,4 +477,116 @@ export default ( ), }; } + + /** + * Search for messages + * @param params Message searching route data + * @returns The messages + */ + async search(params: Omit) { + const messages = (await client.api.post( + `/channels/${this.id as ""}/search`, + params + )) as Message[]; + + return messages.map((message) => + client.Message.new(message._id, message) + ); + } + + /** + * Search for messages including the users that sent them + * @param params Message searching route data + * @returns The messages + */ + async searchWithUsers(params: Omit) { + const data = (await client.api.post(`/channels/${this.id as ""}/search`, { + ...params, + include_users: true, + })) as { messages: Message[]; users: User[]; members?: Member[] }; + + return { + messages: data.messages.map((message) => + client.Message.new(message._id, message) + ), + users: data.users.map((user) => client.User.new(user._id, user)), + members: data.members?.map((member) => + client.ServerMember.new(member._id, member) + ), + }; + } + + async deleteMessages(ids: string[]) { + await client.api.delete(`/channels/${this.id as ""}/messages/bulk`, { + data: { ids }, + }); + } + + /** + * Create an invite to the channel + * @returns Newly created invite code + */ + async createInvite() { + return await client.api.post(`/channels/${this.id as ""}/invites`); + } + + #ackTimeout?: number; + #ackLimit?: number; + + /** + * Mark a channel as read + * @param message Last read message or its ID + * @param skipRateLimiter Whether to skip the internal rate limiter + */ + async ack(message?: Message | string, skipRateLimiter?: boolean) { + const id = + (typeof message === "string" ? message : message?._id) ?? + this.lastMessageId ?? + ulid(); + const performAck = () => { + this.#ackLimit = undefined; + client.api.put(`/channels/${this.id}/ack/${id as ""}`); + }; + + /* TODO: if (!client.options.ackRateLimiter || skipRateLimiter) + return performAck();*/ + + clearTimeout(this.#ackTimeout); + if (this.#ackLimit && +new Date() > this.#ackLimit) { + performAck(); + } + + // We need to use setTimeout here for both Node.js and browser. + this.#ackTimeout = setTimeout(performAck, 5000) as unknown as number; + + if (!this.#ackLimit) { + this.#ackLimit = +new Date() + 15e3; + } + } + + /** + * Set role permissions + * @param role_id Role Id, set to 'default' to affect all users + * @param permissions Permission value + */ + async setPermissions(role_id = "default", permissions: Override) { + return await client.api.put( + `/channels/${this.id as ""}/permissions/${role_id as ""}`, + { permissions } + ); + } + + /** + * Start typing in this channel + */ + startTyping() { + client.events.send({ type: "BeginTyping", channel: this.id }); + } + + /** + * Stop typing in this channel + */ + stopTyping() { + client.events.send({ type: "EndTyping", channel: this.id }); + } }; diff --git a/src/classes/Message.ts b/src/classes/Message.ts index 011db373..13ee37eb 100644 --- a/src/classes/Message.ts +++ b/src/classes/Message.ts @@ -74,6 +74,20 @@ export default ( 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 */ @@ -95,6 +109,23 @@ export default ( 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 */ @@ -178,4 +209,86 @@ export default ( 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) }; + } + } }; diff --git a/src/classes/ServerMember.ts b/src/classes/ServerMember.ts index 0df17b9b..4860d0b4 100644 --- a/src/classes/ServerMember.ts +++ b/src/classes/ServerMember.ts @@ -2,7 +2,7 @@ import { SetStoreFunction } from "solid-js/store"; import type { Member as ApiMember, MemberCompositeKey } from "revolt-api"; -import { Channel, Client, Server } from "../Client"; +import { Channel, Client, FileArgs, Server } from "../Client"; import { StoreCollection } from "../collections/Collection"; import { HydratedServerMember } from "../hydration/serverMember"; import { bitwiseAndEq, calculatePermission } from "../permissions/calculator"; @@ -214,4 +214,24 @@ export default ( 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 + ); + } }; diff --git a/src/classes/User.ts b/src/classes/User.ts index 38456569..fcf3ae6d 100644 --- a/src/classes/User.ts +++ b/src/classes/User.ts @@ -144,24 +144,19 @@ export default ( * URL to the user's avatar */ get avatarURL() { - return this.#generateAvatarURL({ max_side: 256 }); + return ( + client.generateFileURL(this.avatar, { max_side: 256 }) ?? + this.defaultAvatarURL + ); } /** * URL to the user's animated avatar */ get animatedAvatarURL() { - return this.#generateAvatarURL({ max_side: 256 }, true); - } - - /** - * Generate avatar URL - * @param args File args - * @returns URL - */ - #generateAvatarURL(...args: FileArgs) { return ( - client.generateFileURL(this.avatar, ...args) ?? this.defaultAvatarURL + client.generateFileURL(this.avatar, { max_side: 256 }, true) ?? + this.defaultAvatarURL ); }