pi-roundtable 0.30.4 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/docs/plugins.md +18 -6
- package/package.json +1 -1
- package/src/core/contract/channels.ts +8 -0
- package/src/core/discord/inbound-message.ts +23 -1
- package/src/core/discord/poster/poster-card-parts.ts +160 -0
- package/src/core/discord/poster/poster-checks.ts +18 -150
- package/src/core/discord/poster/poster-errors.ts +11 -2
- package/src/core/discord/poster/poster-forum.ts +162 -0
- package/src/core/discord/poster/poster-legacy-card.ts +84 -0
- package/src/core/discord/poster/poster.ts +141 -4
- package/src/core/discord/poster/rest-poster-core.ts +36 -8
- package/src/core/discord/poster/rest-poster-forum.ts +133 -0
- package/src/core/discord/poster/rest-poster-guild.ts +4 -1
- package/src/core/discord/poster/rest-poster.ts +19 -6
- package/src/core/testing/fake-poster-cards.ts +27 -0
- package/src/core/testing/fake-poster-forum.ts +184 -0
- package/src/core/testing/fake-poster-guild.ts +4 -0
- package/src/core/testing/fake-poster-reads.ts +2 -2
- package/src/core/testing/fake-poster.ts +28 -8
- package/src/discord/index.ts +7 -0
- package/src/testing.ts +1 -0
|
@@ -11,12 +11,14 @@ import {
|
|
|
11
11
|
invalid,
|
|
12
12
|
} from "./poster-checks.ts";
|
|
13
13
|
import { DiscordPosterError, failure, isCode } from "./poster-errors.ts";
|
|
14
|
+
import { THREAD_ARCHIVED } from "./poster-forum.ts";
|
|
14
15
|
import {
|
|
15
16
|
type DiscordRest,
|
|
16
17
|
object,
|
|
17
18
|
restCore,
|
|
18
19
|
text,
|
|
19
20
|
} from "./rest-poster-core.ts";
|
|
21
|
+
import { forumOperations } from "./rest-poster-forum.ts";
|
|
20
22
|
import { guildOperations } from "./rest-poster-guild.ts";
|
|
21
23
|
import { readOperations } from "./rest-poster-read.ts";
|
|
22
24
|
|
|
@@ -214,6 +216,7 @@ export function restPoster(
|
|
|
214
216
|
|
|
215
217
|
return {
|
|
216
218
|
...guild,
|
|
219
|
+
...forumOperations(core),
|
|
217
220
|
async post(channelId, post) {
|
|
218
221
|
checkSnowflake("a channel", channelId);
|
|
219
222
|
const { webhook: name, body, files } = checkedPost(post);
|
|
@@ -362,13 +365,23 @@ export function restPoster(
|
|
|
362
365
|
async editCard(channelId, messageId, card) {
|
|
363
366
|
checkSnowflake("a channel", channelId);
|
|
364
367
|
checkSnowflake("a message", messageId);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
const body = cardBody(card);
|
|
369
|
+
const edit = () =>
|
|
370
|
+
rest.patch(Routes.channelMessage(channelId, messageId), { body });
|
|
371
|
+
try {
|
|
372
|
+
await edit();
|
|
373
|
+
} catch (error) {
|
|
374
|
+
// A forum post's first message has the post's own id. Discord refuses its edit while the
|
|
375
|
+
// post is archived (50083), so the post is opened again and the edit sent once more.
|
|
376
|
+
if (channelId !== messageId || !isCode(error, THREAD_ARCHIVED))
|
|
370
377
|
throw failure("editing a card", error);
|
|
371
|
-
|
|
378
|
+
await rest
|
|
379
|
+
.patch(Routes.channel(channelId), { body: { archived: false } })
|
|
380
|
+
.then(edit)
|
|
381
|
+
.catch((again: unknown) => {
|
|
382
|
+
throw failure("editing a card", again);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
372
385
|
},
|
|
373
386
|
async deleteAsBot(channelId, messageId) {
|
|
374
387
|
checkSnowflake("a channel", channelId);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DiscordCard } from "../discord/poster/poster.ts";
|
|
2
|
+
import { DiscordPosterError } from "../discord/poster/poster-errors.ts";
|
|
3
|
+
import { flatPreview } from "../discord/poster/poster-legacy-card.ts";
|
|
4
|
+
import { cardText } from "./fake-poster-guild.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `content` the fake keeps for a card message: the preview line of a card that has one (as
|
|
8
|
+
* Discord holds it), else the card as plain text for a test to read.
|
|
9
|
+
*/
|
|
10
|
+
export function cardContent(card: DiscordCard): string {
|
|
11
|
+
return card.preview === undefined
|
|
12
|
+
? cardText(card)
|
|
13
|
+
: flatPreview(card.preview);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Refuses an edit that changes a message's kind: a Components V2 message cannot be edited into a
|
|
18
|
+
* `preview` (legacy) card, and a legacy message cannot become a Components V2 card.
|
|
19
|
+
*/
|
|
20
|
+
export function assertSameCardKind(held: DiscordCard, edit: DiscordCard): void {
|
|
21
|
+
if ((held.preview !== undefined) !== (edit.preview !== undefined))
|
|
22
|
+
throw new DiscordPosterError(
|
|
23
|
+
"rejected",
|
|
24
|
+
"editing a card failed: the poster cannot change a message's kind, Components V2 or a preview card",
|
|
25
|
+
{ discordCode: 50035, status: 400 },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChannelKindInfo,
|
|
3
|
+
ForumPost,
|
|
4
|
+
ForumPostCreated,
|
|
5
|
+
ForumTag,
|
|
6
|
+
ForumTagInput,
|
|
7
|
+
} from "../discord/poster/poster.ts";
|
|
8
|
+
import { checkSnowflake } from "../discord/poster/poster-checks.ts";
|
|
9
|
+
import { DiscordPosterError } from "../discord/poster/poster-errors.ts";
|
|
10
|
+
import {
|
|
11
|
+
checkedForumPost,
|
|
12
|
+
checkedForumTags,
|
|
13
|
+
checkedTagIds,
|
|
14
|
+
forumTakesPosts,
|
|
15
|
+
notForum,
|
|
16
|
+
readTag,
|
|
17
|
+
} from "../discord/poster/poster-forum.ts";
|
|
18
|
+
import { cardContent } from "./fake-poster-cards.ts";
|
|
19
|
+
import {
|
|
20
|
+
type FakePosterChannel,
|
|
21
|
+
FakePosterGuild,
|
|
22
|
+
} from "./fake-poster-guild.ts";
|
|
23
|
+
|
|
24
|
+
/** A forum the fake holds: the tags it offers. */
|
|
25
|
+
export interface FakeForum {
|
|
26
|
+
tags: ForumTag[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The fake's forum calls and `channelKind`, on top of its channel, thread and member calls. */
|
|
30
|
+
export abstract class FakePosterForum extends FakePosterGuild {
|
|
31
|
+
/**
|
|
32
|
+
* The forums that exist, with their tags. A test makes one by setting an entry (and registering
|
|
33
|
+
* the channel in `channels`); a post made in a channel that is none fails as `not_forum`.
|
|
34
|
+
*/
|
|
35
|
+
readonly forums = new Map<string, FakeForum>();
|
|
36
|
+
|
|
37
|
+
#forum(forumId: string): FakeForum {
|
|
38
|
+
if (this.goneChannels.has(forumId))
|
|
39
|
+
throw new DiscordPosterError(
|
|
40
|
+
"channel_gone",
|
|
41
|
+
"the channel does not exist",
|
|
42
|
+
);
|
|
43
|
+
const forum = this.forums.get(forumId);
|
|
44
|
+
if (forum) return forum;
|
|
45
|
+
if (this.channels.has(forumId) || this.categories.has(forumId))
|
|
46
|
+
throw notForum("that channel");
|
|
47
|
+
throw new DiscordPosterError("channel_gone", "the channel does not exist");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Discord takes no message straight into a forum: a post is made with `createForumPost`. */
|
|
51
|
+
protected refuseForum(channelId: string): void {
|
|
52
|
+
if (this.forums.has(channelId)) throw forumTakesPosts();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A tag the forum does not offer is Discord's 400. */
|
|
56
|
+
#offered(forum: FakeForum, tagIds: readonly string[]): void {
|
|
57
|
+
if (tagIds.some((id) => !forum.tags.some((tag) => tag.id === id)))
|
|
58
|
+
throw new DiscordPosterError(
|
|
59
|
+
"rejected",
|
|
60
|
+
"a tag is not one the forum offers",
|
|
61
|
+
{ discordCode: 50035, status: 400 },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async channelKind(channelId: string): Promise<ChannelKindInfo> {
|
|
66
|
+
checkSnowflake("a channel", channelId);
|
|
67
|
+
this.sent("channelKind");
|
|
68
|
+
if (this.goneChannels.has(channelId))
|
|
69
|
+
throw new DiscordPosterError(
|
|
70
|
+
"channel_gone",
|
|
71
|
+
"the channel does not exist",
|
|
72
|
+
);
|
|
73
|
+
const guildId = this.channelGuilds.get(channelId) ?? this.guildId;
|
|
74
|
+
if (this.categories.has(channelId)) return { guildId, kind: "category" };
|
|
75
|
+
const channel = this.channels.get(channelId);
|
|
76
|
+
if (!channel)
|
|
77
|
+
throw new DiscordPosterError(
|
|
78
|
+
"channel_gone",
|
|
79
|
+
"the channel does not exist",
|
|
80
|
+
);
|
|
81
|
+
const parent = channel.parentId ? { parentId: channel.parentId } : {};
|
|
82
|
+
if (this.forums.has(channelId))
|
|
83
|
+
return { guildId, kind: "forum", ...parent };
|
|
84
|
+
if (channel.startedOn !== undefined)
|
|
85
|
+
return {
|
|
86
|
+
guildId,
|
|
87
|
+
kind:
|
|
88
|
+
channel.parentId && this.forums.has(channel.parentId)
|
|
89
|
+
? "forum_post"
|
|
90
|
+
: "thread",
|
|
91
|
+
...parent,
|
|
92
|
+
};
|
|
93
|
+
return { guildId, kind: "text", ...parent };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async forumTags(forumId: string): Promise<ForumTag[]> {
|
|
97
|
+
checkSnowflake("a forum", forumId);
|
|
98
|
+
this.sent("forumTags");
|
|
99
|
+
return this.#forum(forumId).tags.map((tag) => ({ ...tag }));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async setForumTags(
|
|
103
|
+
forumId: string,
|
|
104
|
+
tags: readonly ForumTagInput[],
|
|
105
|
+
): Promise<ForumTag[]> {
|
|
106
|
+
checkSnowflake("a forum", forumId);
|
|
107
|
+
const wire = checkedForumTags(tags);
|
|
108
|
+
this.sent("setForumTags");
|
|
109
|
+
const forum = this.#forum(forumId);
|
|
110
|
+
const next = wire.map((tag) => {
|
|
111
|
+
if ("id" in tag && !forum.tags.some((held) => held.id === tag.id))
|
|
112
|
+
throw new DiscordPosterError("rejected", "a tag id is unknown", {
|
|
113
|
+
discordCode: 50035,
|
|
114
|
+
status: 400,
|
|
115
|
+
});
|
|
116
|
+
let id = "id" in tag ? tag.id : undefined;
|
|
117
|
+
if (id === undefined) {
|
|
118
|
+
this.nextChannel += 1n;
|
|
119
|
+
id = this.nextChannel.toString();
|
|
120
|
+
}
|
|
121
|
+
return readTag({ ...tag, id }) as ForumTag;
|
|
122
|
+
});
|
|
123
|
+
forum.tags = next;
|
|
124
|
+
return next.map((tag) => ({ ...tag }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async createForumPost(
|
|
128
|
+
forumId: string,
|
|
129
|
+
post: ForumPost,
|
|
130
|
+
): Promise<ForumPostCreated> {
|
|
131
|
+
checkSnowflake("a forum", forumId);
|
|
132
|
+
const checked = checkedForumPost(post);
|
|
133
|
+
this.sent("createForumPost");
|
|
134
|
+
const forum = this.#forum(forumId);
|
|
135
|
+
this.#offered(forum, checked.appliedTags);
|
|
136
|
+
this.nextChannel += 1n;
|
|
137
|
+
const id = this.nextChannel.toString();
|
|
138
|
+
const channel: FakePosterChannel = {
|
|
139
|
+
name: post.name,
|
|
140
|
+
parentId: forumId,
|
|
141
|
+
topic: "",
|
|
142
|
+
messages: [
|
|
143
|
+
{
|
|
144
|
+
id,
|
|
145
|
+
username: "bot",
|
|
146
|
+
content: post.card ? cardContent(post.card) : (post.content ?? ""),
|
|
147
|
+
files: [],
|
|
148
|
+
blobs: [],
|
|
149
|
+
mentions: checked.mentions,
|
|
150
|
+
authorId: this.botId,
|
|
151
|
+
...(post.card ? { card: post.card } : {}),
|
|
152
|
+
at: this.tick(),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
pins: [],
|
|
156
|
+
avatars: new Map(),
|
|
157
|
+
// The first message of a post has the post's own id.
|
|
158
|
+
startedOn: id,
|
|
159
|
+
autoArchiveMinutes: checked.autoArchive,
|
|
160
|
+
appliedTags: checked.appliedTags,
|
|
161
|
+
};
|
|
162
|
+
this.channels.set(id, channel);
|
|
163
|
+
return { threadId: id, messageId: id };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async setPostTags(
|
|
167
|
+
threadId: string,
|
|
168
|
+
tagIds: readonly string[],
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
checkSnowflake("a thread", threadId);
|
|
171
|
+
const ids = checkedTagIds(tagIds);
|
|
172
|
+
this.sent("setPostTags");
|
|
173
|
+
const channel = this.existing(threadId);
|
|
174
|
+
const forum =
|
|
175
|
+
channel.startedOn !== undefined && channel.parentId
|
|
176
|
+
? this.forums.get(channel.parentId)
|
|
177
|
+
: undefined;
|
|
178
|
+
if (!forum) throw notForum("that thread's parent");
|
|
179
|
+
this.#offered(forum, ids);
|
|
180
|
+
// The poster sends `archived: false` with the tags, so an archived post is opened again.
|
|
181
|
+
this.wake(channel);
|
|
182
|
+
channel.appliedTags = ids;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -52,6 +52,8 @@ export interface FakePosterChannel {
|
|
|
52
52
|
avatars: Map<string, Uint8Array>;
|
|
53
53
|
/** For a thread: the message of the parent channel it was started on. */
|
|
54
54
|
startedOn?: string;
|
|
55
|
+
/** For a forum post: the ids of the tags applied to it. */
|
|
56
|
+
appliedTags?: string[];
|
|
55
57
|
/** For a thread: the minutes without a message after which it archives itself. */
|
|
56
58
|
autoArchiveMinutes?: number;
|
|
57
59
|
/** For a thread: whether it is archived. A post by the bot wakes it, as on Discord. */
|
|
@@ -359,6 +361,8 @@ export abstract class FakePosterGuild {
|
|
|
359
361
|
"unavailable",
|
|
360
362
|
"renaming a channel failed (held back by Discord's rate limit)",
|
|
361
363
|
);
|
|
364
|
+
// The poster sends `archived: false` with a thread's name, so an archived thread is opened again.
|
|
365
|
+
this.wake(channel);
|
|
362
366
|
this.#renameTimes.set(channelId, [...recent, at]);
|
|
363
367
|
channel.name = name;
|
|
364
368
|
this.renames.push([channelId, name]);
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
} from "../discord/poster/poster.ts";
|
|
6
6
|
import { checkSnowflake } from "../discord/poster/poster-checks.ts";
|
|
7
7
|
import { DiscordPosterError } from "../discord/poster/poster-errors.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { FakePosterForum } from "./fake-poster-forum.ts";
|
|
9
9
|
|
|
10
10
|
/** The fake's pin calls and message reads, on top of its channel, thread and member calls. */
|
|
11
|
-
export abstract class FakePosterReads extends
|
|
11
|
+
export abstract class FakePosterReads extends FakePosterForum {
|
|
12
12
|
async pin(channelId: string, messageId: string): Promise<void> {
|
|
13
13
|
checkSnowflake("a channel", channelId);
|
|
14
14
|
checkSnowflake("a message", messageId);
|
|
@@ -14,13 +14,16 @@ import {
|
|
|
14
14
|
invalid,
|
|
15
15
|
} from "../discord/poster/poster-checks.ts";
|
|
16
16
|
import { DiscordPosterError } from "../discord/poster/poster-errors.ts";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
import { archivedThread } from "../discord/poster/poster-forum.ts";
|
|
18
|
+
import { assertSameCardKind, cardContent } from "./fake-poster-cards.ts";
|
|
19
|
+
import type { FakeForum } from "./fake-poster-forum.ts";
|
|
20
|
+
import type {
|
|
21
|
+
FakePosterChannel,
|
|
22
|
+
FakePosterMessage,
|
|
21
23
|
} from "./fake-poster-guild.ts";
|
|
22
24
|
import { FakePosterReads } from "./fake-poster-reads.ts";
|
|
23
25
|
|
|
26
|
+
export type { FakeForum } from "./fake-poster-forum.ts";
|
|
24
27
|
export type {
|
|
25
28
|
FakePosterChannel,
|
|
26
29
|
FakePosterMessage,
|
|
@@ -44,6 +47,11 @@ export interface FakeDiscordPoster extends DiscordPoster {
|
|
|
44
47
|
readonly goneChannels: Set<string>;
|
|
45
48
|
/** The categories that exist; creating a channel under any other fails as `channel_gone`. */
|
|
46
49
|
readonly categories: Set<string>;
|
|
50
|
+
/**
|
|
51
|
+
* The forums that exist, with the tags each offers. Make one by setting an entry here and
|
|
52
|
+
* registering the channel in `channels`; a forum call on any other channel fails as `not_forum`.
|
|
53
|
+
*/
|
|
54
|
+
readonly forums: Map<string, FakeForum>;
|
|
47
55
|
/** How many channels a category holds (Discord: 50) and a guild holds (Discord: 500). */
|
|
48
56
|
categoryLimit: number;
|
|
49
57
|
guildLimit: number;
|
|
@@ -155,6 +163,7 @@ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
|
|
|
155
163
|
const { webhook, body, files } = checkedPost(post);
|
|
156
164
|
this.sent("post");
|
|
157
165
|
const channel = this.channel(channelId);
|
|
166
|
+
this.refuseForum(channelId);
|
|
158
167
|
const webhookId = await this.#webhook(channelId, webhook);
|
|
159
168
|
this.next += 1n;
|
|
160
169
|
const message: FakePosterMessage = {
|
|
@@ -245,7 +254,7 @@ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
|
|
|
245
254
|
const message: FakePosterMessage = {
|
|
246
255
|
id: this.next.toString(),
|
|
247
256
|
username: body.username,
|
|
248
|
-
content:
|
|
257
|
+
content: cardContent(post.card),
|
|
249
258
|
files: [],
|
|
250
259
|
blobs: [],
|
|
251
260
|
mentions: [],
|
|
@@ -283,8 +292,9 @@ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
|
|
|
283
292
|
"editing a card failed: that message is not a card",
|
|
284
293
|
{ discordCode: 50035, status: 400 },
|
|
285
294
|
);
|
|
295
|
+
assertSameCardKind(message.card, card);
|
|
286
296
|
message.card = card;
|
|
287
|
-
message.content =
|
|
297
|
+
message.content = cardContent(card);
|
|
288
298
|
}
|
|
289
299
|
|
|
290
300
|
async remove(
|
|
@@ -353,6 +363,7 @@ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
|
|
|
353
363
|
const body = checkedText(post.content, post.mentions);
|
|
354
364
|
this.sent("say");
|
|
355
365
|
const channel = this.channel(channelId);
|
|
366
|
+
this.refuseForum(channelId);
|
|
356
367
|
this.wake(channel);
|
|
357
368
|
this.next += 1n;
|
|
358
369
|
channel.messages.push({
|
|
@@ -373,12 +384,13 @@ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
|
|
|
373
384
|
cardBody(card);
|
|
374
385
|
this.sent("sayCard");
|
|
375
386
|
const channel = this.channel(channelId);
|
|
387
|
+
this.refuseForum(channelId);
|
|
376
388
|
this.wake(channel);
|
|
377
389
|
this.next += 1n;
|
|
378
390
|
channel.messages.push({
|
|
379
391
|
id: this.next.toString(),
|
|
380
392
|
username: "bot",
|
|
381
|
-
content:
|
|
393
|
+
content: cardContent(card),
|
|
382
394
|
files: [],
|
|
383
395
|
blobs: [],
|
|
384
396
|
mentions: [],
|
|
@@ -399,6 +411,13 @@ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
|
|
|
399
411
|
cardBody(card);
|
|
400
412
|
this.sent("editCard");
|
|
401
413
|
const message = this.found(channelId, messageId);
|
|
414
|
+
const thread = this.channels.get(channelId);
|
|
415
|
+
// Discord refuses the edit of a message in an archived thread; the poster opens a post again
|
|
416
|
+
// for its first message (which has the post's id) and sends the edit once more.
|
|
417
|
+
if (thread?.archived) {
|
|
418
|
+
if (channelId !== messageId) throw archivedThread();
|
|
419
|
+
this.wake(thread);
|
|
420
|
+
}
|
|
402
421
|
// A message posted without the Components V2 flag cannot become a card.
|
|
403
422
|
if (!message.card)
|
|
404
423
|
throw new DiscordPosterError(
|
|
@@ -406,8 +425,9 @@ class FakePosterImpl extends FakePosterReads implements FakeDiscordPoster {
|
|
|
406
425
|
"editing a card failed: that message is not a card",
|
|
407
426
|
{ discordCode: 50035, status: 400 },
|
|
408
427
|
);
|
|
428
|
+
assertSameCardKind(message.card, card);
|
|
409
429
|
message.card = card;
|
|
410
|
-
message.content =
|
|
430
|
+
message.content = cardContent(card);
|
|
411
431
|
}
|
|
412
432
|
|
|
413
433
|
async deleteAsBot(channelId: string, messageId: string): Promise<void> {
|
package/src/discord/index.ts
CHANGED
|
@@ -84,14 +84,21 @@ export type {
|
|
|
84
84
|
CardPicture,
|
|
85
85
|
CardSelect,
|
|
86
86
|
CategoryChannel,
|
|
87
|
+
ChannelKind,
|
|
88
|
+
ChannelKindInfo,
|
|
87
89
|
DiscordCard,
|
|
88
90
|
DiscordFailure,
|
|
89
91
|
DiscordMessage,
|
|
90
92
|
DiscordPoster,
|
|
91
93
|
DiscordReason,
|
|
94
|
+
ForumPost,
|
|
95
|
+
ForumPostCreated,
|
|
96
|
+
ForumTag,
|
|
97
|
+
ForumTagInput,
|
|
92
98
|
PostedMessage,
|
|
93
99
|
PosterFile,
|
|
94
100
|
RecentMessage,
|
|
101
|
+
ThreadAutoArchive,
|
|
95
102
|
ThreadCloser,
|
|
96
103
|
ThreadState,
|
|
97
104
|
WebhookCardPost,
|
package/src/testing.ts
CHANGED