Skip to content

Commit 85b0780

Browse files
authored
Merge pull request #328 from Helloyunho/main
✨ feat: add guild forum channel support
2 parents 0247454 + 304e9c4 commit 85b0780

18 files changed

+706
-262
lines changed

mod.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export {
6767
GuildIntegration
6868
} from './src/structures/guild.ts'
6969
export { CategoryChannel } from './src/structures/guildCategoryChannel.ts'
70+
export {
71+
GuildForumChannel,
72+
GuildForumTag
73+
} from './src/structures/guildForumChannel.ts'
7074
export { NewsChannel } from './src/structures/guildNewsChannel.ts'
7175
export { VoiceChannel } from './src/structures/guildVoiceChannel.ts'
7276
export { Invite } from './src/structures/invite.ts'
@@ -87,8 +91,7 @@ export { Snowflake } from './src/utils/snowflake.ts'
8791
export { TextChannel } from './src/structures/textChannel.ts'
8892
export {
8993
GuildTextBasedChannel,
90-
GuildTextChannel,
91-
checkGuildTextBasedChannel
94+
GuildTextChannel
9295
} from './src/structures/guildTextChannel.ts'
9396
export type { AllMessageOptions } from './src/structures/textChannel.ts'
9497
export { MessageReaction } from './src/structures/messageReaction.ts'

src/managers/channelThreads.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import type {
66
ThreadChannel,
77
ThreadMember
88
} from '../structures/threadChannel.ts'
9+
import type { BaseManager, Message } from '../../mod.ts'
910
import type {
1011
CreateThreadOptions,
11-
GuildTextChannel
12-
} from '../structures/guildTextChannel.ts'
13-
import type { BaseManager, Message } from '../../mod.ts'
12+
GuildThreadAvailableChannel
13+
} from '../structures/guildThreadAvailableChannel.ts'
1414

1515
export class ChannelThreadsManager extends BaseChildManager<
1616
ThreadChannelPayload,
1717
ThreadChannel
1818
> {
19-
channel: GuildTextChannel
19+
channel: GuildThreadAvailableChannel
2020
declare parent: BaseManager<ThreadChannelPayload, ThreadChannel>
2121

2222
constructor(
2323
client: Client,
2424
parent: ThreadsManager,
25-
channel: GuildTextChannel
25+
channel: GuildThreadAvailableChannel
2626
) {
2727
super(
2828
client,
@@ -60,7 +60,7 @@ export class ChannelThreadsManager extends BaseChildManager<
6060

6161
async start(
6262
options: CreateThreadOptions,
63-
message: string | Message
63+
message?: string | Message
6464
): Promise<ThreadChannel> {
6565
return this.channel.startThread(options, message)
6666
}

src/managers/emojis.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Guild } from '../../mod.ts'
12
import type { Client } from '../client/mod.ts'
23
import { Emoji } from '../structures/emoji.ts'
34
import type { EmojiPayload } from '../types/emoji.ts'
@@ -32,4 +33,20 @@ export class EmojisManager extends BaseManager<EmojiPayload, Emoji> {
3233
.catch((e) => reject(e))
3334
})
3435
}
36+
37+
/** Try to get Emoji from cache, if not found then fetch */
38+
async resolve(
39+
key: string,
40+
guild?: string | Guild
41+
): Promise<Emoji | undefined> {
42+
const cacheValue = await this.get(key)
43+
if (cacheValue !== undefined) return cacheValue
44+
else {
45+
if (guild !== undefined) {
46+
const guildID = typeof guild === 'string' ? guild : guild.id
47+
const fetchValue = await this.fetch(guildID, key).catch(() => undefined)
48+
if (fetchValue !== undefined) return fetchValue
49+
}
50+
}
51+
}
3552
}

src/rest/endpoints.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -1366,9 +1366,9 @@ The `emoji` must be [URL Encoded](https://en.wikipedia.org/wiki/Percent-encoding
13661366
}
13671367

