Skip to content

Commit 5eaf06a

Browse files
Merge branch 'main' into ci/tag-removal
2 parents 5025071 + 68bb8af commit 5eaf06a

66 files changed

Lines changed: 799 additions & 578 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package.json @discordjs/core
77
pnpm-lock.yaml @discordjs/core
88

9+
/.github/ISSUE_TEMPLATE/ @discordjs/guide @discordjs/core
10+
911
/apps/guide/ @discordjs/website @discordjs/guide
1012
/apps/guide/content/ @discordjs/guide
1113
/apps/website/ @discordjs/website

.github/ISSUE_TEMPLATE/04-guide.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ body:
1515
- Requesting new content
1616
- Changing existing content
1717
- Correcting wrong information
18-
validation:
18+
validations:
1919
required: true
2020
- type: input
2121
id: section

.github/workflows/publish-dev.yml

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ on:
44
- cron: '0 */12 * * *'
55
workflow_dispatch:
66
inputs:
7-
pull:
8-
description: 'The pull number to check out'
7+
ref:
8+
description: 'The ref to check out. e.g. main, feat/new-feature, refs/pull/1234/head'
99
required: false
1010
default: 'main'
1111
tag:
@@ -33,21 +33,12 @@ jobs:
3333
app-id: ${{ vars.DISCORDJS_APP_ID }}
3434
private-key: ${{ secrets.DISCORDJS_APP_KEY_RELEASE }}
3535

36-
- name: Decide ref
37-
id: ref
38-
run: |
39-
if [ -n "${{ github.event.inputs.pull }}" ]; then
40-
echo "ref=refs/pull/${{ github.event.inputs.pull }}/head" >> $GITHUB_OUTPUT
41-
else
42-
echo "ref=refs/heads/main" >> $GITHUB_OUTPUT
43-
fi
44-
4536
- name: Checkout repository
4637
uses: actions/checkout@v5
4738
with:
4839
fetch-depth: 0
4940
token: ${{ steps.app-token.outputs.token }}
50-
ref: ${{ steps.ref.outputs.ref }}
41+
ref: ${{ inputs.ref }}
5142

5243
- name: Install Node.js v24
5344
uses: actions/setup-node@v6
@@ -63,13 +54,13 @@ jobs:
6354
run: pnpm run build
6455

6556
- name: Checkout main repository (non-main ref)
66-
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
57+
if: ${{ inputs.ref != 'main' }}
6758
uses: actions/checkout@v5
6859
with:
6960
path: 'main'
7061

7162
- name: Install action deps (non-main ref)
72-
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
63+
if: ${{ inputs.ref != 'main' }}
7364
shell: bash
7465
working-directory: ./main
7566
env:
@@ -79,7 +70,7 @@ jobs:
7970
pnpm install --filter @discordjs/actions --frozen-lockfile --prefer-offline --loglevel error
8071
8172
- name: Publish packages (non-main ref)
82-
if: ${{ steps.ref.outputs.ref != 'refs/heads/main' }}
73+
if: ${{ inputs.ref != 'main' }}
8374
uses: ./main/packages/actions/src/releasePackages
8475
with:
8576
exclude: '@discordjs/docgen'
@@ -91,7 +82,7 @@ jobs:
9182
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9283

9384
- name: Publish packages (main ref)
94-
if: ${{ steps.ref.outputs.ref == 'refs/heads/main' }}
85+
if: ${{ inputs.ref == 'main' }}
9586
uses: ./packages/actions/src/releasePackages
9687
with:
9788
exclude: '@discordjs/docgen'

