Add deadman switch cog

This commit is contained in:
2026-09-07 15:45:03 -05:00
parent b81f3429d4
commit 9cc3b1ecc1
4 changed files with 310 additions and 0 deletions
+32
View File
@@ -8,6 +8,9 @@ Homepage: https://homepage.kingstons-scrapyard.net/
## Table of Contents
**Deadman**
- [Deadman](#deadman)
**AutoRoom**
- [Installation](#installation)
- [AutoRoom](#autoroom)
@@ -49,6 +52,35 @@ Homepage: https://homepage.kingstons-scrapyard.net/
[p]load autoroom
```
Deadman (bot owner only):
```
[p]cog install scrapyard deadman
[p]load deadman
```
---
## Deadman
A member of the Scrapyard Cogworks package. A deadman switch that alerts listed users if not disabled before it expires.
- **Owner only**: Only the bot owner can arm, disable, or check status.
- **Arm**: `[p]deadman arm <duration> @user1 @user2 [channel]`
- **Disable**: `[p]deadman disable`
- **Status**: `[p]deadman status`
**Duration format** (flexible string):
- `30s` = 30 seconds
- `6h` = 6 hours
- `2h30m` = 2 hours 30 minutes
- `1d` = 1 day
- `1d12h` = 1 day 12 hours
Supported units: `s`, `m`, `h`, `d`, `w` (and their full names).
When the switch is armed, the bot stores the expiry time and alerts users by DM. If an optional channel is provided, it also posts the alert there. If `[p]deadman disable` is not run before expiry, the listed users are DM'd and pinged that "the deadman switch has been TRIGGERED".
---
## AutoRoom
+16
View File
@@ -0,0 +1,16 @@
"""Package for Deadman cog."""
import json
from pathlib import Path
from redbot.core.bot import Red
from .deadman import Deadman
with Path(__file__).parent.joinpath("info.json").open() as fp:
__red_end_user_data_statement__ = json.load(fp)["end_user_data_statement"]
async def setup(bot: Red) -> None:
"""Load Deadman cog."""
await bot.add_cog(Deadman(bot))
+238
View File
@@ -0,0 +1,238 @@
import asyncio
from datetime import datetime, timedelta
from typing import Optional
import discord
from redbot.core import Config, commands
def parse_duration(text: str) -> Optional[timedelta]:
"""Parse a flexible duration string like ``6h``, ``2h30m``, ``90s`` or ``1d``."""
text = text.strip().lower()
if not text:
return None
total = timedelta()
num = ""
unit = ""
seen_unit = False
for char in text:
if char.isdigit():
if seen_unit:
# A digit after a unit means a new component, e.g. "1d12h"
delta = _unit_to_timedelta(int(num), unit)
if delta is None:
return None
total += delta
num = char
unit = ""
seen_unit = False
else:
num += char
else:
if not num:
return None
unit += char
seen_unit = True
if unit:
delta = _unit_to_timedelta(int(num), unit)
if delta is None:
return None
total += delta
elif num:
# Trailing digits with no unit, e.g. "5m30" - invalid.
return None
if total <= timedelta(0):
return None
return total
def _unit_to_timedelta(amount: int, unit: str) -> Optional[timedelta]:
if not amount or not isinstance(amount, int):
return None
if unit and unit.isdigit():
return None
if unit in ("s", "sec", "secs", "second", "seconds"):
return timedelta(seconds=amount)
if unit in ("m", "min", "mins", "minute", "minutes"):
return timedelta(minutes=amount)
if unit in ("h", "hr", "hrs", "hour", "hours"):
return timedelta(hours=amount)
if unit in ("d", "day", "days"):
return timedelta(days=amount)
if unit in ("w", "week", "weeks"):
return timedelta(weeks=amount)
return None
class Deadman(commands.Cog):
"""Deadman switch - pings listed users if [p]deadman disable isn't run before expiration."""
def __init__(self, bot):
self.bot = bot
self.config = Config.get_conf(self, identifier=918273645)
self.config.register_global(
armed=False,
expires_at=None,
users=[],
trigger_channel=None,
)
self._task = None
self._triggered = False
async def red_delete_data_for_user(self, **kwargs):
return
def cog_unload(self):
if self._task:
self._task.cancel()
async def cog_load(self):
self._task = asyncio.create_task(self._monitor())
async def _monitor(self):
"""Background task that checks every 30 seconds whether the switch has expired."""
while True:
try:
await asyncio.sleep(30)
await self._check_expiry()
except asyncio.CancelledError:
break
except Exception:
pass
async def _check_expiry(self):
if not await self.config.armed():
self._triggered = False
return
expires_at = await self.config.expires_at()
if expires_at is None:
return
if datetime.utcnow() >= datetime.fromisoformat(expires_at):
if not self._triggered:
await self._trigger()
# Keep armed/stale so repeated pings don't spam; disable clears state.
async def _trigger(self):
self._triggered = True
users = await self.config.users()
channel_id = await self.config.trigger_channel()
victims: list[discord.User] = []
for uid in users:
user = self.bot.get_user(uid)
if user is None:
try:
user = await self.bot.fetch_user(uid)
except Exception:
continue
if user is not None:
victims.append(user)
for user in victims:
try:
await user.send(
"**GENERAL ALERT**\nThe deadman switch has been TRIGGERED. "
"Please contact the operator immediately."
)
except discord.Forbidden:
pass
except Exception:
pass
if channel_id:
channel = self.bot.get_channel(channel_id)
if channel:
mentions = " ".join(
f"<@{uid}>" for uid in users if self.bot.get_user(uid)
)
try:
await channel.send(
f"**GENERAL ALERT**\nThe deadman switch has been TRIGGERED. "
f"{mentions} Please contact the operator immediately."
)
except discord.Forbidden:
pass
except Exception:
pass
@commands.group()
@commands.is_owner()
async def deadman(self, ctx):
"""Deadman switch control."""
@deadman.command(name="arm")
async def deadman_arm(self, ctx, duration: str, users: commands.Greedy[discord.User], trigger_channel: Optional[discord.TextChannel] = None):
"""Arm the deadman switch for a duration, pinging listed users if not disabled in time.
Duration format: a flexible string like ``6h``, ``2h30m``, ``90s``, ``1d``, or a combination.
Users: mention the users to alert (e.g. ``@user1 @user2``).
Optional: a channel where the alert should also be posted.
"""
if not users:
await ctx.send("You must provide at least one user to alert.")
return
duration_td = parse_duration(duration)
if duration_td is None:
await ctx.send(
"Invalid duration. Examples: `6h`, `2h30m`, `90s`, `1d`, `1d12h`."
)
return
expires_at = datetime.utcnow() + duration_td
await self.config.armed.set(True)
await self.config.expires_at.set(expires_at.isoformat())
await self.config.users.set([u.id for u in users])
await self.config.trigger_channel.set(trigger_channel.id if trigger_channel else None)
self._triggered = False
expires_str = expires_at.strftime("%Y-%m-%d %H:%M UTC")
await ctx.send(
f"Deadman switch ARMED.\n"
f"Expires: **{expires_str}** (in {duration_td})\n"
f"Users notified: {', '.join(u.mention for u in users)}\n"
+ (f"Alert channel: {trigger_channel.mention}\n" if trigger_channel else "\n")
+ "Run `[p]deadman disable` before that time to cancel."
)
@deadman.command(name="disable")
async def deadman_disable(self, ctx):
"""Disable the currently armed deadman switch."""
armed = await self.config.armed()
if not armed:
await ctx.send("The deadman switch is not currently armed.")
return
await self.config.armed.set(False)
await self.config.expires_at.set(None)
await self.config.users.set([])
await self.config.trigger_channel.set(None)
self._triggered = False
await ctx.send("Deadman switch disabled.")
@deadman.command(name="status")
async def deadman_status(self, ctx):
"""Show the current deadman switch status."""
armed = await self.config.armed()
if not armed:
await ctx.send("The deadman switch is currently **disarmed**.")
return
expires_at = await self.config.expires_at()
users = await self.config.users()
expires_dt = datetime.fromisoformat(expires_at)
remaining = expires_dt - datetime.utcnow()
mentions = ", ".join(f"<@{uid}>" for uid in users) if users else "None"
await ctx.send(
f"Deadman switch is **ARMED**.\n"
f"Expires at: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')}\n"
f"Time remaining: {remaining}\n"
f"Users to alert: {mentions}"
)
+24
View File
@@ -0,0 +1,24 @@
{
"name": "Deadman",
"author": [
"Scrapyard Cogworks"
],
"short": "Deadman switch that pings listed users if not disabled in time.",
"description": "Arm a deadman switch with a duration. If [p]deadman disable is not run before it expires, the bot DMs and pings all listed users that the switch has been triggered. Bot owner only.",
"install_msg": "Thanks for installing Deadman from Scrapyard Cogworks! Visit us at https://homepage.kingstons-scrapyard.net/",
"requirements": [],
"tags": [
"deadman",
"switch",
"alert",
"safety",
"ping"
],
"min_bot_version": "3.5.0",
"min_python_version": [
3,
11,
0
],
"end_user_data_statement": "This cog stores Discord user and channel IDs of configured participants. It does not store message content or profane data."
}