13681368
/**
1369-
* Creates a new public thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event.
1369+
* Creates a new thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event.
13701370
*/
1371-
async startPublicThread(
1371+
async startPublicThreadFromMessage(
13721372
channelId: string,
13731373
messageId: string,
13741374
payload: CreateThreadPayload
@@ -1379,14 +1379,41 @@ The `emoji` must be [URL Encoded](https://en.wikipedia.org/wiki/Percent-encoding
13791379
)
13801380
}
13811381

1382+
// Exist for backwards compatibility
1383+
/**
1384+
* Creates a new public thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event.
1385+
*/
1386+
async startPublicThread(
1387+
channelId: string,
1388+
messageId: string,
1389+
payload: CreateThreadPayload
1390+
): Promise<ThreadChannelPayload> {
1391+
return await this.startPublicThreadFromMessage(
1392+
channelId,
1393+
messageId,
1394+
payload
1395+
)
1396+
}
1397+
1398+
/**
1399+
* Creates a new thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event.
1400+
*/
1401+
async startThreadWithoutMessage(
1402+
channelId: string,
1403+
payload: CreateThreadPayload
1404+
): Promise<ThreadChannelPayload> {
1405+
return this.rest.post(`/channels/${channelId}/threads`, payload)
1406+
}
1407+
1408+
// Exist for backwards compatibility
13821409
/**
13831410
* Creates a new private thread. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event.
13841411
*/
13851412
async startPrivateThread(
13861413
channelId: string,
13871414
payload: CreateThreadPayload
13881415
): Promise<ThreadChannelPayload> {
1389-
return this.rest.post(`/channels/${channelId}/threads`, payload)
1416+
return await this.startThreadWithoutMessage(channelId, payload)
13901417
}
13911418

13921419
/**

src/structures/channel.ts

+13
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,23 @@ import type { VoiceChannel } from '../structures/guildVoiceChannel.ts'
4343
import type { StageVoiceChannel } from '../structures/guildVoiceStageChannel.ts'
4444
import type { TextChannel } from '../structures/textChannel.ts'
4545
import type { ThreadChannel } from '../structures/threadChannel.ts'
46+
import { CreateInviteOptions } from '../managers/invites.ts'
47+
import { Invite } from './invite.ts'
4648

4749
export class Channel extends SnowflakeBase {
4850
type!: ChannelTypes
51+
flags!: number
4952

5053
static cacheName = 'channel'
5154

5255
get mention(): string {
5356
return `<#${this.id}>`
5457
}
5558

59+
toString(): string {
60+
return this.mention
61+
}
62+
5663
constructor(client: Client, data: ChannelPayload) {
5764
super(client, data)
5865
this.readFromData(data)
@@ -61,6 +68,7 @@ export class Channel extends SnowflakeBase {
6168
readFromData(data: ChannelPayload): void {
6269
this.type = data.type ?? this.type
6370
this.id = data.id ?? this.id
71+
this.flags = data.flags ?? this.flags
6472
}
6573

6674
isDM(): this is DMChannel {
@@ -435,4 +443,9 @@ export class GuildChannel extends Channel {
435443
async setPosition(position: number): Promise<GuildChannels> {
436444
return await this.edit({ position })
437445
}
446+
447+
/** Create an Invite for this Channel */
448+
async createInvite(options?: CreateInviteOptions): Promise<Invite> {
449+
return this.guild.invites.create(this.id, options)
450+
}
438451
}

