diff --git a/helpers/channels/forums/createForumPost.ts b/helpers/channels/forums/createForumPost.ts new file mode 100644 index 000000000..eed59b798 --- /dev/null +++ b/helpers/channels/forums/createForumPost.ts @@ -0,0 +1,62 @@ +import type { Bot } from "../../../bot.ts"; +import { DiscordChannel } from "../../../types/discord.ts"; +import { AllowedMentions, FileContent, MessageComponents } from "../../../types/mod.ts"; +import { Embed } from "../../../transformers/embed.ts"; + +/** Creates a new public thread from an existing message. Returns a thread channel. */ +export async function createForumPost( + bot: Bot, + channelId: bigint, + options: CreateForumPostWithMessage, +) { + const result = await bot.rest.runMethod( + bot.rest, + "post", + bot.constants.endpoints.FORUM_START(channelId), + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.rateLimitPerUser, + reason: options.reason, + + content: options.content, + embeds: options.embeds?.map((embed) => bot.transformers.reverse.embed(bot, embed)), + allowed_mentions: options.allowedMentions + ? { + parse: options.allowedMentions?.parse, + roles: options.allowedMentions?.roles?.map((id) => id.toString()), + users: options.allowedMentions?.users?.map((id) => id.toString()), + replied_user: options.allowedMentions?.repliedUser, + } + : undefined, + file: options.file, + components: options.components?.map((component) => bot.transformers.reverse.component(bot, component)), + } + ); + + return bot.transformers.channel(bot, { channel: result, guildId: bot.transformers.snowflake(result.guild_id!) }); +} + +export interface CreateForumPostWithMessage extends CreateForumMessage { + /** 1-100 character thread name */ + name: string; + /** Duration in minutes to automatically archive the thread after recent activity */ + autoArchiveDuration: 60 | 1440 | 4320 | 10080; + /** Amount of seconds a user has to wait before sending another message (0-21600) */ + rateLimitPerUser?: number | null; + /** The reason you are creating the thread */ + reason?: string; +} + +export interface CreateForumMessage { + /** The message contents (up to 2000 characters) */ + content?: string; + /** Embedded `rich` content (up to 6000 characters) */ + embeds?: Embed[]; + /** Allowed mentions for the message */ + allowedMentions?: AllowedMentions; + /** The contents of the file being sent */ + file?: FileContent | FileContent[]; + /** The components you would like to have sent in this message */ + components?: MessageComponents; +} diff --git a/helpers/channels/forums/mod.ts b/helpers/channels/forums/mod.ts new file mode 100644 index 000000000..5654e7c46 --- /dev/null +++ b/helpers/channels/forums/mod.ts @@ -0,0 +1 @@ +export * from './createForumPost.ts'; \ No newline at end of file diff --git a/helpers/channels/mod.ts b/helpers/channels/mod.ts index f534f35f5..06019007e 100644 --- a/helpers/channels/mod.ts +++ b/helpers/channels/mod.ts @@ -1,4 +1,5 @@ export * from "./threads/mod.ts"; +export * from "./forums/mod.ts"; export * from "./createChannel.ts"; export * from "./createStageInstance.ts"; diff --git a/plugins/permissions/src/channels/forums/createForumPost.ts b/plugins/permissions/src/channels/forums/createForumPost.ts new file mode 100644 index 000000000..4785b2ed7 --- /dev/null +++ b/plugins/permissions/src/channels/forums/createForumPost.ts @@ -0,0 +1,16 @@ +import { BotWithCache } from "../../../deps.ts"; +import { requireBotChannelPermissions } from "../../permissions.ts"; + +export default function createForumPosts(bot: BotWithCache) { + const createForumPostsOld = bot.helpers.createForumPosts; + + bot.helpers.createForumPosts= async function (channelId, options) { + const channel = bot.channels.get(channelId); + + if (channel) { + await requireBotChannelPermissions(bot, channel, ["SEND_MESSAGES"]); + } + + return await createForumPostsOld(channelId, options); + }; +} diff --git a/plugins/permissions/src/channels/forums/mod.ts b/plugins/permissions/src/channels/forums/mod.ts new file mode 100644 index 000000000..afe6e08df --- /dev/null +++ b/plugins/permissions/src/channels/forums/mod.ts @@ -0,0 +1,6 @@ +import { BotWithCache } from "../../../deps.ts"; +import { createForumPost } from "./createForumPost.ts"; + +export default function setupThreadPermChecks(bot: BotWithCache) { + createForumPost(bot); +} diff --git a/plugins/permissions/src/channels/mod.ts b/plugins/permissions/src/channels/mod.ts index 992c3fec2..4fd36f48c 100644 --- a/plugins/permissions/src/channels/mod.ts +++ b/plugins/permissions/src/channels/mod.ts @@ -1,5 +1,6 @@ import { BotWithCache } from "../../deps.ts"; import setupThreadPermChecks from "./threads/mod.ts"; +import setupForumPermChecks from "./forums/mod.ts"; import setupStagePermChecks from "./stage.ts"; import deleteChannel from "./deleteChannel.ts"; import deleteChannelOverwrite from "./deleteChannelOverwrite.ts"; @@ -11,6 +12,7 @@ import swapChannels from "./swapChannels.ts"; export default function setupChannelPermChecks(bot: BotWithCache) { setupThreadPermChecks(bot); + setupForumPermChecks(bot); setupStagePermChecks(bot); deleteChannel(bot); deleteChannelOverwrite(bot); diff --git a/plugins/permissions/src/messages/create.ts b/plugins/permissions/src/messages/create.ts index efffaf6d7..ad9d29cd2 100644 --- a/plugins/permissions/src/messages/create.ts +++ b/plugins/permissions/src/messages/create.ts @@ -19,6 +19,7 @@ export function sendMessage(bot: BotWithCache) { [ ChannelTypes.GuildCategory, ChannelTypes.GuildStageVoice, + ChannelTypes.GuildForum, ].includes(channel.type) ) { throw new Error( diff --git a/template/.gitignore b/template/.gitignore index 35179457f..24c718926 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -3,3 +3,4 @@ fileloader.ts # Folder made by Kwik db to watch guild command versions db/ +node_modules \ No newline at end of file diff --git a/transformers/channel.ts b/transformers/channel.ts index fb04a6952..ccbe70887 100644 --- a/transformers/channel.ts +++ b/transformers/channel.ts @@ -63,6 +63,7 @@ export function transformChannel(bot: Bot, payload: { channel: DiscordChannel } ? Date.parse(payload.channel.thread_metadata.create_timestamp) : undefined, newlyCreated: payload.channel.newly_created, + flags: payload.channel.flags, }; return channel as Optionalize; diff --git a/types/discord.ts b/types/discord.ts index ec3c04978..6a7046291 100644 --- a/types/discord.ts +++ b/types/discord.ts @@ -36,6 +36,7 @@ import { TeamMembershipStates, TextStyles, UserFlags, + ChannelFlags, VerificationLevels, VideoQualityModes, VisibilityTypes, @@ -700,6 +701,8 @@ export interface DiscordVoiceState { export interface DiscordChannel { /** The type of channel */ type: ChannelTypes; + /** The flags of the channel */ + flags?: ChannelFlags; /** Sorting position of the channel */ position?: number; /** The name of the channel (1-100 characters) */ diff --git a/types/shared.ts b/types/shared.ts index 3f5eac824..ba252f615 100644 --- a/types/shared.ts +++ b/types/shared.ts @@ -25,6 +25,12 @@ export enum UserFlags { BotHttpInteractions = 1 << 19, } +/** https://discord.com/developers/docs/resources/channel#channels-resource */ +export enum ChannelFlags { + None, + Pinned = 1 << 1, +} + /** https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors */ export enum IntegrationExpireBehaviors { RemoveRole, @@ -275,6 +281,8 @@ export enum ChannelTypes { GuildStageVoice, /** A channel in a hub containing the listed servers */ GuildDirectory, + /** A channel which can only contains threads */ + GuildForum, } export enum OverwriteTypes { diff --git a/util/constants.ts b/util/constants.ts index 63aea20cf..7b837aec6 100644 --- a/util/constants.ts +++ b/util/constants.ts @@ -70,6 +70,9 @@ export const endpoints = { THREAD_ARCHIVED_PRIVATE_JOINED: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/users/@me/threads/archived/private`, + // Thread -> Forum Endpoints + FORUM_START: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/threads?has_message=true`, + // Guild Endpoints GUILDS: () => `${baseEndpoints.BASE_URL}/guilds`, GUILD_AUDIT_LOGS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/audit-logs`,