Delete modules/nick_nudge/nick_nudge.py
This commit is contained in:
		
							parent
							
								
									67af31de13
								
							
						
					
					
						commit
						520fe9c9dd
					
				@ -1,331 +0,0 @@
 | 
				
			|||||||
import asyncio
 | 
					 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
from typing import Optional, Tuple
 | 
					 | 
				
			||||||
import discord
 | 
					 | 
				
			||||||
from discord.ext import commands
 | 
					 | 
				
			||||||
from discord import app_commands
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from mod_perms import is_moderator_userid
 | 
					 | 
				
			||||||
from modules.common.emoji_accept import is_accept
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
CHECK = '✅'          # approved/verified
 | 
					 | 
				
			||||||
CROSS = '❌'          # reject / no
 | 
					 | 
				
			||||||
PENDING = '✔️'        # heavy check mark = pending claim
 | 
					 | 
				
			||||||
ACCEPT = {CHECK, '🫡'} 
 | 
					 | 
				
			||||||
NO_MENTIONS = discord.AllowedMentions.none()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _ts_rel(ts: Optional[float] = None) -> str:
 | 
					 | 
				
			||||||
    """Discord relative timestamp like <t:12345:R>."""
 | 
					 | 
				
			||||||
    if ts is None:
 | 
					 | 
				
			||||||
        ts = time.time()
 | 
					 | 
				
			||||||
    return f"<t:{int(ts)}:R>"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NickNudgeCog(commands.Cog):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Handles:
 | 
					 | 
				
			||||||
      • DM nickname nudge loop (optional; unchanged behavior)
 | 
					 | 
				
			||||||
      • Nickname *review* workflow for claims:
 | 
					 | 
				
			||||||
          - On claim (via reaction or /nick_same): create a mod review in mod_channel with ✅/❌
 | 
					 | 
				
			||||||
          - Mods react: ✅ -> mark verified; ❌ -> clear claim and revoke Full Access
 | 
					 | 
				
			||||||
      • Stores review mapping in data_manager['nick_reviews']
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, bot):
 | 
					 | 
				
			||||||
        self.bot = bot
 | 
					 | 
				
			||||||
        cfg = bot.config['DEFAULT']
 | 
					 | 
				
			||||||
        self.modlog_channel_id = int(cfg['modlog_channel_id'])
 | 
					 | 
				
			||||||
        self.mod_channel_id = int(cfg['mod_channel_id'])  # same review channel as pirate reports
 | 
					 | 
				
			||||||
        # Optional DM nudge loop retained
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            self.loop_enabled = cfg.getboolean('nick_nudge_loop_enabled')
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            self.loop_enabled = False
 | 
					 | 
				
			||||||
        self._task = asyncio.create_task(self._nudge_loop()) if self.loop_enabled else None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # ---------- utils ----------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def cog_unload(self):
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            if self._task:
 | 
					 | 
				
			||||||
                self._task.cancel()
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def _modlog(self, guild: discord.Guild, content: str):
 | 
					 | 
				
			||||||
        ch = guild.get_channel(self.modlog_channel_id)
 | 
					 | 
				
			||||||
        if ch:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                await ch.send(content, allowed_mentions=NO_MENTIONS)
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
        self.bot.data_manager.add('modlog', {'ts': time.time(), 'guild_id': guild.id, 'content': content})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def _find_last_nick_change(self, guild: discord.Guild, member: discord.Member) -> Tuple[Optional[str], Optional[str]]:
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Best-effort: look up last nickname change via audit logs.
 | 
					 | 
				
			||||||
        Returns (before_nick, after_nick) or (None, None) if not found/allowed.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            async for entry in guild.audit_logs(limit=10, action=discord.AuditLogAction.member_update):
 | 
					 | 
				
			||||||
                if entry.target.id != member.id or not entry.changes:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
                before_n = getattr(entry.changes.before, 'get', lambda *_: None)('nick')
 | 
					 | 
				
			||||||
                after_n = getattr(entry.changes.after, 'get', lambda *_: None)('nick')
 | 
					 | 
				
			||||||
                if before_n is not None or after_n is not None:
 | 
					 | 
				
			||||||
                    return before_n, after_n
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
        return None, None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # ---------- public API (called by ReactionRole cog) ----------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def start_nick_review(self, guild: discord.Guild, member: discord.Member, source: str = "claim"):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Create (or update) a nickname review entry for this member in the mod channel.
 | 
					 | 
				
			||||||
        - source: "claim" or "nick_same"
 | 
					 | 
				
			||||||
        Stores in data_manager['nick_reviews'] a record keyed by the review message_id.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if not guild:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        mod_ch = guild.get_channel(self.mod_channel_id)
 | 
					 | 
				
			||||||
        if not mod_ch:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        before_n, after_n = await self._find_last_nick_change(guild, member)
 | 
					 | 
				
			||||||
        now_ts = int(time.time())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Compose review text
 | 
					 | 
				
			||||||
        title = "📝 **Nickname Verification Request**"
 | 
					 | 
				
			||||||
        who = f"User: {member.mention} (`{member.id}`)"
 | 
					 | 
				
			||||||
        change = f"Claimed {_ts_rel(now_ts)}"
 | 
					 | 
				
			||||||
        from_to = f"From: {repr(before_n) if before_n is not None else 'unknown'} → To: {repr(member.nick) if member.nick else 'None'}"
 | 
					 | 
				
			||||||
        method = f"Method: {'/nick_same' if source == 'nick_same' else 'reaction'}"
 | 
					 | 
				
			||||||
        instructions = "Moderators: react ✅ to **approve** or ❌ to **reject**."
 | 
					 | 
				
			||||||
        content = f"{title}\n{who}\n{from_to}\n{method}\n{change}\n\n{instructions}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            msg = await mod_ch.send(content, allowed_mentions=NO_MENTIONS)
 | 
					 | 
				
			||||||
            await msg.add_reaction(CHECK)
 | 
					 | 
				
			||||||
            await msg.add_reaction(CROSS)
 | 
					 | 
				
			||||||
        except Exception:
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Persist review mapping
 | 
					 | 
				
			||||||
        self.bot.data_manager.add('nick_reviews', {
 | 
					 | 
				
			||||||
            'message_id': int(msg.id),
 | 
					 | 
				
			||||||
            'guild_id': int(guild.id),
 | 
					 | 
				
			||||||
            'user_id': int(member.id),
 | 
					 | 
				
			||||||
            'before_nick': before_n if before_n is None or isinstance(before_n, str) else str(before_n),
 | 
					 | 
				
			||||||
            'claimed_nick': member.nick if member.nick else None,
 | 
					 | 
				
			||||||
            'status': 'pending',
 | 
					 | 
				
			||||||
            'source': source,
 | 
					 | 
				
			||||||
            'ts': now_ts
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Log to modlog
 | 
					 | 
				
			||||||
        await self._modlog(guild, f"🔎 Nickname review opened for {member.mention} — {method} — {_ts_rel(now_ts)}.")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # ---------- DM nudge loop (unchanged) ----------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def _nudge_loop(self):
 | 
					 | 
				
			||||||
        await self.bot.wait_until_ready()
 | 
					 | 
				
			||||||
        while not self.bot.is_closed():
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                now = time.time()
 | 
					 | 
				
			||||||
                for guild in self.bot.guilds:
 | 
					 | 
				
			||||||
                    for member in guild.members:
 | 
					 | 
				
			||||||
                        if member.bot or not member.joined_at:
 | 
					 | 
				
			||||||
                            continue
 | 
					 | 
				
			||||||
                        if (now - member.joined_at.timestamp()) < 24*3600:
 | 
					 | 
				
			||||||
                            continue
 | 
					 | 
				
			||||||
                        # If they already have a server nick OR already claimed/verified, skip nudging
 | 
					 | 
				
			||||||
                        dm = self.bot.data_manager
 | 
					 | 
				
			||||||
                        if (member.nick and member.nick.strip()):
 | 
					 | 
				
			||||||
                            continue
 | 
					 | 
				
			||||||
                        if member.id in dm.get('nick_verified') or member.id in dm.get('nick_claim_pending'):
 | 
					 | 
				
			||||||
                            continue
 | 
					 | 
				
			||||||
                        if member.id in dm.get('nick_nudged'):
 | 
					 | 
				
			||||||
                            continue
 | 
					 | 
				
			||||||
                        try:
 | 
					 | 
				
			||||||
                            dmchan = await member.create_dm()
 | 
					 | 
				
			||||||
                            msg = await dmchan.send(
 | 
					 | 
				
			||||||
                                "Hey! On this server we require your **server nickname** to match your in-game character name.\n\n"
 | 
					 | 
				
			||||||
                                "If your default Discord display name is already identical to your in-game name, react ✅ **or 🫡** below "
 | 
					 | 
				
			||||||
                                "or run `/nick_same`. Otherwise, please set your **server nickname** to your in-game name and react ✅ **or 🫡**."
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                            await msg.add_reaction(CHECK)
 | 
					 | 
				
			||||||
                            self.bot.data_manager.add('nick_dm_map', {
 | 
					 | 
				
			||||||
                                'message_id': int(msg.id),
 | 
					 | 
				
			||||||
                                'user_id': int(member.id),
 | 
					 | 
				
			||||||
                                'guild_id': int(guild.id),
 | 
					 | 
				
			||||||
                                'ts': now
 | 
					 | 
				
			||||||
                            })
 | 
					 | 
				
			||||||
                            self.bot.data_manager.add('nick_nudged', int(member.id))
 | 
					 | 
				
			||||||
                            await self._modlog(guild, f"📨 Sent nickname nudge to {member.mention}")
 | 
					 | 
				
			||||||
                        except Exception:
 | 
					 | 
				
			||||||
                            pass
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                pass
 | 
					 | 
				
			||||||
            await asyncio.sleep(1800)  # every 30 minutes
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # ---------- listeners ----------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @commands.Cog.listener()
 | 
					 | 
				
			||||||
    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
 | 
					 | 
				
			||||||
        # 1) Handle DM nudge confirmations (user reacts with an accept in DM)
 | 
					 | 
				
			||||||
        if payload.guild_id is None and is_accept(payload.emoji) and payload.user_id != self.bot.user.id:
 | 
					 | 
				
			||||||
            entry = next((m for m in self.bot.data_manager.get('nick_dm_map') if m['message_id'] == payload.message_id), None)
 | 
					 | 
				
			||||||
            if not entry:
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            guild = self.bot.get_guild(entry['guild_id'])
 | 
					 | 
				
			||||||
            member = guild.get_member(entry['user_id']) if guild else None
 | 
					 | 
				
			||||||
            if not member:
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            # Treat as a claim: mark pending (idempotent) and open review only on first transition
 | 
					 | 
				
			||||||
            dm = self.bot.data_manager
 | 
					 | 
				
			||||||
            if member.id not in dm.get('agreed_nickname'):
 | 
					 | 
				
			||||||
                dm.add('agreed_nickname', int(member.id))
 | 
					 | 
				
			||||||
            dm.remove('nick_verified', lambda x: x == member.id)
 | 
					 | 
				
			||||||
            newly_pending = False
 | 
					 | 
				
			||||||
            if member.id not in dm.get('nick_claim_pending'):
 | 
					 | 
				
			||||||
                dm.add('nick_claim_pending', int(member.id))
 | 
					 | 
				
			||||||
                newly_pending = True
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if newly_pending:
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    await self.start_nick_review(guild, member, source="nick_same")
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Clean map entry
 | 
					 | 
				
			||||||
            self.bot.data_manager.remove('nick_dm_map', lambda m: m['message_id'] == payload.message_id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Refresh card and maybe full access (pending does NOT block full access)
 | 
					 | 
				
			||||||
            rr = self.bot.get_cog('ReactionRoleCog')
 | 
					 | 
				
			||||||
            if rr:
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    await rr.maybe_apply_full_access(member)
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    pass
 | 
					 | 
				
			||||||
            cards = self.bot.get_cog('UserCardsCog')
 | 
					 | 
				
			||||||
            if cards:
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    await cards.refresh_card(member)
 | 
					 | 
				
			||||||
                except Exception:
 | 
					 | 
				
			||||||
                    pass
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # 2) Handle moderator review reactions in mod channel
 | 
					 | 
				
			||||||
        if payload.guild_id and str(payload.emoji) in (CHECK, CROSS) and payload.user_id != self.bot.user.id:
 | 
					 | 
				
			||||||
            if payload.channel_id != self.mod_channel_id:
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            guild = self.bot.get_guild(payload.guild_id)
 | 
					 | 
				
			||||||
            if not guild:
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
            # Only moderators can act
 | 
					 | 
				
			||||||
            if not is_moderator_userid(guild, payload.user_id, self.bot):
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Is this a review message?
 | 
					 | 
				
			||||||
            reviews = self.bot.data_manager.get('nick_reviews')
 | 
					 | 
				
			||||||
            review = next((r for r in reviews if r.get('message_id') == payload.message_id and r.get('guild_id') == guild.id), None)
 | 
					 | 
				
			||||||
            if not review or review.get('status') != 'pending':
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            member = guild.get_member(int(review['user_id']))
 | 
					 | 
				
			||||||
            if not member:
 | 
					 | 
				
			||||||
                # mark closed missing
 | 
					 | 
				
			||||||
                self.bot.data_manager.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status':'closed_missing'}), r)[1])
 | 
					 | 
				
			||||||
                return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Fetch and edit the review message content (best-effort)
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                ch = self.bot.get_channel(payload.channel_id)
 | 
					 | 
				
			||||||
                msg = await ch.fetch_message(payload.message_id)
 | 
					 | 
				
			||||||
            except Exception:
 | 
					 | 
				
			||||||
                msg = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            dm = self.bot.data_manager
 | 
					 | 
				
			||||||
            now_ts = int(time.time())
 | 
					 | 
				
			||||||
            approver = f"<@{payload.user_id}>"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if str(payload.emoji) == CHECK:
 | 
					 | 
				
			||||||
                # Approve: mark verified, clear pending, ensure agreed flag set
 | 
					 | 
				
			||||||
                if member.id not in dm.get('agreed_nickname'):
 | 
					 | 
				
			||||||
                    dm.add('agreed_nickname', int(member.id))
 | 
					 | 
				
			||||||
                dm.remove('nick_claim_pending', lambda x: x == member.id)
 | 
					 | 
				
			||||||
                if member.id not in dm.get('nick_verified'):
 | 
					 | 
				
			||||||
                    dm.add('nick_verified', int(member.id))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Update review record
 | 
					 | 
				
			||||||
                dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'approved', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Edit the review message
 | 
					 | 
				
			||||||
                if msg:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await msg.clear_reactions()
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await msg.edit(content=f"✅ **Nickname Approved** for {member.mention} by {approver} — {_ts_rel(now_ts)}")
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Modlog
 | 
					 | 
				
			||||||
                await self._modlog(guild,
 | 
					 | 
				
			||||||
                    f"✅ Nickname **verified** for {member.mention} by {approver} — {_ts_rel(now_ts)}."
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Refresh roles / card
 | 
					 | 
				
			||||||
                rr = self.bot.get_cog('ReactionRoleCog')
 | 
					 | 
				
			||||||
                if rr:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await rr.maybe_apply_full_access(member)
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
                cards = self.bot.get_cog('UserCardsCog')
 | 
					 | 
				
			||||||
                if cards:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await cards.refresh_card(member)
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                # Reject: clear all nickname flags; Full Access should be revoked by maybe_apply_full_access
 | 
					 | 
				
			||||||
                dm.remove('agreed_nickname', lambda x: x == member.id)
 | 
					 | 
				
			||||||
                dm.remove('nick_claim_pending', lambda x: x == member.id)
 | 
					 | 
				
			||||||
                dm.remove('nick_verified', lambda x: x == member.id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Update review record
 | 
					 | 
				
			||||||
                dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'rejected', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if msg:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await msg.clear_reactions()
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await msg.edit(content=f"❌ **Nickname Rejected** for {member.mention} by {approver} — {_ts_rel(now_ts)}")
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                await self._modlog(guild,
 | 
					 | 
				
			||||||
                    f"❌ Nickname **rejected** for {member.mention} by {approver} — {_ts_rel(now_ts)}."
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # Refresh roles / card
 | 
					 | 
				
			||||||
                rr = self.bot.get_cog('ReactionRoleCog')
 | 
					 | 
				
			||||||
                if rr:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await rr.maybe_apply_full_access(member)
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
                cards = self.bot.get_cog('UserCardsCog')
 | 
					 | 
				
			||||||
                if cards:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await cards.refresh_card(member)
 | 
					 | 
				
			||||||
                    except Exception:
 | 
					 | 
				
			||||||
                        pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def setup(bot):
 | 
					 | 
				
			||||||
    await bot.add_cog(NickNudgeCog(bot))
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user