CoolFace
Apppublic

hugging-science/HuggingMod

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py723 linesDownload Raw Back to root
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