apps/guide/content/docs/legacy/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"pages": [
33
"[MessageCircleQuestion][FAQ](/legacy/popular-topics/faq)",
44
"[ArrowDownToLine][Updating to v14](/legacy/additional-info/changes-in-v14)",
5-
"[LibraryBig][Documentation](https://discord.js.org/docs)",
5+
"external:[LibraryBig][Documentation](https://discord.js.org/docs)",
66
"[Info][Introduction](/legacy)",
77
"---Setup---",
88
"preparations",

apps/guide/content/docs/voice/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"title": "Voice",
33
"description": "Working with the voice library",
44
"pages": [
5-
"[LibraryBig][Documentation](https://discord.js.org/docs/packages/voice/main)",
5+
"external:[LibraryBig][Documentation](https://discord.js.org/docs/packages/voice/main)",
66
"---Working with Voice---",
77
"index",
88
"life-cycles",

packages/api-extractor-model/src/model/ApiPackage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,9 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
216216
if (semVer === 'workspace:^') {
217217
this._dependencies[pack] =
218218
PackageJsonLookup.instance.tryLoadPackageJsonFor(pathToPackage)?.version ?? 'unknown';
219-
} else if (FileSystem.exists(pathToPackage)) {
220-
this._dependencies[pack] = semVer;
219+
} else {
220+
// if (FileSystem.exists(pathToPackage))
221+
this._dependencies[pack] = semVer.replace(/^[\^~]/, '');
221222
}
222223
}
223224
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Buffer } from 'node:buffer';
2+
import type { RawFile } from '@discordjs/util';
3+
import { test, expect } from 'vitest';
4+
import { AttachmentBuilder, MessageBuilder } from '../../src/index.js';
5+
6+
test('AttachmentBuilder stores and exposes file data', () => {
7+
const data = Buffer.from('hello world');
8+
const attachment = new AttachmentBuilder()
9+
.setId('0')
10+
.setFilename('greeting.txt')
11+
.setFileData(data)
12+
.setFileContentType('text/plain');
13+
14+
expect(attachment.getRawFile()).toStrictEqual({
15+
contentType: 'text/plain',
16+
data,
17+
key: 'files[0]',
18+
name: 'greeting.txt',
19+
});
20+
21+
attachment.clearFileData();
22+
attachment.clearFileContentType();
23+
attachment.clearFilename();
24+
expect(attachment.getRawFile()).toBe(undefined);
25+
});
26+
27+
test('MessageBuilder.toFileBody returns JSON body and files', () => {
28+
const msg = new MessageBuilder().setContent('here is a file').addAttachments(
29+
new AttachmentBuilder()
30+
.setId('0')
31+
.setFilename('file.bin')
32+
.setFileData(Buffer.from([1, 2, 3]))
33+
.setFileContentType('application/octet-stream'),
34+
);
35+
36+
const { body, files } = msg.toFileBody();
37+
38+
// body should match toJSON()
39+
expect(body).toStrictEqual(msg.toJSON());
40+
41+
// files should contain the uploaded file
42+
expect(files).toHaveLength(1);
43+
const [fileEntry] = files as [RawFile];
44+
expect(fileEntry.name).toBe('file.bin');
45+
expect(fileEntry.contentType).toBe('application/octet-stream');
46+
expect(fileEntry.data).toBeDefined();
47+
});
48+
49+
test('MessageBuilder.toFileBody returns empty files when attachments reference existing uploads', () => {
50+
const msg = new MessageBuilder().addAttachments(new AttachmentBuilder().setId('123').setFilename('existing.png'));
51+
52+
const { body, files } = msg.toFileBody();
53+
expect(body).toEqual(msg.toJSON());
54+
expect(files.length).toBe(0);
55+
});

packages/builders/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ export * from './util/ValidationError.js';
9797

9898
export * from './Assertions.js';
9999

