From 353a2eb427d70a7b0614e6a6feb24eecef8ef08f Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 12 Apr 2023 11:20:04 +0100 Subject: [PATCH] feat: implement Bot and PublicBot --- src/Client.ts | 5 +- src/classes/Bot.ts | 130 +++++++++++++++++++++++++++++++ src/classes/Channel.ts | 12 ++- src/classes/PublicBot.ts | 47 +++++++++++ src/classes/Server.ts | 10 +-- src/classes/index.ts | 18 +++-- src/collections/BotCollection.ts | 70 +++++++++++++++++ src/collections/index.ts | 1 + src/hydration/bot.ts | 44 +++++++++++ src/hydration/index.ts | 2 + src/hydration/user.ts | 6 +- 11 files changed, 320 insertions(+), 25 deletions(-) create mode 100644 src/classes/Bot.ts create mode 100644 src/classes/PublicBot.ts create mode 100644 src/collections/BotCollection.ts create mode 100644 src/hydration/bot.ts diff --git a/src/Client.ts b/src/Client.ts index 7ff07aba..b36d79a0 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -6,6 +6,7 @@ import type { DataLogin, RevoltConfig } from "revolt-api"; import { Channel, Emoji, Message, Server, ServerMember, User } from "./classes"; import { + BotCollection, ChannelCollection, ChannelUnreadCollection, EmojiCollection, @@ -134,13 +135,14 @@ export type ClientOptions = Partial & { * Revolt.js Clients */ export class Client extends EventEmitter { + readonly bots; readonly channels; readonly channelUnreads; readonly emojis; readonly messages; - readonly users; readonly servers; readonly serverMembers; + readonly users; readonly api: API; readonly options: ClientOptions; @@ -199,6 +201,7 @@ export class Client extends EventEmitter { this.connectionFailureCount = connectionFailureCount; this.#setConnectionFailureCount = setConnectionFailureCount; + this.bots = new BotCollection(this); this.channels = new ChannelCollection(this); this.channelUnreads = new ChannelUnreadCollection(this); this.emojis = new EmojiCollection(this); diff --git a/src/classes/Bot.ts b/src/classes/Bot.ts new file mode 100644 index 00000000..5f06c1a9 --- /dev/null +++ b/src/classes/Bot.ts @@ -0,0 +1,130 @@ +import { DataEditBot } from "revolt-api"; +import { decodeTime } from "ulid"; + +import { BotCollection } from "../collections"; + +/** + * Bot Class + */ +export class Bot { + readonly #collection: BotCollection; + readonly id: string; + + /** + * Construct Bot + * @param collection Collection + * @param id Id + */ + constructor(collection: BotCollection, id: string) { + this.#collection = collection; + this.id = id; + } + + /** + * Convert to string + * @returns String + */ + toString() { + return `<@${this.id}>`; + } + + /** + * Time when this user created their account + */ + get createdAt() { + return new Date(decodeTime(this.id)); + } + + /** + * Corresponding user + */ + get user() { + return this.#collection.client.users.get(this.id); + } + + /** + * Owner's Id + */ + get ownerId() { + return this.#collection.getUnderlyingObject(this.id).ownerId; + } + + /** + * Owner + */ + get owner() { + return this.#collection.client.users.get(this.ownerId); + } + + /** + * Bot Token + */ + get token() { + return this.#collection.getUnderlyingObject(this.id).token; + } + + /** + * Whether this bot can be invited by anyone + */ + get public() { + return this.#collection.getUnderlyingObject(this.id).public; + } + + /** + * Whether this bot has analytics enabled + */ + get analytics() { + return this.#collection.getUnderlyingObject(this.id).analytics; + } + + /** + * Whether this bot shows up on Discover + */ + get discoverable() { + return this.#collection.getUnderlyingObject(this.id).discoverable; + } + + /** + * Interactions URL + */ + get interactionsUrl() { + return this.#collection.getUnderlyingObject(this.id).interactionsUrl; + } + + /** + * Link to terms of service + */ + get termsOfServiceUrl() { + return this.#collection.getUnderlyingObject(this.id).termsOfServiceUrl; + } + + /** + * Link to privacy policy + */ + get privacyPolicyUrl() { + return this.#collection.getUnderlyingObject(this.id).privacyPolicyUrl; + } + + /** + * Bot Flags + */ + get flags() { + return this.#collection.getUnderlyingObject(this.id).flags; + } + + /** + * Edit a bot + * @param data Changes + */ + async edit(data: DataEditBot) { + await this.#collection.client.api.patch(`/bots/${this.id as ""}`, data); + } + + /** + * Delete a bot + */ + async delete() { + await this.#collection.client.api.delete(`/bots/${this.id as ""}`); + this.#collection.delete(this.id); + } +} diff --git a/src/classes/Channel.ts b/src/classes/Channel.ts index c6512157..9986a787 100644 --- a/src/classes/Channel.ts +++ b/src/classes/Channel.ts @@ -358,21 +358,19 @@ export class Channel { /** * Delete or leave a channel * @param leaveSilently Whether to not send a message on leave - * @param noRequest Whether to not send a request * @requires `DirectMessage`, `Group`, `TextChannel`, `VoiceChannel` */ - async delete(leaveSilently?: boolean, noRequest?: boolean) { - if (!noRequest) - await this.#collection.client.api.delete(`/channels/${this.id as ""}`, { - leave_silently: leaveSilently, - }); + 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.client.channels.delete(this.id); + this.#collection.delete(this.id); } /** diff --git a/src/classes/PublicBot.ts b/src/classes/PublicBot.ts new file mode 100644 index 00000000..d705e655 --- /dev/null +++ b/src/classes/PublicBot.ts @@ -0,0 +1,47 @@ +import { API, Channel, Client, File, Server } from ".."; + +/** + * Public Bot Class + */ +export class PublicBot { + #client: Client; + + readonly id: string; + readonly username: string; + readonly avatar?: File; + readonly description?: string; + + /** + * Construct Public Bot + * @param client Client + * @param data Data + */ + constructor(client: Client, data: API.PublicBot) { + this.#client = client; + this.id = data._id; + this.username = data.username; + this.avatar = data.avatar ? new File(client, data.avatar) : undefined; + this.description = data.description!; + } + + /** + * Add the bot to a server + * @param server Server + */ + addToServer(server: Server | string) { + this.#client.api.post(`/bots/${this.id as ""}/invite`, { + server: server instanceof Server ? server.id : server, + }); + } + + /** + * Add the bot to a group + * @param group Group + */ + addToGroup(group: Channel | string) { + // TODO: should use GroupChannel once that is added + this.#client.api.post(`/bots/${this.id as ""}/invite`, { + group: group instanceof Channel ? group.id : group, + }); + } +} diff --git a/src/classes/Server.ts b/src/classes/Server.ts index ca668868..29a0d2ea 100644 --- a/src/classes/Server.ts +++ b/src/classes/Server.ts @@ -338,13 +338,11 @@ export class Server { /** * Delete or leave a server * @param leaveSilently Whether to not send a message on leave - * @param noRequest Whether to not send a request */ - async delete(leaveSilently?: boolean, avoidReq?: boolean) { - if (!avoidReq) - await this.#collection.client.api.delete(`/servers/${this.id as ""}`, { - leave_silently: leaveSilently, - }); + async delete(leaveSilently?: boolean) { + await this.#collection.client.api.delete(`/servers/${this.id as ""}`, { + leave_silently: leaveSilently, + }); this.#collection.delete(this.id); } diff --git a/src/classes/index.ts b/src/classes/index.ts index b57524c5..1725b72d 100644 --- a/src/classes/index.ts +++ b/src/classes/index.ts @@ -1,9 +1,11 @@ -export { Channel } from "./Channel"; -export { Emoji } from "./Emoji"; -export { Message } from "./Message"; -export { Server } from "./Server"; -export { ServerMember } from "./ServerMember"; -export { User } from "./User"; -export { File } from "./File"; -export { ChannelUnread } from "./ChannelUnread"; +export * from "./Bot"; +export * from "./Channel"; +export * from "./ChannelUnread"; +export * from "./Emoji"; +export * from "./File"; +export * from "./Message"; export * from "./MessageEmbed"; +export * from "./PublicBot"; +export * from "./Server"; +export * from "./ServerMember"; +export * from "./User"; diff --git a/src/collections/BotCollection.ts b/src/collections/BotCollection.ts new file mode 100644 index 00000000..698e0c65 --- /dev/null +++ b/src/collections/BotCollection.ts @@ -0,0 +1,70 @@ +import { OwnedBotsResponse } from "revolt-api"; + +import { API, Bot, PublicBot } from ".."; +import { HydratedBot } from "../hydration/bot"; + +import { ClassCollection } from "."; + +export class BotCollection extends ClassCollection { + /** + * Fetch bot by ID + * @param id Id + * @returns Bot + */ + async fetch(id: string): Promise { + const bot = this.get(id); + if (bot) return bot; + const data = await this.client.api.get(`/bots/${id as ""}`); + this.client.users.getOrCreate(data.user._id, data.user); + return this.getOrCreate(data.bot._id, data.bot); + } + + /** + * Fetch owned bots + * @returns List of bots + */ + async fetchOwned(): Promise { + const data = (await this.client.api.get("/bots/@me")) as OwnedBotsResponse; + data.users.forEach((user) => this.client.users.getOrCreate(user._id, user)); + return data.bots.map((bot) => this.getOrCreate(bot._id, bot)); + } + + /** + * Fetch public bot by ID + * @param id Id + * @returns Public Bot + */ + async fetchPublic(id: string): Promise { + const data = await this.client.api.get(`/bots/${id as ""}/invite`); + return new PublicBot(this.client, data); + } + + /** + * Get or create + * @param id Id + * @param data Data + * @returns Bot + */ + getOrCreate(id: string, data: API.Bot) { + if (this.has(id)) { + return this.get(id)!; + } else { + const instance = new Bot(this, id); + this.create(id, "bot", instance, this.client, data); + return instance; + } + } + + /** + * Create a bot + * @param name Bot name + * @returns The newly-created bot + */ + async createBot(name: string) { + const bot = await this.client.api.post(`/bots/create`, { + name, + }); + + return this.getOrCreate(bot._id, bot); + } +} diff --git a/src/collections/index.ts b/src/collections/index.ts index 57dee4d5..a236c85c 100644 --- a/src/collections/index.ts +++ b/src/collections/index.ts @@ -1,5 +1,6 @@ export * from "./Collection"; +export { BotCollection } from "./BotCollection"; export { ChannelCollection } from "./ChannelCollection"; export { ChannelUnreadCollection } from "./ChannelUnreadCollection"; export { EmojiCollection } from "./EmojiCollection"; diff --git a/src/hydration/bot.ts b/src/hydration/bot.ts new file mode 100644 index 00000000..12809253 --- /dev/null +++ b/src/hydration/bot.ts @@ -0,0 +1,44 @@ +import { Bot as ApiBot } from "revolt-api"; + +import { Hydrate } from "."; + +export type HydratedBot = { + id: string; + ownerId: string; + token: string; + public: boolean; + analytics: boolean; + discoverable: boolean; + interactionsUrl?: string; + termsOfServiceUrl?: string; + privacyPolicyUrl?: string; + flags: BotFlags; +}; + +export const botHydration: Hydrate = { + keyMapping: { + _id: "id", + owner: "ownerId", + interactions_url: "interactionsUrl", + terms_of_service_url: "termsOfServiceUrl", + privacy_policy_url: "privacyPolicyUrl", + }, + functions: { + id: (bot) => bot._id, + ownerId: (bot) => bot.owner, + token: (bot) => bot.token, + public: (bot) => bot.public, + analytics: (bot) => bot.analytics!, + discoverable: (bot) => bot.discoverable!, + interactionsUrl: (bot) => bot.interactions_url!, + termsOfServiceUrl: (bot) => bot.terms_of_service_url!, + privacyPolicyUrl: (bot) => bot.privacy_policy_url!, + flags: (bot) => bot.flags!, + }, + initialHydration: () => ({}), +}; + +/** + * Flags attributed to users + */ +export enum BotFlags {} diff --git a/src/hydration/index.ts b/src/hydration/index.ts index 4cb1fdd7..9d8173f9 100644 --- a/src/hydration/index.ts +++ b/src/hydration/index.ts @@ -1,3 +1,4 @@ +import { botHydration } from "./bot"; import { channelHydration } from "./channel"; import { channelUnreadHydration } from "./channelUnread"; import { emojiHydration } from "./emoji"; @@ -66,6 +67,7 @@ function hydrateInternal( } const hydrators = { + bot: botHydration, channel: channelHydration, channelUnread: channelUnreadHydration, emoji: emojiHydration, diff --git a/src/hydration/user.ts b/src/hydration/user.ts index 9b38cfe6..c1974087 100644 --- a/src/hydration/user.ts +++ b/src/hydration/user.ts @@ -32,13 +32,13 @@ export const userHydration: Hydrate = { functions: { id: (user) => user._id, username: (user) => user.username, - relationship: (user) => user.relationship ?? "None", + relationship: (user) => user.relationship!, online: (user) => user.online!, privileged: (user) => user.privileged, - badges: (user) => user.badges ?? 0, - flags: (user) => user.flags ?? 0, + badges: (user) => user.badges!, + flags: (user) => user.flags!, avatar: (user, ctx) => new File(ctx, user.avatar!), status: (user) => user.status!,