Skip to content

Commit 71dee9f

Browse files
committed
feat(status-roles): add cog to assign roles based on user status
1 parent 4ded8b1 commit 71dee9f

File tree

3 files changed

+118
-0
lines changed

3 files changed

+118
-0
lines changed

config/settings.yml.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ TEMPVC_CATEGORY_ID: 123456789012345679
5050
# Set this to the channel ID where you want the temporary voice channels to be created.
5151
TEMPVC_CHANNEL_ID: 123456789012345679
5252

53+
# This will automatically give people with a status regex a role.
54+
STATUS_ROLES:
55+
#- server_id: 123456789012345679
56+
# status_regex: ".*"
57+
# role_id: 123456789012345679
5358

5459
SNIPPETS:
5560
LIMIT_TO_ROLE_IDS: false # Only allow users with the specified role IDs to use the snippet command

tux/cogs/services/status_roles.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import re
2+
3+
import discord
4+
from discord.ext import commands
5+
from loguru import logger
6+
7+
from tux.utils.config import CONFIG
8+
9+
10+
class StatusRoles(commands.Cog):
11+
"""Assign roles to users based on their status."""
12+
13+
def __init__(self, bot: commands.Bot):
14+
self.bot = bot
15+
self.status_roles = CONFIG.STATUS_ROLES
16+
logger.info("StatusRoles cog initialized with %d role configurations", len(self.status_roles))
17+
18+
@commands.Cog.listener()
19+
async def on_ready(self):
20+
"""Check all users' statuses when the bot starts up."""
21+
logger.info("StatusRoles cog ready, checking all users' statuses")
22+
for guild in self.bot.guilds:
23+
for member in guild.members:
24+
await self.check_and_update_roles(member)
25+
26+
@commands.Cog.listener()
27+
async def on_presence_update(self, before: discord.Member, after: discord.Member):
28+
"""Event triggered when a user's presence changes."""
29+
logger.debug(f"Presence update for {after.display_name}: {before.status} -> {after.status}")
30+
# Only process if the custom status changed
31+
before_status = self.get_custom_status(before)
32+
after_status = self.get_custom_status(after)
33+
34+
if before_status != after_status or self.has_activity_changed(before, after):
35+
logger.debug(f"Status change detected for {after.display_name}: '{before_status}' -> '{after_status}'")
36+
await self.check_and_update_roles(after)
37+
38+
def has_activity_changed(self, before: discord.Member, after: discord.Member) -> bool:
39+
"""Check if there was a relevant change in activities."""
40+
before_has_custom = (
41+
any(isinstance(a, discord.CustomActivity) for a in before.activities) if before.activities else False
42+
)
43+
after_has_custom = (
44+
any(isinstance(a, discord.CustomActivity) for a in after.activities) if after.activities else False
45+
)
46+
return before_has_custom != after_has_custom
47+
48+
def get_custom_status(self, member: discord.Member) -> str | None:
49+
"""Extract the custom status text from a member's activities."""
50+
if not member.activities:
51+
return None
52+
53+
for activity in member.activities:
54+
if isinstance(activity, discord.CustomActivity) and activity.name:
55+
return activity.name
56+
57+
return None
58+
59+
async def check_and_update_roles(self, member: discord.Member):
60+
"""Check a member's status against configured patterns and update roles accordingly."""
61+
if member.bot:
62+
return
63+
64+
status_text = self.get_custom_status(member)
65+
if status_text is None:
66+
status_text = "" # Use empty string for regex matching if no status
67+
68+
for config in self.status_roles:
69+
# Skip if the config is for a different server
70+
if int(config.get("server_id", 0)) != member.guild.id:
71+
continue
72+
73+
role_id = int(config.get("role_id", 0))
74+
pattern = str(config.get("status_regex", ".*"))
75+
76+
role = member.guild.get_role(role_id)
77+
if not role:
78+
logger.warning(f"Role {role_id} configured in STATUS_ROLES not found in guild {member.guild.name}")
79+
continue
80+
81+
try:
82+
matches = bool(re.search(pattern, status_text, re.IGNORECASE))
83+
84+
has_role = role in member.roles
85+
86+
if matches and not has_role:
87+
# Add role if status matches and member doesn't have the role
88+
logger.info(
89+
f"Adding role {role.name} to {member.display_name} (status: '{status_text}' matched '{pattern}')",
90+
)
91+
await member.add_roles(role, reason="Status role automation")
92+
93+
elif not matches and has_role:
94+
# Remove role if status doesn't match and member has the role
95+
logger.info(f"Removing role {role.name} from {member.display_name} (status no longer matches)")
96+
await member.remove_roles(role, reason="Status role automation")
97+
98+
except re.error:
99+
logger.exception(f"Invalid regex pattern '{pattern}' in STATUS_ROLES config")
100+
except discord.Forbidden:
101+
logger.exception(
102+
f"Bot lacks permission to modify roles for {member.display_name} in {member.guild.name}",
103+
)
104+
except Exception:
105+
logger.exception(f"Error updating roles for {member.display_name}")
106+
107+
108+
async def setup(bot: commands.Bot):
109+
await bot.add_cog(StatusRoles(bot))
110+
logger.info("Loaded StatusRoles cog")

tux/utils/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ class Config:
6363
ACTIVITIES: Final[str] = config["BOT_INFO"]["ACTIVITIES"]
6464
HIDE_BOT_OWNER: Final[bool] = config["BOT_INFO"]["HIDE_BOT_OWNER"]
6565

66+
# Status Roles
67+
STATUS_ROLES: Final[list[dict[str, int]]] = config["STATUS_ROLES"]
68+
6669
# Debug env
6770
DEBUG: Final[bool] = bool(os.getenv("DEBUG", "True"))
6871

0 commit comments

Comments
 (0)