Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gateway-attachment-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wolfstar/plugin-gateway": minor
---

Add `AttachmentBuilder` to `@wolfstar/plugin-gateway`, like discord.js: `new AttachmentBuilder(file, { name, description })` with the chainable `setFile`, `setName`, `setDescription`, `setSpoiler`, `setTitle`, `setDuration` and `setWaveform`, the `spoiler` getter, `toJSON()`, and `AttachmentBuilder.from(other)` to copy a builder, a payload or a received `Attachment`. Pass it in the `files` of a message. `Attachment#attachment` now returns the URL of a received attachment, as in discord.js.
10 changes: 10 additions & 0 deletions packages/plugin-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,16 @@ DM (`OmitPartialGroupDMChannel`), `fetch(force)` answers from the cache when `fo
channel or its ID, and `messageSnapshots` hold `MessageSnapshot`s. `sharedClientTheme`, `resolveComponent(customId)`,
and `fetchWebhook()` are discord.js's.

Files to send are `AttachmentBuilder`s, as in discord.js: `new AttachmentBuilder(file, { name, description })` takes
a buffer, a path, a URL, a stream, or a blob, and `setFile`, `setName`, `setDescription`, `setSpoiler` (which adds or removes
the `SPOILER_` prefix), `setTitle`, `setDuration`, and `setWaveform` (for voice messages) chain. Pass them in the `files` of a
message, and copy a builder, a payload, or a received `Attachment` (whose `attachment` is its URL) with `AttachmentBuilder.from`.

```ts
const file = new AttachmentBuilder("./howl.ogg").setDescription("A howl").setSpoiler();
await channel.send({ content: "Awoo", files: [file] });
```

As in discord.js, `attachments`, `stickers`, `messageSnapshots`, and `reactions.cache` are
`Collection`s keyed by ID (reactions by the ID of a custom emoji, the name of a Unicode one), while
`embeds` and `components` are arrays. `react()` resolves to the `MessageReaction`, counting the bot.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export class Attachment extends BaseAttachment {
initStructure(this, data, relations);
}

/**
* The URL of the file, like discord.js's `Attachment#attachment`, so that an attachment can be resent with
* `AttachmentBuilder.from`.
*/
public get attachment() {
return this.url;
}

