Skip to content

Commit 6181467

Browse files
committed
snooze(move): auto-unsnooze on reply/any mod message; enforce hidden permissions on auto-created Snoozed Threads and sync perms on move; restore original overwrites on unsnooze; add capacity guard and config docs
1 parent 0990639 commit 6181467

File tree

8 files changed

+379
-63
lines changed

8 files changed

+379
-63
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@ Configuration Options:
3434
* `snooze_text`: Customizes the text for snooze notifications.
3535
* `unsnooze_text`: Customizes the text for unsnooze notifications.
3636
* `unsnooze_notify_channel`: Specifies the channel for unsnooze notifications.
37+
* `snooze_behavior`: Choose between `delete` (legacy) or `move` behavior for snoozing.
38+
* `snoozed_category_id`: Target category for `move` snoozing; required when `snooze_behavior` is `move`.
3739
* `thread_min_characters`: Minimum number of characters required.
3840
* `thread_min_characters_title`: Title shown when the message is too short.
3941
* `thread_min_characters_response`: Response shown to the user if their message is too short.
4042
* `thread_min_characters_footer`: Footer displaying the minimum required characters.
4143

44+
Behavioral changes:
45+
- When `snooze_behavior` is set to `move`, the snoozed category now has a hard limit of 49 channels. New snoozes are blocked once it’s full until space is freed.
46+
- When switching `snooze_behavior` to `move` via `?config set`, the bot reminds admins to set `snoozed_category_id` if it’s missing.
47+
4248
# v4.1.2
4349

4450
### Fixed

bot.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,27 @@ async def process_commands(self, message):
13981398

13991399
thread = await self.threads.find(channel=ctx.channel)
14001400
if thread is not None:
1401+
# If thread is snoozed (moved), auto-unsnooze when a mod sends a message directly in channel
1402+
try:
1403+
behavior = (self.config.get("snooze_behavior") or "delete").lower()
1404+
except Exception:
1405+
behavior = "delete"
1406+
if thread.snoozed and behavior == "move":
1407+
if not thread.snooze_data:
1408+
try:
1409+
log_entry = await self.api.logs.find_one(
1410+
{"recipient.id": str(thread.id), "snoozed": True}
1411+
)
1412+
if log_entry:
1413+
thread.snooze_data = log_entry.get("snooze_data")
1414+
except Exception:
1415+
pass
1416+
try:
1417+
await thread.restore_from_snooze()
1418+
# refresh local cache
1419+
self.threads.cache[thread.id] = thread
1420+
except Exception as e:
1421+
logger.warning("Auto-unsnooze on direct message failed: %s", e)
14011422
anonymous = False
14021423
plain = False
14031424
if self.config.get("anon_reply_without_command"):

cogs/modmail.py

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,9 +2284,11 @@ async def isenable(self, ctx):
22842284
@checks.thread_only()
22852285
async def snooze(self, ctx, *, duration: UserFriendlyTime = None):
22862286
"""
2287-
Snooze this thread: deletes the channel, keeps the ticket open in DM, and restores it when the user replies or a moderator unsnoozes it.
2288-
Optionally specify a duration, e.g. 'snooze 2d' for 2 days.
2289-
Uses config: max_snooze_time, snooze_title, snooze_text
2287+
Snooze this thread. Behavior depends on config:
2288+
- delete (default): deletes the channel and restores it later
2289+
- move: moves the channel to the configured snoozed category
2290+
Optionally specify a duration, e.g. 'snooze 2d' for 2 days.
2291+
Uses config: max_snooze_time, snooze_title, snooze_text
22902292
"""
22912293
thread = ctx.thread
22922294
if thread.snoozed:
@@ -2310,6 +2312,85 @@ async def snooze(self, ctx, *, duration: UserFriendlyTime = None):
23102312
else:
23112313
snooze_for = max_snooze
23122314

