!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
!solved, and type !unanswered in a text channel.
How it works, piece by piece
The starter arrives on messageCreate
The starter arrives on messageCreate
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.Sending into a post is a cursor move
Sending into a post is a cursor move
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.A reply pointer is an answer
A reply pointer is an answer
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.Forum channels come from fetchChannels()
Forum channels come from fetchChannels()
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.Following is a preference, not access
Following is a preference, not access
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.No named forum permissions
No named forum permissions
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.