/**
* The name of the file.
*/
Expand Down
216 changes: 216 additions & 0 deletions packages/plugin-gateway/src/structures/messages/AttachmentBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
* Adapted from discord.js's `AttachmentBuilder`
* (https://github.com/discordjs/discord.js/blob/main/packages/discord.js/src/structures/AttachmentBuilder.js).
* Copyright 2021 Noel Buechler, Copyright 2015 Amish Shah. Licensed under the Apache License, Version 2.0.
*/

import { basename } from "node:path";
import type { AttachmentPayload, BufferResolvable, JSONEncodable } from "./MessagePayload.js";

const SpoilerPrefix = "SPOILER_";

/**
* The optional data of an {@link AttachmentBuilder}.
*/
export interface AttachmentBuilderData {
/**
* The name of the file, inferred from a path or URL when omitted.
*/
name?: string;
/**
* The description (alt text) of the file.
*/
description?: string;
}

/**
* What {@link AttachmentBuilder.from} copies: an {@link AttachmentPayload}, an {@link AttachmentBuilder}, or a received
* `Attachment`, whose `attachment` is its URL.
*/
export interface AttachmentBuilderSource {
attachment: BufferResolvable;
name?: string | null;
description?: string | null;
title?: string | null;
duration?: number | null;
waveform?: string | null;
spoiler?: boolean;
}

/**
* Builds a file to send with a message, like discord.js's `AttachmentBuilder`. Pass it in the `files` of a message.
*
* @example
* ```ts
* const file = new AttachmentBuilder("./howl.ogg").setDescription("A howl").setSpoiler();
* await channel.send({ content: "Awoo", files: [file] });
* ```
*/
export class AttachmentBuilder implements JSONEncodable<AttachmentPayload> {
/**
* The contents of the file: its data, a path on disk, a URL, a stream, or a blob.
*/
public attachment: BufferResolvable;

/**
* The name of the file, `undefined` to infer it from a path or URL.
*/
public name?: string;

/**
* The description (alt text) of the file.
*/
public description?: string;

/**
* The title of the file.
*/
public title?: string;

/**
* The duration of a voice message, in seconds.
*/
public duration?: number;

/**
* The waveform of a voice message, base64-encoded.
*/
public waveform?: string;

/**
* Whether the file is a spoiler while it has no name to prefix with `SPOILER_`.
*/
private pendingSpoiler = false;

/**
* @param attachment The contents of the file.
* @param data The name and description of the file.
*/
public constructor(attachment: BufferResolvable, data: AttachmentBuilderData = {}) {
this.attachment = attachment;
this.name = data.name;
this.description = data.description;
}

/**
* Whether the file is a spoiler: its name starts with `SPOILER_`.
*/
public get spoiler(): boolean {
return this.name === undefined
? this.pendingSpoiler
: basename(this.name).startsWith(SpoilerPrefix);
}

/**
* Sets the description (alt text) of the file.
*
* @param description The description.
*/
public setDescription(description: string): this {
this.description = description;
return this;
}

/**
* Sets the contents of the file, and its name when given.
*
* @param attachment The contents of the file.
* @param name The name of the file, kept as is when omitted.
*/
public setFile(attachment: BufferResolvable, name?: string): this {
this.attachment = attachment;
if (name !== undefined) this.setName(name);
return this;
}

/**
* Sets the name of the file.
*
* @param name The name of the file.
*/
public setName(name: string): this {
this.name = name;
if (this.pendingSpoiler) {
this.pendingSpoiler = false;
this.setSpoiler();
}
return this;
}

/**
* Marks the file as a spoiler, or not, by adding or removing the `SPOILER_` prefix of its name.
*
* @param spoiler Whether the file is a spoiler.
*/
public setSpoiler(spoiler = true): this {
if (this.name === undefined) {
this.pendingSpoiler = spoiler;
} else if (!spoiler) {
while (this.name.startsWith(SpoilerPrefix)) this.name = this.name.slice(SpoilerPrefix.length);
} else if (!this.name.startsWith(SpoilerPrefix)) {
this.name = `${SpoilerPrefix}${this.name}`;
}
return this;
}

/**
* Sets the title of the file.
*
* @param title The title.
*/
public setTitle(title: string): this {
this.title = title;
return this;
}

/**
* Sets the duration of a voice message.
*
* @param duration The duration, in seconds.
*/
public setDuration(duration: number): this {
this.duration = duration;
return this;
}

/**
* Sets the waveform of a voice message.
*
* @param waveform The waveform, base64-encoded.
*/
public setWaveform(waveform: string): this {
this.waveform = waveform;
return this;
}

/**
* The file as an {@link AttachmentPayload}.
*/
public toJSON(): AttachmentPayload {
const payload: AttachmentPayload = { attachment: this.attachment };
if (this.name !== undefined) payload.name = this.name;
if (this.description !== undefined) payload.description = this.description;
if (this.title !== undefined) payload.title = this.title;
if (this.duration !== undefined) payload.duration = this.duration;
if (this.waveform !== undefined) payload.waveform = this.waveform;
if (this.name === undefined && this.pendingSpoiler) payload.spoiler = true;
return payload;
}

/**
* Copies a file.
*
* @param other The builder, payload, or received attachment to copy.
*/
public static from(other: AttachmentBuilderSource): AttachmentBuilder {
const builder = new AttachmentBuilder(other.attachment, {
name: other.name ?? undefined,
description: other.description ?? undefined,
});
if (other.title != null) builder.title = other.title;
if (other.duration != null) builder.duration = other.duration;
if (other.waveform != null) builder.waveform = other.waveform;
if (other.spoiler) builder.setSpoiler();
return builder;
}
}
1 change: 1 addition & 0 deletions packages/plugin-gateway/src/structures/messages/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./Attachment.js";
export * from "./AttachmentBuilder.js";
export * from "./Embed.js";
export * from "./Message.js";
export * from "./MessageMentions.js";
Expand Down
Loading
Loading