100+
// We expose this type in our public API. We shouldn't assume every user of builders is also using REST
101+
export type { RawFile } from '@discordjs/util';
102+
100103
/**
101104
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/builders#readme | @discordjs/builders} version
102105
* that you are currently using.

packages/builders/src/messages/Assertions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1+
import { Buffer } from 'node:buffer';
12
import { AllowedMentionsTypes, ComponentType, MessageFlags, MessageReferenceType } from 'discord-api-types/v10';
23
import { z } from 'zod';
34
import { embedPredicate } from './embed/Assertions.js';
45
import { pollPredicate } from './poll/Assertions.js';
56

7+
const fileKeyRegex = /^files\[(?<placeholder>\d+?)]$/;
8+
9+
export const rawFilePredicate = z.object({
10+
data: z.union([z.instanceof(Buffer), z.instanceof(Uint8Array), z.string()]),
11+
name: z.string().min(1),
12+
contentType: z.string().optional(),
13+
key: z.string().regex(fileKeyRegex).optional(),
14+
});
15+
616
export const attachmentPredicate = z.object({
17+
// As a string it only makes sense for edits when we do have an attachment snowflake
718
id: z.union([z.string(), z.number()]),
819
description: z.string().max(1_024).optional(),
920
duration_secs: z
@@ -125,3 +136,11 @@ const messageComponentsV2Predicate = baseMessagePredicate.extend({
125136
});
126137

127138
export const messagePredicate = z.union([messageNoComponentsV2Predicate, messageComponentsV2Predicate]);
139+
140+
// This validator does not assert file.key <-> attachment.id coherence. This is fine, because the builders
141+
// should effectively guarantee that.
142+
export const fileBodyMessagePredicate = z.object({
143+
body: messagePredicate,
144+
// No min length to support message edits
145+
files: rawFilePredicate.array().max(10),
146+
});

packages/builders/src/messages/Attachment.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { JSONEncodable } from '@discordjs/util';
1+
import type { Buffer } from 'node:buffer';
2+
import type { JSONEncodable, RawFile } from '@discordjs/util';
23
import type { RESTAPIAttachment, Snowflake } from 'discord-api-types/v10';
34
import { validate } from '../util/validation.js';
45
import { attachmentPredicate } from './Assertions.js';
@@ -12,21 +13,33 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
1213
*/
1314
private readonly data: Partial<RESTAPIAttachment>;
1415

16+
/**
17+
* This data is not included in the output of `toJSON()`. For this class specifically, this refers to binary data
18+
* that will wind up being included in the multipart/form-data request, if used with the `MessageBuilder`.
19+
* To retrieve this data, use {@link getRawFile}.
20+
*
21+
* @remarks This cannot be set via the constructor, primarily because of the behavior described
22+
* {@link https://discord.com/developers/docs/reference#editing-message-attachments | here}.
23+
* That is, when editing a message's attachments, you should only be providing file data for new attachments.
24+
*/
25+
private readonly fileData: Partial<Pick<RawFile, 'contentType' | 'data'>>;
26+
1527
/**
1628
* Creates a new attachment builder.
1729
*
1830
* @param data - The API data to create this attachment with
1931
*/
2032
public constructor(data: Partial<RESTAPIAttachment> = {}) {
2133
this.data = structuredClone(data);
34+
this.fileData = {};
2235
}
2336

2437
/**
2538
* Sets the id of the attachment.
2639
*
2740
* @param id - The id of the attachment
2841
*/
29-
public setId(id: Snowflake): this {
42+
public setId(id: Snowflake | number): this {
3043
this.data.id = id;
3144
return this;
3245
}
@@ -85,6 +98,60 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
8598
return this;
8699
}
87100

101+
/**
102+
* Sets the file data to upload with this attachment.
103+
*
104+
* @param data - The file data
105+
* @remarks Note that this data is NOT included in the {@link toJSON} output. To retrieve it, use {@link getRawFile}.
106+
*/
107+
public setFileData(data: Buffer | Uint8Array | string): this {
108+
this.fileData.data = data;
109+
return this;
110+
}
111+
112+
/**
113+
* Clears the file data from this attachment.
114+
*/
115+
public clearFileData(): this {
116+
this.fileData.data = undefined;
117+
return this;
118+
}
119+
120+
/**
121+
* Sets the content type of the file data to upload with this attachment.
122+
*
123+
* @remarks Note that this data is NOT included in the {@link toJSON} output. To retrieve it, use {@link getRawFile}.
124+
*/
125+
public setFileContentType(contentType: string): this {
126+
this.fileData.contentType = contentType;
127+
return this;
128+
}
129+
130+
/**
131+
* Clears the content type of the file data from this attachment.
132+
*/
133+
public clearFileContentType(): this {
134+
this.fileData.contentType = undefined;
135+
return this;
136+
}
137+
138+
/**
139+
* Converts this attachment to a {@link RawFile} for uploading.
140+
*
141+
* @returns A {@link RawFile} object, or `undefined` if no file data is set
142+
*/
143+
public getRawFile(): Partial<RawFile> | undefined {
144+
if (!this.fileData?.data) {
145+
return;
146+
}
147+
148+
return {
149+
...this.fileData,
150+
name: this.data.filename,
151+
key: this.data.id ? `files[${this.data.id}]` : undefined,
152+
};
153+
}
154+
88155
/**
89156
* Sets the title of this attachment.
90157
*

0 commit comments

Comments
 (0)