Skip to main content
This bot does three things in every forum channel it can see. When a post opens, it replies inside the post with the forum’s guidelines and follows it. When someone inside a post replies to a message with !solved, it marks that message as the accepted answer. And !unanswered in any text channel lists the open posts that still have no accepted answer. It exercises the whole forum surface: the starter arriving on messageCreate, sending with postId, fetchForumMeta(), fetchForumPosts(), setForumPostState(), and followForumPost().

The full bot

forum-helper-bot.ts

Run it

Then open a post in a forum channel the bot can see. It answers inside the post. Reply to any message in the post with !solved, and type !unanswered in a text channel.

How it works, piece by piece

A post’s first message never rides the ordinary message stream. The SDK emits it from the post’s create event as a messageCreate with starter: true, once, right before the matching forumPostUpdate. Its messageId is ForumPost.starterMessageId. Because a post frame carries its parent on the wire, msg.forumChannelId is always set, which is what the send back into the post needs.
send(serverId, forumChannelId, text, { postId }) names the parent forum and the post. The SDK selects the server and enters the post, which repoints the session’s cursor at it, then sends the ordinary frame. A run of sends into one post enters once. Sending into the forum channel itself, with no postId, is refused with -17 on sendRejected.
setForumPostState(..., 'answered', value) takes anything with messageId, authorId, and createdAt. msg.repliedTo has exactly those, so replying to the solving message with !solved is enough. Archiving and answering allow the post’s author; on anyone else’s post they need message_manage, like locking, pinning, and moving, which is why the deny is reported back into the post rather than swallowed.
ChannelType.FORUM is 4. fetchChannels() is filtered server-side to what the bot may view, so the loop only touches forums it can read. fetchForumPosts() with solved: false returns unanswered posts only; the server caps a page at 50 and hasMore says whether to page with before.
followForumPost() rings the bell for this bot. It changes nothing about delivery: a firehose bot already receives every visible post’s replies. There are no private posts, so nothing gates a post beyond the parent forum’s visibility.
There is no create_posts or manage_posts name, and there never will be. The server’s create-posts capability bottoms out at message_send, resolved against the forum channel (which may override it per channel), so can('message_send', serverId) is the pre-check for opening a post or replying inside one. Moderating other members’ posts is message_manage. The bot declares both and still catches the CloakActionError, because a per-channel override can deny what the server-wide grant allows.

Next

Forums

The full forum surface and its deny codes.

Message history

Read a post’s backlog with fetchMessages(..., { postId }).

Threads

The other container, and how entering it differs.

Events

forumPostUpdate and forumPostDelete.