hugging-science/HuggingMod
0
1import discord2import os3import threading4import gradio as gr5import requests6import json7import random8import time9import re10from discord import Embed, Color11from discord.ext import commands12from gradio_client import Client13from PIL import Image14from datetime import datetime, timedelta # for times15from pytz import timezone # for times16import asyncio # check if used17import logging18import urllib.parse19 20from discord.ui import Button, View21 22zurich_tz = timezone("Europe/Zurich")23 24def convert_to_timezone(dt, tz):25 return dt.astimezone(tz).strftime("%Y-%m-%d %H:%M:%S %Z")26 27DISCORD_TOKEN = os.environ.get("DISCORD_TOKEN", None)28intents = discord.Intents.all()29bot = commands.Bot(command_prefix='!', intents=intents, max_messages=1000000)30 31logger = logging.getLogger(__name__)32logging.basicConfig(level=logging.DEBUG)33 34# ============================================================35# CONFIGURATION โ edit these for your server36# ============================================================37GUILD_ID = 1417920887549857874 # your server's ID38LOG_CHANNEL_ID = 1418169465555910699 # channel where mod events are posted39ADMIN_USER_ID = 847651116863062047 # ban-appeal contact + owner of !role_buttons40EXEMPT_ROLE_IDS = { # roles exempt from spam detection / autoban41 1418158261735264327,42 1419966815559225344,43 1418179704065884243,44 1418886227830112297,45 1418510579894845472,46}47ALERT_ROLE_ID = 1418510579894845472 # role pinged on spam alerts / bans48ADMIN_CHANNEL_ID = None # set to a channel ID to suppress spam warnings there49SELF_ASSIGN_ROLE_IDS = [] # role IDs for !role_buttons (empty disables)50# ============================================================51 52#rate_limiter = RateLimiter(max_calls=10, period=60) # needs testing53message_cache = {}54 55AUTO_BAN_ALERT_THRESHOLD = 756AUTO_BAN_EXEMPT_ROLE_IDS = EXEMPT_ROLE_IDS57 58def is_exempt(member: discord.Member) -> bool:59 return any(getattr(r, "id", None) in AUTO_BAN_EXEMPT_ROLE_IDS for r in getattr(member, "roles", []))60 61 62# stats stuff ---------------------------------------------------------------------------------------------------------------------------------------------------------63number_of_messages = 064user_cooldowns = {}65 66@bot.event67async def on_message(message):68 try: 69 global number_of_messages70 if message.author != bot.user:71 message_cache[message.id] = message72 admin_user = bot.get_user(ADMIN_USER_ID)73 74 """Backup"""75 76 number_of_messages = number_of_messages + 177 message_link = f"[#{urllib.parse.quote(message.channel.name)}]({message.jump_url})"78 msgcnt = message.content 79 backup_message = f"{number_of_messages} | {message_link} | {message.author.id} | {message.author}: {msgcnt}"80 # check for attachments81 if message.attachments:82 for attachment in message.attachments:83 attachment_url = attachment.url84 backup_message += f"\nAttachment: {attachment_url}"85 # check for embeds86 if message.embeds:87 for embed in message.embeds:88 backup_message += f"\nEmbed Title: {embed.title}\nEmbed Description: {embed.description}"89 if admin_user is not None:90 dm_message = await admin_user.send(backup_message)91 92 """Antispam"""93 #Detecting certain unwanted strings94 try:95 forbidden_patterns = [r"@everyone",96 r"@here",97 r"(https?:\/\/|http?:\/\/)?(www.)?(discord.(gg|io|me|li)|discordapp.com\/invite|discord.com\/invite)\/[^\s\/]+?(?=\b)"]98 if any(re.search(pattern, message.content, re.IGNORECASE) for pattern in forbidden_patterns):99 ignored_role_ids = list(EXEMPT_ROLE_IDS)100 if any(role.id in ignored_role_ids for role in message.author.roles):101 if message.author.id != ADMIN_USER_ID:102 return103 if admin_user is not None:104 dm_unwanted = await admin_user.send(f" {admin_user.mention} [experimental] SUSPICIOUS MESSAGE: {message_link} | {message.author}: {message.content}")105 except Exception as e:106 print(f"Antispam->Detecting certain unwanted strings Error: {e}")107 108 #Posting too fast109 """110 cooldown_duration determines the time window within which the bot tracks a user's posting behavior.111 This is useful for detecting "staggered" instances of spam, where 20-50 messages are sent 2-10s apart, 112 over timespans of typically a few minutes.113 114 If a user hasn't posted anything for a duration longer than cooldown_duration, their record is cleared, 115 and they start fresh if they post again.116 117 If a user posts within the cooldown_duration, their activity count is updated, 118 and their record persists until it exceeds the specified threshold or until the cooldown_duration window resets.119 120 Increasing cooldown_duration = More robust at detecting "staggered" / "delayed" spam, but more false positives (fast chatters)121 122 """123 124 # cooldown_duration = 3; false_positive_threshld = 3; -> 99% spam at spam_count of 10+ (could still be wrong, so we timeout)125 cooldown_duration = 7 # messages per n seconds, was 1, now 3, could try 5 (adjusted 5->7)126 false_positive_threshold = 5 # big = alert less (catch less spam), small = alert more (catch more spam) # was 4127 timeout_threshold = 10 # number of messages before issuing a timeout (similar function to ban, easier to reverse)128 timeout_duration = 168 # timeout duration in hours (1 week)129 130 if message.author.id not in user_cooldowns:131 user_cooldowns[message.author.id] = {'count': 1, 'timestamp': message.created_at}132 else:133 if (message.created_at - user_cooldowns[message.author.id]['timestamp']).total_seconds() > cooldown_duration:134 var1 = message.created_at135 var2 = user_cooldowns[message.author.id]['timestamp']136 print(f"seconds since last message by {message.author}: ({var1} - {var2}).seconds = {(var1 - var2).total_seconds()}")137 138 # if we wait longer than cooldown_duration, count will reset 139 user_cooldowns[message.author.id] = {'count': 1, 'timestamp': message.created_at}140 else:141 user_cooldowns[message.author.id]['count'] += 1142 spam_count = user_cooldowns[message.author.id]['count']143 144 # tldr; if we post 2 messages with less than [cooldown_duration]seconds between them145 if spam_count >= false_positive_threshold: # n in a row, helps avoid false positives for posting in threads146 # warning for 4+147 channel = message.channel148 if spam_count == false_positive_threshold:149 if ADMIN_CHANNEL_ID is None or channel.id != ADMIN_CHANNEL_ID:150 await channel.send(f"{message.author.mention}, you may be posting too quickly! Please slow down a bit ๐ค")151 152 var1 = message.created_at153 var2 = user_cooldowns[message.author.id]['timestamp']154 print(f"seconds since last message by {message.author}: {(var1 - var2).total_seconds()}") 155 print(f"spam_count: {spam_count}")156 157 alert = f"<@&{ALERT_ROLE_ID}>"158 159 await bot.log_channel.send(160 f"[EXPERIMENTAL ALERT] {message.author} may be posting too quickly! \n"161 f"Spam count: {spam_count}\n"162 f"Message content: {message.content}\n"163 f"[Jump to message!](https://discord.com/channels/{message.guild.id}/{message.channel.id}/{message.id})\n"164 f"{alert}"165 )166 167 # AUTO BAN ======================================================================================================168 # Only in guild text contexts169 if not message.guild:170 # DMs or weird contexts โ never autoban171 pass172 else:173 # Resolve a proper Member object174 member = message.author if isinstance(message.author, discord.Member) else message.guild.get_member(message.author.id)175 176 if member is None:177 print("Autoban: could not resolve Member, skipping.")178 else:179 # Prepare alert ping safely180 alert = f"<@&{ALERT_ROLE_ID}>"181 182 # Skip bots and exempt roles183 if member.bot or is_exempt(member):184 pass185 elif spam_count >= AUTO_BAN_ALERT_THRESHOLD:186 me: discord.Member = message.guild.me187 can_ban = (188 isinstance(me, discord.Member)189 and me.guild_permissions.ban_members190 and (member.top_role < me.top_role) # role hierarchy check191 )192 193 if not can_ban:194 try:195 await bot.log_channel.send(196 f"โ Auto-ban skipped for {member.mention} โ missing `Ban Members` or role hierarchy issue."197 )198 except Exception as e:199 print(f"Autoban permission log error: {e}")200 else:201 try:202 # Try to DM; ignore failures203 try:204 await member.send(205 f"You have been automatically banned from **{message.guild.name}** for repeated spam. "206 f"If this was an error, please contact <@{ADMIN_USER_ID}>."207 )208 except Exception as dm_err:209 print(f"Could not DM user before ban: {dm_err}")210 211 await member.ban(212 delete_message_seconds=600, # delete all messages from past 10 minutes213 reason=f"Auto-ban: reached {spam_count} spam alerts within ~{cooldown_duration}s inter-arrival window"214 )215 216 try:217 await bot.log_channel.send(218 f"๐จ **AUTO-BANNED** {member.mention} ({member.id}) for repeated spam "219 f"({spam_count}/{AUTO_BAN_ALERT_THRESHOLD} within ~{cooldown_duration}s). {alert}"220 )221 except Exception as log_err:222 print(f"Autoban log error: {log_err}")223 224 except discord.Forbidden:225 await bot.log_channel.send(226 f"โ Forbidden when auto-banning {member.mention} โ check role order / perms."227 )228 except discord.HTTPException as e:229 await bot.log_channel.send(f"โ HTTPException during auto-ban for {member.mention}: `{e}`")230 except Exception as e:231 await bot.log_channel.send(f"โ Unexpected error during auto-ban for {member.mention}: `{e}`")232 233 # Reset their burst window so we don't double-trigger234 user_cooldowns.pop(member.id, None)235 # ================= END AUTO BAN (hardened) =================236 237 user_cooldowns[message.author.id]['timestamp'] = message.created_at 238 239 await bot.process_commands(message)240 241 except Exception as e:242 print(f"on_message Error: {e}") 243 244 245# moderation stuff-----------------------------------------------------------------------------------------------------------------------------------------------------246 247@bot.event248async def on_message_edit(before, after):249 try:250 if before.author == bot.user:251 return252 253 if before.content != after.content:254 embed = Embed(color=Color.orange())255 embed.set_author(name=f"{before.author} ID: {before.author.id}", icon_url=before.author.avatar.url if before.author.avatar else bot.user.avatar.url)256 embed.title = "Message Edited"257 embed.description = f"**Before:** {before.content or '*(empty message)*'}\n**After:** {after.content or '*(empty message)*'}"258 embed.add_field(name="Author Username", value=before.author.name, inline=True)259 embed.add_field(name="Channel", value=before.channel.mention, inline=True)260 #embed.add_field(name="Message Created On", value=before.created_at.strftime("%Y-%m-%d %H:%M:%S UTC"), inline=True)261 embed.add_field(name="Message Created On", value=convert_to_timezone(before.created_at, zurich_tz), inline=True)262 embed.add_field(name="Message ID", value=before.id, inline=True)263 embed.add_field(name="Message Jump URL", value=f"[Jump to message!](https://discord.com/channels/{before.guild.id}/{before.channel.id}/{before.id})", inline=True)264 if before.attachments:265 attachment_urls = "\n".join([attachment.url for attachment in before.attachments])266 embed.add_field(name="Attachments", value=attachment_urls, inline=False)267 #embed.set_footer(text=f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}")268 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")269 await bot.log_channel.send(embed=embed)270 271 except Exception as e:272 print(f"on_message_edit Error: {e}") 273 274@bot.event275async def on_raw_message_delete(payload):276 try: 277 message_id = payload.message_id278 channel_id = payload.channel_id279 message = message_cache.pop(message_id, None)280 281 if message:282 if message.author == bot.user:283 return284 285 embed = Embed(color=Color.red())286 embed.set_author(name=f"{message.author} ID: {message.author.id}", icon_url=message.author.avatar.url if message.author.avatar else bot.user.avatar.url)287 embed.title = "Message Deleted"288 embed.description = message.content or "*(empty message)*"289 embed.add_field(name="Author Username", value=message.author.name, inline=True)290 embed.add_field(name="Channel", value=message.channel.mention, inline=True)291 #embed.add_field(name="Message Created On", value=message.created_at.strftime("%Y-%m-%d %H:%M:%S UTC"), inline=True)292 embed.add_field(name="Message Created On", value=convert_to_timezone(message.created_at, zurich_tz), inline=True)293 embed.add_field(name="Message ID", value=message.id, inline=True)294 embed.add_field(name="Message Jump URL", value=f"[Jump to message!](https://discord.com/channels/{message.guild.id}/{message.channel.id}/{message.id})", inline=True)295 if message.attachments:296 attachment_urls = "\n".join([attachment.url for attachment in message.attachments])297 embed.add_field(name="Attachments", value=attachment_urls, inline=False)298 #embed.set_footer(text=f"{datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}")299 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")300 await bot.log_channel.send(embed=embed)301 302 except Exception as e:303 print(f"on_message_delete Error: {e}") 304 305# nickname stuff ---------------------------------------------------------------------------------------------------------------------------306@bot.event307async def on_member_update(before, after):308 try: 309 """310 if before.name != after.name:311 async for entry in before.guild.audit_logs(limit=5):312 print(f'{entry.user} did {entry.action} to {entry.target}') 313 """314 if before.nick != after.nick:315 embed = Embed(color=Color.orange())316 embed.set_author(name=f"{after} ID: {after.id}", icon_url=after.avatar.url if after.avatar else bot.user.avatar.url)317 embed.title = "Nickname Modified"318 embed.add_field(name="Mention", value=after.mention, inline=True)319 embed.add_field(name="Old", value=before.nick, inline=True)320 embed.add_field(name="New", value=after.nick, inline=True)321 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")322 await bot.log_channel.send(embed=embed)323 324 # roles being added/removed325 before_roles = set(before.roles)326 after_roles = set(after.roles)327 328 # added329 added_roles = after_roles - before_roles330 for role in added_roles:331 async for entry in after.guild.audit_logs(action=discord.AuditLogAction.member_role_update, limit=5):332 if entry.target == after and role in entry.changes.after.roles:333 moderator = entry.user334 break335 else:336 moderator = "Unknown"337 338 embed = Embed(color=Color.green())339 embed.set_author(name=f"{after} ID: {after.id}", icon_url=after.avatar.url if after.avatar else bot.user.avatar.url)340 embed.title = "Role Added"341 embed.add_field(name="User", value=after.mention, inline=True)342 embed.add_field(name="Role", value=f"{role.name} ({role.mention})", inline=True)343 embed.add_field(name="Added By", value=moderator.mention if isinstance(moderator, discord.Member) else "Unknown", inline=True)344 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")345 await bot.log_channel.send(embed=embed)346 347 # removed348 removed_roles = before_roles - after_roles349 for role in removed_roles:350 async for entry in after.guild.audit_logs(action=discord.AuditLogAction.member_role_update, limit=5):351 if entry.target == after and role in entry.changes.before.roles:352 moderator = entry.user353 break354 else:355 moderator = "Unknown"356 357 embed = Embed(color=Color.red())358 embed.set_author(name=f"{after} ID: {after.id}", icon_url=after.avatar.url if after.avatar else bot.user.avatar.url)359 embed.title = "Role Removed"360 embed.add_field(name="User", value=after.mention, inline=True)361 embed.add_field(name="Role", value=f"{role.name} ({role.mention})", inline=True)362 embed.add_field(name="Removed By", value=moderator.mention if isinstance(moderator, discord.Member) else "Unknown", inline=True)363 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")364 await bot.log_channel.send(embed=embed)365 366 except Exception as e:367 print(f"on_member_update Error: {e}") 368 369 370@bot.event371async def on_member_ban(guild, banned_user):372 try:373 await asyncio.sleep(1)374 entry1 = await guild.fetch_ban(banned_user)375 ban_reason = entry1.reason376 print(f"ban_reason: {ban_reason}")377 378 async for entry2 in guild.audit_logs(action=discord.AuditLogAction.ban, limit=1):379 if ban_reason:380 print(f'{entry2.user} banned {entry2.target} for {ban_reason}')381 else:382 print(f'{entry2.user} banned {entry2.target} (no reason specified)') 383 384 content = f"<@&{ALERT_ROLE_ID}>"385 embed = Embed(color=Color.red())386 embed.set_author(name=f"{entry2.target} ID: {entry2.target.id}", icon_url=entry2.target.avatar.url if entry2.target.avatar else bot.user.avatar.url)387 embed.title = "User Banned"388 embed.add_field(name="User", value=entry2.target.mention, inline=True)389 #nickname = entry2.target.nick if entry2.target.nick else "None"390 #embed.add_field(name="Nickname", value=nicknmae, inline=True)391 #embed.add_field(name="Account Created At", value=entry2.target.created_at, inline=True)392 embed.add_field(name="Moderator", value=entry2.user.mention, inline=True)393 embed.add_field(name="Nickname", value=entry2.user.nick, inline=True)394 embed.add_field(name="Reason", value=ban_reason, inline=False)395 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")396 397 await bot.log_channel.send(content=content, embed=embed)398 399 try:400 dm_message = await banned_user.send(f"You've been banned from {guild.name}. To appeal, reach out to <@{ADMIN_USER_ID}> via DM")401 except Exception as e:402 print(f"Could not send DM to banned user: {e}")403 404 405 except Exception as e:406 print(f"on_member_ban Error: {e}") 407 408 409@bot.event410async def on_member_unban(guild, unbanned_user):411 try:412 await asyncio.sleep(5)413 async for entry in guild.audit_logs(action=discord.AuditLogAction.unban, limit=1):414 if unbanned_user == entry.target: # verify that unbanned user is in audit log415 moderator = entry.user 416 417 created_and_age = f"{unbanned_user.created_at}"418 content = f"<@&{ALERT_ROLE_ID}>"419 embed = Embed(color=Color.red())420 embed.set_author(name=f"{unbanned_user} ID: {unbanned_user.id}", icon_url=unbanned_user.avatar.url if unbanned_user.avatar else bot.user.avatar.url)421 embed.title = "User Unbanned"422 embed.add_field(name="User", value=unbanned_user.mention, inline=True)423 embed.add_field(name="Account Created At", value=created_and_age, inline=True)424 embed.add_field(name="Moderator", value=moderator.mention, inline=True)425 embed.add_field(name="Nickname", value=moderator.nick, inline=True)426 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")427 428 await bot.log_channel.send(content=content, embed=embed)429 430 except Exception as e:431 print(f"on_member_unban Error: {e}") 432 433# admin stuff-----------------------------------------------------------------------------------------------------------------------434 435 436@bot.event437async def on_member_join(member):438 try:439 await asyncio.sleep(5)440 guild = bot.get_guild(GUILD_ID)441 442 embed = Embed(color=Color.blue())443 avatar_url = member.avatar.url if member.avatar else bot.user.avatar.url444 embed.set_author(name=f"{member} ID: {member.id}", icon_url=avatar_url)445 embed.title = "User Joined"446 embed.add_field(name="Mention", value=member.mention, inline=True)447 embed.add_field(name="Nickname", value=member.nick, inline=True)448 embed.add_field(name="Account Created At", value=member.created_at, inline=True)449 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")450 await bot.log_channel.send(embed=embed) 451 452 except Exception as e:453 print(f"on_member_join Error: {e}") 454 455 456@bot.event457async def on_member_remove(member):458 try:459 embed = Embed(color=Color.blue())460 embed.set_author(name=f"{member} ID: {member.id}", icon_url=member.avatar.url if member.avatar else bot.user.avatar.url)461 embed.title = "User Left"462 embed.add_field(name="Mention", value=member.mention, inline=True)463 embed.add_field(name="Nickname", value=member.nick, inline=True)464 embed.add_field(name="Account Created At", value=member.created_at, inline=True)465 embed.set_footer(text=f"{convert_to_timezone(datetime.utcnow(), zurich_tz)}")466 await bot.log_channel.send(embed=embed) 467 468 except Exception as e:469 print(f"on_member_remove Error: {e}") 470 471 472@bot.event473async def on_guild_channel_create(channel):474 try:475 # creating channels476 embed = Embed(description=f'Channel {channel.mention} was created', color=Color.green())477 await bot.log_channel.send(embed=embed)478 except Exception as e:479 print(f"on_guild_channel_create Error: {e}") 480 481 482@bot.event483async def on_guild_channel_delete(channel):484 try:485 # deleting channels, should ping @alerts for this486 embed = Embed(description=f'Channel {channel.name} ({channel.mention}) was deleted', color=Color.red())487 await bot.log_channel.send(embed=embed)488 except Exception as e:489 print(f"on_guild_channel_delete Error: {e}") 490 491 492@bot.event493async def on_guild_role_create(role):494 try:495 # creating roles496 async for entry in role.guild.audit_logs(action=discord.AuditLogAction.role_create, limit=5):497 if entry.target.id == role.id:498 creator = entry.user499 break500 else:501 creator = None502 503 embed = Embed(description=f'Role {role.mention} was created', color=Color.green())504 embed.add_field(name="Role Name", value=role.name, inline=True)505 embed.add_field(name="Created By", value=creator.mention if creator else "Unknown", inline=True)506 embed.set_footer(text=f"Role ID: {role.id}")507 await bot.log_channel.send(embed=embed)508 509 except Exception as e:510 print(f"on_guild_role_create Error: {e}")511 512 513@bot.event514async def on_guild_role_delete(role):515 try:516 # deleting roles, should ping @alerts for this517 async for entry in role.guild.audit_logs(action=discord.AuditLogAction.role_delete, limit=5):518 if entry.target.id == role.id:519 deleter = entry.user520 break521 else:522 deleter = None523 524 embed = Embed(description=f'Role {role.name} ({role.mention}) was deleted', color=Color.red())525 embed.add_field(name="Deleted By", value=deleter.mention if deleter else "Unknown", inline=True)526 embed.set_footer(text=f"Role ID: {role.id}")527 await bot.log_channel.send(embed=embed)528 529 except Exception as e:530 print(f"on_guild_role_delete Error: {e}")531 532 533 534 535@bot.event536async def on_guild_role_update(before, after):537 try:538 # Track name changes539 if before.name != after.name:540 async for entry in after.guild.audit_logs(action=discord.AuditLogAction.role_update, limit=5):541 if entry.target.id == after.id and 'name' in entry.changes:542 changer = entry.user543 break544 else:545 changer = None546 547 embed = Embed(description=f'Role {before.mention} was renamed to {after.name}', color=Color.orange())548 embed.add_field(name="Changed By", value=changer.mention if changer else "Unknown", inline=True)549 await bot.log_channel.send(embed=embed)550 551 if before.permissions != after.permissions:552 # Find the user who changed the permissions553 async for entry in after.guild.audit_logs(action=discord.AuditLogAction.role_update, limit=5):554 if entry.target.id == after.id:555 changer = entry.user556 break557 else:558 changer = None559 560 # what changed?561 changed_permissions = []562 for perm, value in after.permissions:563 if getattr(before.permissions, perm) != value:564 change_status = "enabled" if value else "disabled"565 changed_permissions.append(f"{perm.replace('_', ' ').title()}: {change_status}")566 567 embed = Embed(color=Color.red() if "administrator" in changed_permissions else Color.orange())568 embed.set_author(name=f"{after.name} Role Updated", icon_url=after.guild.icon.url if after.guild.icon else "")569 embed.add_field(name="Changed By", value=changer.mention if changer else "Unknown", inline=True)570 embed.add_field(name="Changes", value="\n".join(changed_permissions) if changed_permissions else "No permissions changed", inline=False)571 embed.set_footer(text=f"Role ID: {after.id}")572 await bot.log_channel.send(embed=embed)573 574 except Exception as e:575 print(f"on_guild_role_update Error: {e}")576 577 578@bot.event579async def on_voice_state_update(member, before, after):580 try:581 if before.mute != after.mute:582 # muting members583 embed = Embed(description=f'{member} was {"muted" if after.mute else "unmuted"} in voice chat', color=Color.orange())584 await bot.log_channel.send(embed=embed)585 586 if before.deaf != after.deaf:587 # deafening members588 embed = Embed(description=f'{member} was {"deafened" if after.deaf else "undeafened"} in voice chat', color=Color.orange())589 await bot.log_channel.send(embed=embed)590 except Exception as e:591 print(f"on_voice_state_update Error: {e}") 592 593# -------------------------------------------------------------------------------------------------------------------------------------594 595# Custom persistent button that toggles a role for the clicking user.596class RoleToggleButton(Button):597 def __init__(self, role: discord.Role):598 # Assign a fixed custom_id that uniquely identifies this button.599 # Ensure that the custom ID is unique among all buttons in your bot.600 super().__init__(601 label=role.name, 602 style=discord.ButtonStyle.primary,603 custom_id=f"persistent_role_toggle_{role.id}"604 )605 self.role = role606 607 async def callback(self, interaction: discord.Interaction):608 if self.role in interaction.user.roles:609 try:610 await interaction.user.remove_roles(self.role)611 await interaction.response.send_message(612 f"Removed role: {self.role.name}", ephemeral=True613 )614 except Exception as e:615 await interaction.response.send_message(616 f"Error removing role: {e}", ephemeral=True617 )618 else:619 try:620 await interaction.user.add_roles(self.role)621 await interaction.response.send_message(622 f"Added role: {self.role.name}", ephemeral=True623 )624 except Exception as e:625 await interaction.response.send_message(626 f"Error adding role: {e}", ephemeral=True627 )628 629# Persistent view that holds one toggle button per role.630class PersistentRoleSelectionView(View):631 def __init__(self, roles: list):632 # Set timeout to None to keep the view indefinitely active.633 super().__init__(timeout=None)634 # Create a button for each role635 for role in roles:636 self.add_item(RoleToggleButton(role))637 638# Command that sends the role buttons message.639@bot.command(name="role_buttons")640async def role_buttons(ctx):641 # Only the configured admin user is allowed to invoke this command.642 if ctx.author.id != ADMIN_USER_ID:643 await ctx.send("You are not authorized to use this command.", delete_after=10)644 return645 646 if not SELF_ASSIGN_ROLE_IDS:647 await ctx.send("No self-assignable roles configured. Set SELF_ASSIGN_ROLE_IDS in app.py.", delete_after=15)648 return649 650 roles = [ctx.guild.get_role(rid) for rid in SELF_ASSIGN_ROLE_IDS if ctx.guild.get_role(rid) is not None]651 652 # Create the persistent view.653 view = PersistentRoleSelectionView(roles)654 # Send the message with the persistent view attached.655 await ctx.send("Click the buttons below to toggle roles:", view=view)656 657 658 659 660 661 662# github test stuff -------------------------------------------------------------------------------------------------------------------663"""664async def check_github():665 url = f'https://api.github.com/repos/{github_repo}/pulls'666 response = requests.get(url)667 pulls = response.json()668 669 for pull in pulls:670 # Check if the pull request was just opened671 if pull['state'] == 'open' and pull['created_at'] == pull['updated_at']:672 channel = client.get_channel(channel_id)673 if channel:674 await channel.send(f'New PR opened: {pull["title"]}')675"""676# bot stuff ---------------------------------------------------------------------------------------------------------------------------677 678@bot.event679async def on_ready():680 await asyncio.sleep(5)681 print('Logged on as', bot.user)682 await asyncio.sleep(5)683 bot.log_channel = bot.get_channel(LOG_CHANNEL_ID)684 await asyncio.sleep(5)685 print(bot.log_channel)686 guild = bot.get_guild(GUILD_ID)687 688 if guild and SELF_ASSIGN_ROLE_IDS:689 roles = [guild.get_role(rid) for rid in SELF_ASSIGN_ROLE_IDS if guild.get_role(rid) is not None]690 if roles:691 persistent_view = PersistentRoleSelectionView(roles)692 bot.add_view(persistent_view) # This makes the view persistent across restarts.693 694 if guild:695 for channel in guild.text_channels: # helps with more accurate logging across restarts696 try:697 message_cache.update({m.id: m async for m in channel.history(limit=10000)})698 print(f"Finished caching messages for channel: {channel.name}")699 except Exception as e:700 print(f"An error occurred while fetching messages from {channel.name}: {e}")701 await asyncio.sleep(0.1)702 else:703 print(f"on_ready: could not find guild with ID {GUILD_ID}. Is the bot in the server?")704 705 706 707 708 709 710 711def run_bot():712 bot.run(DISCORD_TOKEN)713 714threading.Thread(target=run_bot).start()715 716with gr.Blocks() as demo:717 gr.Markdown(718 r"""719 # Client for the [HuggingFace Discord](https://hf.co/join/discord) bot720 All code for this bot is under the [app.py](https://huggingface.co/spaces/discord-community/HuggingMod/blob/main/app.py) file.721 """)722demo.launch()723 