2315+
# Capacity pre-check: if behavior is move, ensure snoozed category has room (<49 channels)
2316+
behavior = (self.bot.config.get("snooze_behavior") or "delete").lower()
2317+
if behavior == "move":
2318+
snoozed_cat_id = self.bot.config.get("snoozed_category_id")
2319+
target_category = None
2320+
if snoozed_cat_id:
2321+
try:
2322+
target_category = self.bot.modmail_guild.get_channel(int(snoozed_cat_id))
2323+
except Exception:
2324+
target_category = None
2325+
# Auto-create snoozed category if missing
2326+
if not isinstance(target_category, discord.CategoryChannel):
2327+
try:
2328+
# Hide category by default; only bot can view/manage
2329+
overwrites = {
2330+
self.bot.modmail_guild.default_role: discord.PermissionOverwrite(view_channel=False)
2331+
}
2332+
bot_member = self.bot.modmail_guild.me
2333+
if bot_member is not None:
2334+
overwrites[bot_member] = discord.PermissionOverwrite(
2335+
view_channel=True,
2336+
send_messages=True,
2337+
read_message_history=True,
2338+
manage_channels=True,
2339+
manage_messages=True,
2340+
attach_files=True,
2341+
embed_links=True,
2342+
add_reactions=True,
2343+
)
2344+
target_category = await self.bot.modmail_guild.create_category(
2345+
name="Snoozed Threads",
2346+
overwrites=overwrites,
2347+
reason="Auto-created snoozed category for move-based snoozing",
2348+
)
2349+
try:
2350+
await self.bot.config.set("snoozed_category_id", target_category.id)
2351+
await self.bot.config.update()
2352+
except Exception:
2353+
pass
2354+
await ctx.send(
2355+
embed=discord.Embed(
2356+
title="Snoozed category created",
2357+
description=(
2358+
f"Created category {target_category.mention if hasattr(target_category,'mention') else target_category.name} "
2359+
"and set it as `snoozed_category_id`."
2360+
),
2361+
color=self.bot.main_color,
2362+
)
2363+
)
2364+
except Exception as e:
2365+
await ctx.send(
2366+
embed=discord.Embed(
2367+
title="Could not create snoozed category",
2368+
description=(
2369+
"I couldn't create a category automatically. Please ensure I have Manage Channels "
2370+
"permission, or set `snoozed_category_id` manually."
2371+
),
2372+
color=self.bot.error_color,
2373+
)
2374+
)
2375+
logging.warning("Failed to auto-create snoozed category: %s", e)
2376+
# Capacity check after ensuring category exists
2377+
if isinstance(target_category, discord.CategoryChannel):
2378+
try:
2379+
if len(target_category.channels) >= 49:
2380+
await ctx.send(
2381+
embed=discord.Embed(
2382+
title="Snooze unavailable",
2383+
description=(
2384+
"The configured snoozed category is full (49 channels). "
2385+
"Unsnooze or move some channels out before snoozing more."
2386+
),
2387+
color=self.bot.error_color,
2388+
)
2389+
)
2390+
return
2391+
except Exception:
2392+
pass
2393+
23132394
# Storing snooze_start and snooze_for in the log entry
23142395
now = datetime.now(timezone.utc)
23152396
await self.bot.api.logs.update_one(

cogs/utility.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,31 @@ async def config_set(self, ctx, key: str.lower, *, value: str):
810810
color=self.bot.main_color,
811811
description=f"Set `{key}` to `{self.bot.config[key]}`.",
812812
)
813+
# If turning on move-based snoozing, remind to set snoozed_category_id
814+
if key == "snooze_behavior":
815+
behavior = (
816+
str(self.bot.config.get("snooze_behavior", convert=False)).strip().lower().strip('"')
817+
)
818+
if behavior == "move":
819+
cat_id = self.bot.config.get("snoozed_category_id", convert=False)
820+
valid = False
821+
if cat_id:
822+
try:
823+
cat_obj = self.bot.modmail_guild.get_channel(int(str(cat_id)))
824+
valid = isinstance(cat_obj, discord.CategoryChannel)
825+
except Exception:
826+
valid = False
827+
if not valid:
828+
example = f"`{self.bot.prefix}config set snoozed_category_id <category_id>`"
829+
embed.add_field(
830+
name="Action required",
831+
value=(
832+
"You set `snooze_behavior` to `move`. Please set `snoozed_category_id` "
833+
"to the category where snoozed threads should be moved.\n"
834+
f"For example: {example}"
835+
),
836+
inline=False,
837+
)
813838
except InvalidConfigError as exc:
814839
embed = exc.embed
815840
else:

core/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ class ConfigManager:
140140
"snooze_text": "This thread has been snoozed. The channel will be restored when the user replies or a moderator unsnoozes it.",
141141
"unsnooze_text": "This thread has been unsnoozed and restored.",
142142
"unsnooze_notify_channel": "thread", # Can be a channel ID or 'thread' for the thread's own channel
143+
# snooze behavior
144+
"snooze_behavior": "delete", # 'delete' to delete channel, 'move' to move channel to snoozed_category_id
145+
"snoozed_category_id": None, # Category ID to move snoozed channels into when snooze_behavior == 'move'
143146
}
144147

145148
private_keys = {

core/config_help.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,5 +1322,28 @@
13221322
"If set to a channel ID, the notification will be sent to that specific channel.",
13231323
"See also: `unsnooze_text`, `max_snooze_time`."
13241324
]
1325+
},
1326+
"snooze_behavior": {
1327+
"default": "\"delete\"",
1328+
"description": "Controls how snoozing behaves. 'delete' removes the thread channel and restores it later; 'move' moves the channel into the 'snoozed_category_id' without deleting it.",
1329+
"examples": [
1330+
"`{prefix}config set snooze_behavior delete`",
1331+
"`{prefix}config set snooze_behavior move`"
1332+
],
1333+
"notes": [
1334+
"When set to 'move', set `snoozed_category_id` to a valid Category ID.",
1335+
"When unsnoozed, channels moved will return to their original category and position when possible; if original no longer exists they will be moved under `main_category_id`."
1336+
]
1337+
},
1338+
"snoozed_category_id": {
1339+
"default": "None",
1340+
"description": "The category ID where snoozed threads are moved when `snooze_behavior` is set to 'move'.",
1341+
"examples": [
1342+
"`{prefix}config set snoozed_category_id 123456789012345678`"
1343+
],
1344+
"notes": [
1345+
"Only used when `snooze_behavior` is 'move'.",
1346+
"If not set or invalid, the channel will remain in its current category or the bot will fall back to deleting on failure."
1347+
]
13251348
}
13261349
}

0 commit comments

Comments
 (0)