Files
javascript-client-sdk/src/classes/Channel.ts
T
2024-12-23 22:01:53 +00:00

744 lines
18 KiB
TypeScript

import { batch } from "solid-js";
import type {
Member as ApiMember,
Message as ApiMessage,
User as ApiUser,
DataEditChannel,
DataMessageSearch,
DataMessageSend,
Override,
} from "revolt-api";
import { APIRoutes } from "revolt-api/dist/routes";
import { decodeTime, ulid } from "ulid";
import { ChannelCollection } from "../collections/index.js";
import { Message } from "../index.js";
import {
bitwiseAndEq,
calculatePermission,
} from "../permissions/calculator.js";
import { Permission } from "../permissions/definitions.js";
/**
* Channel Class
*/
export class Channel {
readonly #collection: ChannelCollection;
readonly id: string;
/**
* Construct Channel
* @param collection Collection
* @param id Channel Id
*/
constructor(collection: ChannelCollection, id: string) {
this.#collection = collection;
this.id = id;
}
/**
* Write to string as a channel mention
* @returns Formatted String
*/
toString() {
return `<#${this.id}>`;
}
/**
* Whether this object exists
*/
get $exists() {
return !!this.#collection.getUnderlyingObject(this.id).id;
}
/**
* Time when this server was created
*/
get createdAt() {
return new Date(decodeTime(this.id));
}
/**
* Channel type
*/
get type() {
return this.#collection.getUnderlyingObject(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 this.#collection.client.configuration?.app + this.path;
}
/**
* Channel name
*/
get name() {
return this.#collection.getUnderlyingObject(this.id).name;
}
/**
* Display name
*/
get displayName() {
return this.type === "SavedMessages"
? this.user?.username
: this.type === "DirectMessage"
? this.recipient?.username
: this.name;
}
/**
* Channel description
*/
get description() {
return this.#collection.getUnderlyingObject(this.id).description;
}
/**
* Channel icon
*/
get icon() {
return this.#collection.getUnderlyingObject(this.id).icon;
}
/**
* Whether the conversation is active
*/
get active() {
return this.#collection.getUnderlyingObject(this.id).active;
}
/**
* User ids of people currently typing in channel
*/
get typingIds() {
return this.#collection.getUnderlyingObject(this.id).typingIds;
}
/**
* Users currently trying in channel
*/
get typing() {
return [...this.typingIds.values()].map(
(id) => this.#collection.client.users.get(id)!
);
}
/**
* User ids of recipients of the group
*/
get recipientIds() {
return this.#collection.getUnderlyingObject(this.id).recipientIds;
}
/**
* Recipients of the group
*/
get recipients() {
return [
...this.#collection.getUnderlyingObject(this.id).recipientIds.values(),
].map((id) => this.#collection.client.users.get(id)!);
}
/**
* Find recipient of this DM
*/
get recipient() {
return this.type === "DirectMessage"
? this.recipients?.find(
(user) => user?.id !== this.#collection.client.user!.id
)
: undefined;
}
/**
* User ID
*/
get userId() {
return this.#collection.getUnderlyingObject(this.id).userId!;
}
/**
* User this channel belongs to
*/
get user() {
return this.#collection.client.users.get(
this.#collection.getUnderlyingObject(this.id).userId!
);
}
/**
* Owner ID
*/
get ownerId() {
return this.#collection.getUnderlyingObject(this.id).ownerId!;
}
/**
* Owner of the group
*/
get owner() {
return this.#collection.client.users.get(
this.#collection.getUnderlyingObject(this.id).ownerId!
);
}
/**
* Server ID
*/
get serverId() {
return this.#collection.getUnderlyingObject(this.id).serverId!;
}
/**
* Server this channel is in
*/
get server() {
return this.#collection.client.servers.get(
this.#collection.getUnderlyingObject(this.id).serverId!
);
}
/**
* Permissions allowed for users in this group
*/
get permissions() {
return this.#collection.getUnderlyingObject(this.id).permissions;
}
/**
* Default permissions for this server channel
*/
get defaultPermissions() {
return this.#collection.getUnderlyingObject(this.id).defaultPermissions;
}
/**
* Role permissions for this server channel
*/
get rolePermissions() {
return this.#collection.getUnderlyingObject(this.id).rolePermissions;
}
/**
* Whether this channel is marked as mature
*/
get mature() {
return this.#collection.getUnderlyingObject(this.id).nsfw;
}
/**
* ID of the last message sent in this channel
*/
get lastMessageId() {
return this.#collection.getUnderlyingObject(this.id).lastMessageId;
}
/**
* Last message sent in this channel
*/
get lastMessage() {
return this.#collection.client.messages.get(this.lastMessageId!);
}
/**
* Time when the last message was sent
*/
get lastMessageAt() {
return this.lastMessageId
? new Date(decodeTime(this.lastMessageId))
: undefined;
}
/**
* Time when the channel was last updated (either created or a message was sent)
*/
get updatedAt() {
return this.lastMessageAt ?? this.createdAt;
}
/**
* Get whether this channel is unread.
*/
get unread() {
if (
!this.lastMessageId ||
this.type === "SavedMessages" ||
this.type === "VoiceChannel" ||
this.#collection.client.options.channelIsMuted(this)
)
return false;
return (
(
this.#collection.client.channelUnreads.get(this.id)?.lastMessageId ??
"0"
).localeCompare(this.lastMessageId) === -1
);
}
/**
* Get mentions in this channel for user.
*/
get mentions() {
if (this.type === "SavedMessages" || this.type === "VoiceChannel")
return undefined;
return this.#collection.client.channelUnreads.get(this.id)
?.messageMentionIds;
}
/**
* URL to the channel icon
*/
get iconURL() {
return this.icon?.createFileURL() ?? this.recipient?.avatarURL;
}
/**
* URL to the animated channel icon
*/
get animatedIconURL() {
return this.icon?.createFileURL(true) ?? this.recipient?.animatedAvatarURL;
}
/**
* Whether this channel may be hidden to some users
*/
get potentiallyRestrictedChannel() {
if (!this.serverId) return false;
return (
bitwiseAndEq(this.defaultPermissions?.d ?? 0, Permission.ViewChannel) ||
!bitwiseAndEq(this.server!.defaultPermissions, Permission.ViewChannel) ||
[...(this.server?.roles.keys() ?? [])].find(
(role) =>
bitwiseAndEq(
this.rolePermissions?.[role]?.d ?? 0,
Permission.ViewChannel
) ||
bitwiseAndEq(
this.server?.roles.get(role)?.permissions.d ?? 0,
Permission.ViewChannel
)
)
);
}
/**
* Permission the currently authenticated user has against this channel
*/
get permission() {
return calculatePermission(this.#collection.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])
);
}
/**
* Check whether we have at least one of the given permissions in a channel
* @param permission Permission Names
* @returns Whether we have one of the permissions
*/
orPermission(...permission: (keyof typeof Permission)[]) {
return (
permission.findIndex((x) =>
bitwiseAndEq(this.permission, Permission[x])
) !== -1
);
}
/**
* Fetch a channel's members.
* @requires `Group`
* @returns An array of the channel's members.
*/
async fetchMembers() {
const members = await this.#collection.client.api.get(
`/channels/${this.id as ""}/members`
);
return batch(() =>
members.map((user) =>
this.#collection.client.users.getOrCreate(user._id, user)
)
);
}
/**
* Fetch a channel's webhooks
* @requires `TextChannel`, `Group`
* @returns Webhooks
*/
async fetchWebhooks() {
const webhooks = await this.#collection.client.api.get(
`/channels/${this.id as ""}/webhooks`
);
return batch(() =>
webhooks.map((webhook) =>
this.#collection.client.channelWebhooks.getOrCreate(webhook.id, webhook)
)
);
}
/**
* Edit a channel
* @param data Changes
*/
async edit(data: DataEditChannel) {
await this.#collection.client.api.patch(`/channels/${this.id as ""}`, data);
}
/**
* Delete or leave a channel
* @param leaveSilently Whether to not send a message on leave
* @requires `DirectMessage`, `Group`, `TextChannel`, `VoiceChannel`
*/
async delete(leaveSilently?: boolean) {
await this.#collection.client.api.delete(`/channels/${this.id as ""}`, {
leave_silently: leaveSilently,
});
if (this.type === "DirectMessage") {
this.#collection.updateUnderlyingObject(this.id, "active", false);
return;
}
this.#collection.delete(this.id);
}
/**
* Add a user to a group
* @param user_id ID of the target user
* @requires `Group`
*/
async addMember(user_id: string) {
return await this.#collection.client.api.put(
`/channels/${this.id as ""}/recipients/${user_id as ""}`
);
}
/**
* Remove a user from a group
* @param user_id ID of the target user
* @requires `Group`
*/
async removeMember(user_id: string) {
return await this.#collection.client.api.delete(
`/channels/${this.id as ""}/recipients/${user_id as ""}`
);
}
/**
* Send a message
* @param data Either the message as a string or message sending route data
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
* @returns Sent message
*/
async sendMessage(
data: string | DataMessageSend,
idempotencyKey: string = ulid()
) {
const msg: DataMessageSend =
typeof data === "string" ? { content: data } : data;
// Mark as silent message
if (msg.content?.startsWith("@silent ")) {
msg.content = msg.content.substring(8);
msg.flags ||= 1;
msg.flags |= 1;
}
const message = await this.#collection.client.api.post(
`/channels/${this.id as ""}/messages`,
msg,
{
headers: {
"Idempotency-Key": idempotencyKey,
},
}
);
return this.#collection.client.messages.getOrCreate(
message._id,
message,
true
);
}
/**
* Fetch a message by its ID
* @param messageId ID of the target message
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
* @returns Message
*/
async fetchMessage(messageId: string) {
const message = await this.#collection.client.api.get(
`/channels/${this.id as ""}/messages/${messageId as ""}`
);
return this.#collection.client.messages.getOrCreate(message._id, message);
}
/**
* Fetch multiple messages from a channel
* @param params Message fetching route data
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
* @returns Messages
*/
async fetchMessages(
params?: Omit<
(APIRoutes & {
method: "get";
path: "/channels/{target}/messages";
})["params"],
"include_users"
>
) {
const messages = (await this.#collection.client.api.get(
`/channels/${this.id as ""}/messages`,
{ ...params }
)) as ApiMessage[];
return messages.map((message) =>
this.#collection.client.messages.getOrCreate(message._id, message)
);
}
/**
* Fetch multiple messages from a channel including the users that sent them
* @param params Message fetching route data
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
* @returns Object including messages and users
*/
async fetchMessagesWithUsers(
params?: Omit<
(APIRoutes & {
method: "get";
path: "/channels/{target}/messages";
})["params"],
"include_users"
>
) {
const data = (await this.#collection.client.api.get(
`/channels/${this.id as ""}/messages`,
{ ...params, include_users: true }
)) as { messages: ApiMessage[]; users: ApiUser[]; members?: ApiMember[] };
return batch(() => ({
messages: data.messages.map((message) =>
this.#collection.client.messages.getOrCreate(message._id, message)
),
users: data.users.map((user) =>
this.#collection.client.users.getOrCreate(user._id, user)
),
members: data.members?.map((member) =>
this.#collection.client.serverMembers.getOrCreate(member._id, member)
),
}));
}
/**
* Search for messages
* @param params Message searching route data
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
* @returns Messages
*/
async search(params: Omit<DataMessageSearch, "include_users">) {
const messages = (await this.#collection.client.api.post(
`/channels/${this.id as ""}/search`,
params
)) as ApiMessage[];
return batch(() =>
messages.map((message) =>
this.#collection.client.messages.getOrCreate(message._id, message)
)
);
}
/**
* Search for messages including the users that sent them
* @param params Message searching route data
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
* @returns Object including messages and users
*/
async searchWithUsers(params: Omit<DataMessageSearch, "include_users">) {
const data = (await this.#collection.client.api.post(
`/channels/${this.id as ""}/search`,
{
...params,
include_users: true,
}
)) as { messages: ApiMessage[]; users: ApiUser[]; members?: ApiMember[] };
return batch(() => ({
messages: data.messages.map((message) =>
this.#collection.client.messages.getOrCreate(message._id, message)
),
users: data.users.map((user) =>
this.#collection.client.users.getOrCreate(user._id, user)
),
members: data.members?.map((member) =>
this.#collection.client.serverMembers.getOrCreate(member._id, member)
),
}));
}
/**
* Delete many messages by their IDs
* @param ids List of message IDs
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
*/
async deleteMessages(ids: string[]) {
await this.#collection.client.api.delete(
`/channels/${this.id as ""}/messages/bulk`,
{
ids,
}
);
}
/**
* Create an invite to the channel
* @requires `TextChannel`, `VoiceChannel`
* @returns Newly created invite code
*/
async createInvite() {
return await this.#collection.client.api.post(
`/channels/${this.id as ""}/invites`
);
}
#ackTimeout?: number;
#ackLimit?: number;
#manuallyMarked?: boolean;
/**
* Mark a channel as read
* @param message Last read message or its ID
* @param skipRateLimiter Whether to skip the internal rate limiter
* @param skipRequest For internal updates only
* @param skipNextMarking For internal usage only
* @requires `SavedMessages`, `DirectMessage`, `Group`, `TextChannel`
*/
async ack(
message?: Message | string,
skipRateLimiter?: boolean,
skipRequest?: boolean,
skipNextMarking?: boolean
) {
if (!message && this.#manuallyMarked) {
this.#manuallyMarked = false;
return;
}
// Skip the next unread marking
else if (skipNextMarking) {
this.#manuallyMarked = true;
}
const lastMessageId =
(typeof message === "string" ? message : message?.id) ??
this.lastMessageId ??
ulid();
const unreads = this.#collection.client.channelUnreads;
const channelUnread = unreads.get(this.id);
if (channelUnread) {
unreads.updateUnderlyingObject(this.id, {
lastMessageId,
});
if (channelUnread.messageMentionIds.size) {
channelUnread.messageMentionIds.clear();
}
}
// Skip request if not needed
if (skipRequest) return;
/**
* Send the actual acknowledgement request
*/
const performAck = () => {
this.#ackLimit = undefined;
this.#collection.client.api.put(
`/channels/${this.id}/ack/${lastMessageId as ""}`
);
};
if (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
* @requires `Group`, `TextChannel`, `VoiceChannel`
*/
async setPermissions(role_id = "default", permissions: Override) {
return await this.#collection.client.api.put(
`/channels/${this.id as ""}/permissions/${role_id as ""}`,
{ permissions }
);
}
/**
* Start typing in this channel
* @requires `DirectMessage`, `Group`, `TextChannel`
*/
startTyping() {
this.#collection.client.events.send({
type: "BeginTyping",
channel: this.id,
});
}
/**
* Stop typing in this channel
* @requires `DirectMessage`, `Group`, `TextChannel`
*/
stopTyping() {
this.#collection.client.events.send({
type: "EndTyping",
channel: this.id,
});
}
}