diff --git a/lib/attachments/upload.ts b/lib/attachments/upload.ts new file mode 100644 index 0000000..589db4f --- /dev/null +++ b/lib/attachments/upload.ts @@ -0,0 +1,29 @@ +import axios from 'axios'; +import { FileData } from 'eludris-api-types/effis'; +import { Bot } from '../bot.js'; + +// Make an uploadAttachment function where file.file must use FileData and Buffer +export default async function uploadAttachment( + bot: Bot, + file: { + file: Buffer, + name: string, + spoiler?: boolean, + } +): Promise { + const fixedFile = { + file: file.file, + name: file.name, + spoiler: file.spoiler, + }; + const response = await axios.post(`${bot.cdn}/attachments`, { + file: fixedFile, + }, + { + headers: { + "Content-Type": "multipart/form-data", + } + } + ); + return response.data as FileData; +} \ No newline at end of file diff --git a/lib/bot.ts b/lib/bot.ts index 1db5924..eea8da9 100644 --- a/lib/bot.ts +++ b/lib/bot.ts @@ -1,11 +1,16 @@ -import { RawData, WebSocket } from 'ws' -import { EventEmitter } from 'events' -import axios from 'axios' -import { Message } from 'eludris-api-types/oprish' -import { EvangelineValueError } from './errors.js' - -const DEFAULT_REST_URL = 'https://eludris.tooty.xyz/' -const DEFAULT_WS_URL = 'wss://eludris.tooty.xyz/ws/' +import fs from 'fs'; +import { RawData, WebSocket } from 'ws'; +import { EventEmitter } from 'events'; +import axios from 'axios'; +import { Message } from 'eludris-api-types/oprish'; +import { EvangelineValueError } from './errors.js'; +import uploadAttachment from './attachments/upload.js'; +import { FileData } from 'eludris-api-types/effis'; +import path from 'path'; + +const DEFAULT_REST_URL = 'https://eludris.tooty.xyz/'; +const DEFAULT_WS_URL = 'wss://eludris.tooty.xyz/ws/'; +const DEFAULT_CDN_URL = 'https://effis.tooty.xyz/'; /** * The options for the bot. @@ -19,6 +24,10 @@ export interface BotOptions { * The REST URL to send requests to. */ restURL?: string; + /** + * The CDN URL to send requests to. + */ + cdnURL?: string; } export declare interface Bot { @@ -26,7 +35,7 @@ export declare interface Bot { * A function to listen to certain events. * @param event The event to listen to. * @param listener The parameters (if needed) to be used in the event. - * @returns {@link Bot} + * @returns {@link Bot} * @example * import { Bot } from 'evangeline'; * @@ -44,11 +53,12 @@ export declare interface Bot { } export class Bot extends EventEmitter { - public author: string - private options?: BotOptions - public ws: WebSocket | null = null - public rest: string - private interval: NodeJS.Timer | null = null + public author: string; + private options?: BotOptions; + public ws: WebSocket | null = null; + public cdn: string; + public rest: string; + private interval: NodeJS.Timer | null = null; /** * The main bot class. @@ -64,12 +74,13 @@ export class Bot extends EventEmitter { * }) */ constructor(author: string, options?: BotOptions) { - super() - this.author = author - this.options = options - this.rest = this.options?.restURL || DEFAULT_REST_URL + super(); + this.author = author; + this.options = options; + this.rest = this.options?.restURL || DEFAULT_REST_URL; + this.cdn = this.options?.cdnURL || DEFAULT_CDN_URL; } - + /** * Connects to the Eludris gateway. * @throws {EvangelineValueError} If `author` is not 2-32 characters long. @@ -82,33 +93,33 @@ export class Bot extends EventEmitter { */ connect() { if (this.author.length < 2 || this.author.length > 32) { - throw new EvangelineValueError('author passed is not 2-32 characters long') + throw new EvangelineValueError('author passed is not 2-32 characters long'); } - this.ws = new WebSocket(this.options?.gatewayURL || DEFAULT_WS_URL) + this.ws = new WebSocket(this.options?.gatewayURL || DEFAULT_WS_URL); this.ws.on('open', () => { - this.emit('ready') - }) + this.emit('ready'); + }); this.ws.on('message', (data: RawData) => { - const event = JSON.parse(data.toString()) + const event = JSON.parse(data.toString()); if (event.op == "MESSAGE_CREATE") { - this.emit('messageCreate', event.d as Message) + this.emit('messageCreate', event.d as Message); } - }) + }); this.ws.on('close', (code: number, reason: Buffer) => { - this.emit('close', code, reason) - }) + this.emit('close', code, reason); + }); this.ws.on('error', (error: Error) => { - this.emit('error', error) - }) + this.emit('error', error); + }); this.interval = setInterval(() => { - this.ws?.send(JSON.stringify({op: "PING"})) - }, 45 * 1000) + this.ws?.send(JSON.stringify({ op: "PING" })); + }, 45 * 1000); } /** @@ -116,8 +127,8 @@ export class Bot extends EventEmitter { */ close() { if (this.ws?.readyState === WebSocket.OPEN) { - this.ws.close() - clearInterval(this.interval!) + this.ws.close(); + clearInterval(this.interval!); } } @@ -133,23 +144,101 @@ export class Bot extends EventEmitter { * await bot.sendMessage('woah, I\'m alive!') * }) */ - async sendMessage(content: string): Promise { - if (content === '' || content === undefined || content === null) { - throw new EvangelineValueError('Message content cannot be empty') + // make use of Buffer.from() to send files + async sendMessage(content?: string, file?: { + name: string, + file: Buffer, + spoiler?: boolean; + }): Promise { + let fullMessage: string; + if (file) { + const attachment = await uploadAttachment(this, file); + const fixedURL = `${this.cdn}${attachment.id}`; + fullMessage = `${content || ''} ${fixedURL}`; + } else { + fullMessage = content || ''; } - return await axios.post(`${this.rest}/messages`, { + return (await axios.post(`${this.rest}/messages`, { author: this.author, - content: content - }).then((v) => v.data) as Promise + content: fullMessage, + })).data as Message; } /** * Send a message to Eludris. (Alias for {@link sendMessage}) * @param content The content of the message. */ - async send(content: string): Promise { - return await this.sendMessage(content) + async send(content?: string, file?: { + name: string, + file: Buffer, + spoiler?: boolean; + }): Promise { + return await this.sendMessage(content, file); + } + + /** + * Fetches an attachment from the attachment bucket. + * @param id The ID of the attachment. + * @returns {Buffer} The attachment. + */ + async fetchAttachment(id: string): Promise { + return (await axios.get(`${this.cdn}/attachments/${id}`, { + responseType: 'arraybuffer' + })).data as Buffer; + } + + /** + * Fetches an attachment's data from the attachment bucket. + * @param id The ID of the attachment. + * @returns {FileData} The attachment's data. + */ + async fetchAttachmentData(id: string): Promise { + return (await axios.get(`${this.cdn}/attachments/${id}/data`, { + responseType: 'json' + })).data as FileData; } + /** + * Downloads an attachment from the attachment bucket. + * @param id The ID of the attachment. + * @param name The name of the attachment. + * @returns {Promise} A promise that resolves when the attachment is downloaded. + */ + async downloadAttachment(id: string, name: string): Promise { + const data = this.fetchAttachment(id); + data.then((attachment) => { + return fs.writeFileSync( + path.join(process.cwd(), name), + attachment + ); + }); + } + + /** + * Fetch a static file with the given name. These are added only by the instance owner. + * @param name The name of the file. + * @returns {Buffer} The file. + */ + async fetchStaticFile(name: string): Promise { + return (await axios.get(`${this.cdn}/static/${name}`, { + responseType: 'arraybuffer' + })).data as Buffer; + } + + /** + * Download a static file with the given name. These are added only by the instance owner. + * @param name The name of the file. + * @returns {Promise} A promise that resolves when the file is downloaded. + */ + async downloadStaticFile(name: string): Promise { + const response = (await axios.get(`${this.cdn}/static/${name}/download`, { + responseType: 'arraybuffer' + })).data as Buffer; + + return fs.writeFileSync( + path.join(process.cwd(), name), + response + ); + } }