Skip to content

Commit 5bb76d5

Browse files
authored
Automatic Thread Creation (#53)
* feat(auto-threads): Add auto-thread creation and config schema Automatically create threads from messages in configured channels. Adds a call to MessageCreate._handleAutoThreads and implements the handler to lookup per-channel auto-thread config, skip users with excluded roles, and start a thread with a templated name (supports $USERNAME, $SURFACE_NAME, $USER_ID). Updates the guild config schema with autoThreadSchema and registers auto_threads in rawGuildConfigSchema (includes channel_id, name with placeholders, and exclude_roles). Thread creation errors are caught to avoid unhandled rejections. * fix(eslint): Resolve trailing comma eslint error * style(config-schema): Use spaces for indentation * feat(config-schema): Replace role exclusion with full role scoping in auto-threads * feat(auto-thread-creation): Add full role scoping logic to auto-threads * fix(eslint): Resolve unnecessary nullish coalescing use eslint error * docs(auto-threads): Add auto-thread creation feature
1 parent dfef634 commit 5bb76d5

4 files changed

Lines changed: 52 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ A Discord moderation and utility bot built with [Bun](https://bun.sh), [discord.
2323
- **Moderation Activity** — View staff moderation stats (infractions dealt, requests reviewed/made), filterable by month and year.
2424
- **Auto-Publish** — Automatically crosspost messages in announcement channels.
2525
- **Auto-Reactions** — Add configured reactions to messages in specified channels.
26+
- **Auto-Threads** — Automatically start threads on messages in specified channels.
2627
- **Media Channels** — Enforce attachment requirements in specified channels.
2728
- **Scheduled Messages** — Cron-based scheduled messages with Sentry monitor slugs.
2829
- **Role Requests** — Configurable role request channels with optional TTL.

docs/configuration.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ auto_reactions:
124124
exclude_patterns: ["regex"] # Optional
125125
```
126126

127+
### Auto-Threads
128+
129+
```yaml
130+
auto_threads:
131+
- channel_id: "<channel_id>"
132+
# $USERNAME = user's username
133+
# $SURFACE_NAME = user's username and surface name, if any
134+
# $USER_ID = user's ID
135+
name: "$SURFACE_NAME's thread"
136+
role_scoping:
137+
include_roles: ["<role_id>"]
138+
exclude_roles: ["<role_id>"]
139+
```
140+
127141
### Media Channels
128142

129143
```yaml

src/events/MessageCreate.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default class MessageCreate extends EventListener {
6161

6262
MessageCache.cache(message);
6363
MessageCreate._handleAutoReactions(message, config);
64+
MessageCreate._handleAutoThreads(message, config);
6465
await MessageCreate._handleMediaChannel(message, config);
6566

6667
// Handle media conversion
@@ -235,6 +236,25 @@ export default class MessageCreate extends EventListener {
235236
}
236237
}
237238

239+
private static _handleAutoThreads(message: Message<true>, config: GuildConfig): void {
240+
const autoThreadChannel = config.data.auto_threads
241+
.find(autoThreadChannel => autoThreadChannel.channel_id === message.channel.id);
242+
243+
if (!autoThreadChannel || !message.member) return;
244+
245+
const inScope = config.roleInScope(message.member, autoThreadChannel.role_scoping);
246+
247+
if (!inScope) return;
248+
249+
message.startThread({
250+
name: autoThreadChannel.name
251+
.replace("$USERNAME", `@${message.author.username}`)
252+
.replace("$SURFACE_NAME", getSurfaceName(message.member))
253+
.replace("$USER_ID", message.author.id),
254+
reason: `Auto-thread created from @${message.author.username} (${message.author.id})'s message with ID ${message.id} in #${message.channel.name}`
255+
}).catch(() => null);
256+
}
257+
238258
private static async _handleMediaConversion(message: Message<true>, config: GuildConfig): Promise<void> {
239259
if (!message.attachments.size || message.content) return;
240260

src/managers/config/schema.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,22 @@ const autoReactionSchema = z.object({
299299
exclude_patterns: z.array(stringSchema).default([])
300300
});
301301

302+
const autoThreadSchema = z.object({
303+
// The channel to listen for messages in
304+
channel_id: snowflakeSchema,
305+
/**
306+
* The name of the thread to create
307+
*
308+
* ## Args
309+
*
310+
* - `$USERNAME`: The username of the message author
311+
* - `$SURFACE_NAME`: The username and, if available, visible name of the message author on the current surface (e.g., their nickname in that guild)
312+
* - `$USER_ID`: The ID of the message author
313+
*/
314+
name: placeholderString(["USERNAME", "SURFACE_NAME", "USER_ID"], 1, 100).default("$SURFACE_NAME's thread"),
315+
role_scoping: roleScopingSchema.default({})
316+
});
317+
302318
const reportSchema = z.object({
303319
// Channel to send reports to
304320
channel_id: snowflakeSchema,
@@ -546,6 +562,7 @@ export const rawGuildConfigSchema = z.object({
546562
// Automatically publish announcement messages in these channels
547563
auto_publish_announcements: z.array(snowflakeSchema).default([]),
548564
auto_reactions: z.array(autoReactionSchema).default([]),
565+
auto_threads: z.array(autoThreadSchema).default([]),
549566
// Toggle the `SendMessages` permission in a channel depending on whether a stage event is active
550567
stage_event_overrides: z.array(stageEventOverrideSchema).default([]),
551568
notification_channel_id: snowflakeSchema.optional(),

0 commit comments

Comments
 (0)