src/structures/guildForumChannel.ts

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { Client } from '../client/client.ts'
2+
import type { AllMessageOptions } from '../managers/channels.ts'
3+
import {
4+
CreateThreadInForumPayload,
5+
GuildForumChannelPayload,
6+
GuildForumSortOrderTypes,
7+
GuildForumTagPayload,
8+
ModifyGuildForumChannelOption,
9+
ModifyGuildForumChannelPayload,
10+
ThreadChannelPayload
11+
} from '../types/channel.ts'
12+
import { CHANNEL } from '../types/endpoint.ts'
13+
import { transformComponent } from '../utils/components.ts'
14+
import { Embed } from './embed.ts'
15+
import { Emoji } from './emoji.ts'
16+
import { Guild } from './guild.ts'
17+
import { GuildThreadAvailableChannel } from './guildThreadAvailableChannel.ts'
18+
import { Message } from './message.ts'
19+
import { ThreadChannel } from './threadChannel.ts'
20+
21+
export interface CreateThreadInForumOptions {
22+
/** 2-100 character channel name */
23+
name: string
24+
/** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */
25+
autoArchiveDuration?: number
26+
slowmode?: number | null
27+
message: string | AllMessageOptions
28+
appliedTags?: string[] | GuildForumTag[]
29+
}
30+
31+
export class GuildForumTag {
32+
id!: string
33+
name!: string
34+
moderated!: boolean
35+
emojiID!: string
36+
emojiName!: string | null
37+
38+
constructor(data: GuildForumTagPayload) {
39+
this.readFromData(data)
40+
}
41+
42+
readFromData(data: GuildForumTagPayload): void {
43+
this.id = data.id ?? this.id
44+
this.name = data.name ?? this.name
45+
this.moderated = data.moderated ?? this.moderated
46+
this.emojiID = data.emoji_id ?? this.emojiID
47+
this.emojiName = data.emoji_name ?? this.emojiName
48+
}
49+
}
50+
51+
export class GuildForumChannel extends GuildThreadAvailableChannel {
52+
availableTags!: GuildForumTag[]
53+
defaultReactionEmoji!: Emoji
54+
defaultSortOrder!: GuildForumSortOrderTypes
55+
56+
constructor(client: Client, data: GuildForumChannelPayload, guild: Guild) {
57+
super(client, data, guild)
58+
this.readFromData(data)
59+
}
60+
61+
readFromData(data: GuildForumChannelPayload): void {
62+
super.readFromData(data)
63+
this.availableTags =
64+
data.available_tags?.map((tag) => new GuildForumTag(tag)) ??
65+
this.availableTags
66+
this.defaultReactionEmoji =
67+
data.default_reaction_emoji !== null
68+
? new Emoji(this.client, {
69+
id: data.default_reaction_emoji.emoji_id,
70+
name: data.default_reaction_emoji.emoji_name
71+
})
72+
: this.defaultReactionEmoji
73+
this.defaultSortOrder = data.default_sort_order ?? this.defaultSortOrder
74+
}
75+
76+
async edit(
77+
options?: ModifyGuildForumChannelOption
78+
): Promise<GuildForumChannel> {
79+
if (options?.defaultReactionEmoji !== undefined) {
80+
if (options.defaultReactionEmoji instanceof Emoji) {
81+
options.defaultReactionEmoji = {
82+
emoji_id: options.defaultReactionEmoji.id,
83+
emoji_name: options.defaultReactionEmoji.name
84+
}
85+
}
86+
}
87+
if (options?.availableTags !== undefined) {
88+
options.availableTags = options.availableTags?.map((tag) => {
89+
if (tag instanceof GuildForumTag) {
90+
return {
91+
id: tag.id,
92+
name: tag.name,
93+
moderated: tag.moderated,
94+
emoji_id: tag.emojiID,
95+
emoji_name: tag.emojiName
96+
}
97+
}
98+
return tag
99+
})
100+
}
101+
102+
const body: ModifyGuildForumChannelPayload = {
103+
name: options?.name,
104+
position: options?.position,
105+
permission_overwrites: options?.permissionOverwrites,
106+
parent_id: options?.parentID,
107+
nsfw: options?.nsfw,
108+
topic: options?.topic,
109+
rate_limit_per_user: options?.slowmode,
110+
default_auto_archive_duration: options?.defaultAutoArchiveDuration,
111+
default_thread_rate_limit_per_user: options?.defaultThreadSlowmode,
112+
default_sort_order: options?.defaultSortOrder,
113+
default_reaction_emoji: options?.defaultReactionEmoji,
114+
available_tags: options?.availableTags
115+
}
116+
117+
const resp = await this.client.rest.patch(CHANNEL(this.id), body)
118+
119+
return new GuildForumChannel(this.client, resp, this.guild)
120+
}
121+
122+
override async startThread(
123+
options: CreateThreadInForumOptions,
124+
message?: string | AllMessageOptions | Message
125+
): Promise<ThreadChannel> {
126+
if (options.message !== undefined) {
127+
message = options.message
128+
}
129+
if (message instanceof Message) {
130+
message = {
131+
content: message.content,
132+
embeds: message.embeds.map((embed) => new Embed(embed)),
133+
components: message.components
134+
}
135+
} else if (message instanceof Embed) {
136+
message = {
137+
embed: message
138+
}
139+
} else if (Array.isArray(message)) {
140+
message = {
141+
embeds: message
142+
}
143+
} else if (typeof message === 'string') {
144+
message = {
145+
content: message
146+
}
147+
}
148+
149+
const messageObject = {
150+
content: message?.content,
151+
embed: message?.embed,
152+
embeds: message?.embeds,
153+
file: message?.file,
154+
files: message?.files,
155+
allowed_mentions: message?.allowedMentions,
156+
components:
157+
message?.components !== undefined
158+
? typeof message.components === 'function'
159+
? message.components()
160+
: transformComponent(message.components)
161+
: undefined
162+
}
163+
164+
if (
165+
messageObject.content === undefined &&
166+
messageObject.embed === undefined
167+
) {
168+
messageObject.content = ''
169+
}
170+
171+
const body: CreateThreadInForumPayload = {
172+
name: options.name,
173+
auto_archive_duration: options.autoArchiveDuration,
174+
rate_limit_per_user: options.slowmode,
175+
message: messageObject,
176+
applied_tags: options.appliedTags?.map((tag) => {
177+
if (tag instanceof GuildForumTag) {
178+
return tag.id
179+
}
180+
return tag
181+
})
182+
}
183+
184+
const resp: ThreadChannelPayload = await this.client.rest.api.channels[
185+
this.id
186+
].threads.post(body)
187+
const thread = new ThreadChannel(this.client, resp, this.guild)
188+
this.threads.set(thread.id, resp)
189+
return thread
190+
}
191+
}

src/structures/guildNewsChannel.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { Mixin } from '../../deps.ts'
12
import { GuildTextBasedChannel } from './guildTextChannel.ts'
3+
import { GuildThreadAvailableChannel } from './guildThreadAvailableChannel.ts'
24

3-
export class NewsChannel extends GuildTextBasedChannel {}
5+
export class NewsChannel extends Mixin(
6+
GuildTextBasedChannel,
7+
GuildThreadAvailableChannel
8+
) {}

0 commit comments

Comments
 (0)