Files

266 lines
8.8 KiB
Python

import datetime
import discord
import io
import random
import re
import traceback
from .checks import check_staff
from discord.ext import commands
from discord.utils import format_dt
from inspect import cleandoc
from typing import Optional
class ConsoleColor(discord.Color):
@classmethod
def n3ds(cls):
return cls(0xCE181E)
@classmethod
def wiiu(cls):
return cls(0x009AC7)
@classmethod
def switch(cls):
return cls(0xE60012)
@classmethod
def wii(cls):
return cls(0x009AC7)
@classmethod
def legacy(cls):
return cls(0x707070)
PUBLIC_CHAN_IDS = [
196635695958196224, # 3ds 1
247557068490276864, # 3ds 2
279783073497874442, # wiiu
439933093118476289, # switch 1
655681905907466250, # switch 2
1159223824676565114, # wii-vwii
196635745551646720, # modding gen
770582653132734464, # tt
314582689829355521, # legacy
196635781798952960, # dev
233002779717795850, # hardware
270890866820775946, # appeals
872309776233684992, # meta
918315061003579482, # kurisu-dev
1159936589997281390, # guide meta
1022667983081967636, # wiki discussion
1364102432790941776, # hw wiki discussion
314856589716750346, # off topic
300388576451887104, # nintendo discussion
485138525885431808, # elsewhere (just in case)
1130632654857973861, # animal zone
]
async def send_dm_message(member: discord.Member, message: str, ctx: Optional[commands.Context] = None, **kwargs) -> bool:
"""A helper method for sending a message to a member's DMs.
Returns a boolean indicating success of the DM
and notifies of the failure if ctx is supplied."""
try:
await member.send(message, **kwargs)
return True
except (discord.HTTPException, discord.Forbidden, discord.NotFound, AttributeError):
if ctx:
await ctx.send(f"Failed to send DM message to {member.mention}")
return False
async def get_user(ctx: commands.Context | discord.Interaction, user_id: int) -> Optional[discord.Member | discord.User]:
if ctx.guild and (user := ctx.guild.get_member(user_id)):
return user
else:
bot = ctx.bot if isinstance(ctx, commands.Context) else ctx.client
return await bot.fetch_user(user_id)
def command_signature(command, *, prefix=".") -> str:
"""Helper method for a command signature
Parameters
-----------
command: :class:`discord.ext.commands.Command`
The command to generate a signature for
prefix: str
The prefix to include in the signature"""
return f"{discord.utils.escape_markdown(prefix)}{command.qualified_name} {command.signature}"
def gen_color(seed) -> discord.Color:
random.seed(seed)
c_r = random.randint(0, 255)
c_g = random.randint(0, 255)
c_b = random.randint(0, 255)
return discord.Color((c_r << 16) + (c_g << 8) + c_b)
def parse_time(time_string: str) -> int:
"""Parses a time string in dhms format to seconds"""
# thanks, Luc#5653
units = {
"d": 86400,
"h": 3600,
"m": 60,
"s": 1
}
match = re.findall("([0-9]+[smhd])", time_string) # Thanks to 3dshax server's former bot
if not match:
return -1
return sum(int(item[:-1]) * units[item[-1]] for item in match)
def parse_date(date_string: str) -> Optional[datetime.datetime]:
date_lst = date_string.split(' ')
if len(date_lst) == 1:
date_lst.append('00:00')
elif len(date_lst) != 2:
return None
try:
datetime_obj = datetime.datetime.strptime(' '.join(date_lst), "%Y-%m-%d %H:%M")
except ValueError:
return None
return datetime_obj
def create_error_embed(ctx: commands.Context | discord.Interaction, exc: Exception) -> discord.Embed:
app_command: bool = isinstance(ctx, discord.Interaction)
author = ctx.user if app_command else ctx.author
command: str = ctx.command.name if ctx.command else "unknown command"
embed = discord.Embed(title=f"Unexpected exception in command {command}", color=0xe50730)
trace = "".join(traceback.format_exception(exc))
if len(trace) > 4080:
trace = trace[-4080:]
embed.description = f'```py\n{trace}```'
embed.add_field(name="Exception Type", value=exc.__class__.__name__)
embed.add_field(name="Information", value=f"channel: {ctx.channel.mention if isinstance(ctx.channel, discord.TextChannel) else 'Direct Message'}\ncommand: {command}\nauthor: {author.mention}\n{f'message: {ctx.message.content}' if not app_command else ''}", inline=False)
return embed
def paginate_message(msg: str, prefix: str = '```', suffix: str = '```', max_size: int = 2000):
paginator = commands.Paginator(prefix=prefix, suffix=suffix, max_size=max_size)
sep = max_size - len(prefix) - len(suffix) - 2
for chunk in [msg[i:i + sep] for i in range(0, len(msg), sep)]:
paginator.add_line(chunk)
return paginator
def text_to_discord_file(text: str, *, name: str = 'output.txt'):
encoded = text.encode("utf-8")
return discord.File(filename=name, fp=io.BytesIO(encoded))
# https://stackoverflow.com/questions/9647202/ordinal-numbers-replacement
# but modified to be an f-string
def ordinal(n: int) -> str:
return f'{n}{"tsnrhtdd"[(n // 10 % 10 != 1) * (n % 10 < 4) * n % 10::4]}'
class KurisuCooldown:
def __init__(self, rate: float, per: float):
self.rate = rate
self.per = per
def __call__(self, ctx: commands.Context) -> Optional[commands.Cooldown]:
if check_staff(ctx.bot, 'Helper', ctx.author.id):
return None
else:
return commands.Cooldown(self.rate, self.per)
async def create_userinfo_embed(user: discord.Member | discord.User, guild: discord.Guild) -> discord.Embed:
embed = discord.Embed(color=gen_color(user.id))
embed.description = (
f"**User:** {user.mention}\n"
f"**User's ID:** {user.id}\n"
f"**Created on:** {format_dt(user.created_at)} ({format_dt(user.created_at, style='R')})\n"
f"**Default Profile Picture:** {user.default_avatar}\n"
)
if isinstance(user, discord.Member):
member_type = "member"
embed.description += (
f"**Join date:** {format_dt(user.joined_at) if user.joined_at else None} ({format_dt(user.joined_at, style='R') if user.joined_at else None})\n"
f"**Current Status:** {user.status}\n"
f"**User Activity:** {user.activity}\n"
f"**Current Display Name:** {user.display_name}\n"
f"**Nitro Boost Info:** {f'Boosting since {format_dt(user.premium_since)}' if user.premium_since else 'Not a booster'}\n"
f"**Current Top Role:** {user.top_role}\n"
f"**Color:** {user.color}\n"
f"**Profile Picture:** [link]({user.avatar})"
)
if user.guild_avatar:
embed.description += f"\n**Guild Profile Picture:** [link]({user.guild_avatar})"
else:
member_type = "user"
try:
ban = await guild.fetch_ban(user)
embed.description += f"\n**Banned**, reason: {ban.reason}"
except discord.NotFound:
pass
member_type = member_type if not user.bot else "bot"
embed.title = f"**Userinfo for {member_type} {user}**"
embed.set_thumbnail(url=user.display_avatar.url)
return embed
async def simple_embed(ctx: commands.Context, text: str, *, title: str = "", color=discord.Color.default()):
mention_author = any(ctx.message.mentions)
embed = discord.Embed(title=title, color=color)
embed.description = cleandoc(text)
await ctx.send(embed=embed, reference=ctx.message.reference, mention_author=mention_author)
async def scam_purge(
guild: discord.Guild,
target_id: int,
*,
reason: str,
limit: int = 50,
) -> tuple[int, list[str]]:
"""
Purge messages by a user from PUBLIC_CHAN_IDS in the last 24 hours.
"""
after = discord.utils.utcnow() - datetime.timedelta(days=1)
deleted_count = 0
failures: list[str] = []
for channel_id in PUBLIC_CHAN_IDS:
channel = guild.get_channel(channel_id)
if channel is None or not isinstance(channel, discord.TextChannel):
failures.append(f"{channel_id}: not found or not text")
continue
try:
deleted = await channel.purge(
limit=limit,
after=after,
oldest_first=False,
check=lambda m, tid=target_id: m.author.id == tid,
reason=reason,
bulk=True,
)
deleted_count += len(deleted)
except (discord.Forbidden, discord.HTTPException) as e:
status = getattr(e, "status", None)
code = getattr(e, "code", None)
failures.append(
f"#{channel.name}: {type(e).__name__} (status={status}, code={code})"
)
return deleted_count, failures