jarvis-bot/jarvis/cogs/util.py

400 lines
14 KiB
Python

"""JARVIS Utility Cog."""
import logging
import re
import secrets
import string
from datetime import timezone
from io import BytesIO
import numpy as np
from dateparser import parse
from PIL import Image
from tzlocal import get_localzone
from jarvis import const as jconst
from jarvis.data import pigpen
from jarvis.data.robotcamo import emotes, hk, names
from jarvis.utils import build_embed, get_repo_hash
from naff import Client, Extension, InteractionContext, const
from naff.models.discord.channel import GuildCategory, GuildText, GuildVoice
from naff.models.discord.embed import EmbedField
from naff.models.discord.file import File
from naff.models.discord.guild import Guild
from naff.models.discord.role import Role
from naff.models.discord.user import User
from naff.models.naff.application_commands import (
CommandTypes,
OptionTypes,
SlashCommandChoice,
context_menu,
slash_command,
slash_option,
)
from naff.models.naff.command import cooldown
from naff.models.naff.cooldowns import Buckets
JARVIS_LOGO = Image.open("jarvis_small.png").convert("RGBA")
class UtilCog(Extension):
"""
Utility functions for JARVIS
Mostly system utility functions, but may change over time
"""
def __init__(self, bot: Client):
self.bot = bot
self.logger = logging.getLogger(__name__)
@slash_command(name="status", description="Retrieve JARVIS status")
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _status(self, ctx: InteractionContext) -> None:
title = "JARVIS Status"
desc = f"All systems online\nConnected to **{len(self.bot.guilds)}** guilds"
color = "#3498db"
fields = []
uptime = int(self.bot.start_time.timestamp())
fields.append(EmbedField(name="Version", value=jconst.__version__, inline=True))
fields.append(EmbedField(name="naff", value=const.__version__, inline=True))
fields.append(EmbedField(name="Git Hash", value=get_repo_hash()[:7], inline=True))
fields.append(EmbedField(name="Online Since", value=f"<t:{uptime}:F>", inline=False))
num_domains = len(self.bot.phishing_domains)
fields.append(
EmbedField(
name="Phishing Protection", value=f"Detecting {num_domains} phishing domains"
)
)
embed = build_embed(title=title, description=desc, fields=fields, color=color)
await ctx.send(embeds=embed)
@slash_command(
name="logo",
description="Get the current logo",
)
@cooldown(bucket=Buckets.CHANNEL, rate=1, interval=30)
async def _logo(self, ctx: InteractionContext) -> None:
with BytesIO() as image_bytes:
JARVIS_LOGO.save(image_bytes, "PNG")
image_bytes.seek(0)
logo = File(image_bytes, file_name="logo.png")
await ctx.send(file=logo)
@slash_command(name="rchk", description="Robot Camo HK416")
async def _rchk(self, ctx: InteractionContext) -> None:
await ctx.send(content=hk, ephemeral=True)
@slash_command(
name="rcauto",
description="Automates robot camo letters",
)
@slash_option(
name="text",
description="Text to camo-ify",
opt_type=OptionTypes.STRING,
required=True,
)
async def _rcauto(self, ctx: InteractionContext, text: str) -> None:
to_send = ""
if len(text) == 1 and not re.match(r"^[A-Z0-9-()$@!?^'#. ]$", text.upper()):
await ctx.send("Please use ASCII characters.", ephemeral=True)
return
for letter in text.upper():
if letter == " ":
to_send += " "
elif re.match(r"^[A-Z0-9-()$@!?^'#.]$", letter):
id = emotes[letter]
to_send += f":{names[id]}:"
if len(to_send) > 2000:
await ctx.send("Too long.", ephemeral=True)
elif len(to_send) == 0:
await ctx.send("No valid text found", ephemeral=True)
else:
await ctx.send(to_send, ephemeral=True)
@slash_command(name="avatar", description="Get a user avatar")
@slash_option(
name="user",
description="User to view avatar of",
opt_type=OptionTypes.USER,
required=False,
)
@cooldown(bucket=Buckets.USER, rate=1, interval=5)
async def _avatar(self, ctx: InteractionContext, user: User = None) -> None:
if not user:
user = ctx.author
avatar = user.display_avatar.url
embed = build_embed(title="Avatar", description="", fields=[], color="#00FFEE")
embed.set_image(url=avatar)
embed.set_author(name=f"{user.username}#{user.discriminator}", icon_url=avatar)
await ctx.send(embeds=embed)
@slash_command(
name="roleinfo",
description="Get role info",
)
@slash_option(
name="role",
description="Role to get info of",
opt_type=OptionTypes.ROLE,
required=True,
)
async def _roleinfo(self, ctx: InteractionContext, role: Role) -> None:
fields = [
EmbedField(name="ID", value=str(role.id), inline=True),
EmbedField(name="Name", value=role.mention, inline=True),
EmbedField(name="Color", value=str(role.color.hex), inline=True),
EmbedField(name="Mention", value=f"`{role.mention}`", inline=True),
EmbedField(name="Hoisted", value="Yes" if role.hoist else "No", inline=True),
EmbedField(name="Position", value=str(role.position), inline=True),
EmbedField(name="Mentionable", value="Yes" if role.mentionable else "No", inline=True),
EmbedField(name="Member Count", value=str(len(role.members)), inline=True),
EmbedField(name="Created At", value=f"<t:{int(role.created_at.timestamp())}:F>"),
]
embed = build_embed(
title="",
description="",
fields=fields,
color=role.color,
timestamp=role.created_at,
)
embed.set_footer(text="Role Created")
embed.set_thumbnail(url="attachment://color_show.png")
data = np.array(JARVIS_LOGO)
r, g, b, a = data.T
fill = a > 0
data[..., :-1][fill.T] = list(role.color.rgb)
im = Image.fromarray(data)
with BytesIO() as image_bytes:
im.save(image_bytes, "PNG")
image_bytes.seek(0)
color_show = File(image_bytes, file_name="color_show.png")
await ctx.send(embeds=embed, file=color_show)
async def _userinfo(self, ctx: InteractionContext, user: User = None) -> None:
await ctx.defer()
if not user:
user = ctx.author
if not await ctx.guild.fetch_member(user.id):
await ctx.send("That user isn't in this guild.", ephemeral=True)
return
user_roles = user.roles
if user_roles:
user_roles = sorted(user.roles, key=lambda x: -x.position)
fields = [
EmbedField(
name="Joined",
value=f"<t:{int(user.joined_at.timestamp())}:F>",
),
EmbedField(
name="Registered",
value=f"<t:{int(user.created_at.timestamp())}:F>",
),
EmbedField(
name=f"Roles [{len(user_roles)}]",
value=" ".join([x.mention for x in user_roles]) if user_roles else "None",
inline=False,
),
]
embed = build_embed(
title="",
description=user.mention,
fields=fields,
color=str(user_roles[0].color) if user_roles else "#3498db",
)
embed.set_author(
name=f"{user.display_name}#{user.discriminator}", icon_url=user.display_avatar.url
)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"ID: {user.id}")
await ctx.send(embeds=embed)
@slash_command(
name="userinfo",
description="Get user info",
)
@slash_option(
name="user",
description="User to get info of",
opt_type=OptionTypes.USER,
required=False,
)
async def _userinfo_slsh(self, ctx: InteractionContext, user: User = None) -> None:
await self._userinfo(ctx, user)
@context_menu(name="User Info", context_type=CommandTypes.USER)
async def _userinfo_menu(self, ctx: InteractionContext) -> None:
await self._userinfo(ctx, ctx.target)
@slash_command(name="serverinfo", description="Get server info")
async def _server_info(self, ctx: InteractionContext) -> None:
guild: Guild = ctx.guild
owner = await guild.fetch_owner()
owner = f"{owner.username}#{owner.discriminator}" if owner else "||`[redacted]`||"
categories = len([x for x in guild.channels if isinstance(x, GuildCategory)])
text_channels = len([x for x in guild.channels if isinstance(x, GuildText)])
voice_channels = len([x for x in guild.channels if isinstance(x, GuildVoice)])
threads = len(guild.threads)
members = guild.member_count
roles = len(guild.roles)
role_list = sorted(guild.roles, key=lambda x: x.position, reverse=True)
role_list = ", ".join(role.mention for role in role_list)
fields = [
EmbedField(name="Owner", value=owner, inline=True),
EmbedField(name="Channel Categories", value=str(categories), inline=True),
EmbedField(name="Text Channels", value=str(text_channels), inline=True),
EmbedField(name="Voice Channels", value=str(voice_channels), inline=True),
EmbedField(name="Threads", value=str(threads), inline=True),
EmbedField(name="Members", value=str(members), inline=True),
EmbedField(name="Roles", value=str(roles), inline=True),
EmbedField(name="Created At", value=f"<t:{int(guild.created_at.timestamp())}:F>"),
]
if len(role_list) < 1024:
fields.append(EmbedField(name="Role List", value=role_list, inline=False))
embed = build_embed(title="", description="", fields=fields, timestamp=guild.created_at)
embed.set_author(name=guild.name, icon_url=guild.icon.url)
embed.set_thumbnail(url=guild.icon.url)
embed.set_footer(text=f"ID: {guild.id} | Server Created")
await ctx.send(embeds=embed)
@slash_command(
name="pw",
sub_cmd_name="gen",
description="Generate a secure password",
scopes=[862402786116763668],
)
@slash_option(
name="length",
description="Password length (default 32)",
opt_type=OptionTypes.INTEGER,
required=False,
)
@slash_option(
name="chars",
description="Characters to include (default last option)",
opt_type=OptionTypes.INTEGER,
required=False,
choices=[
SlashCommandChoice(name="A-Za-z", value=0),
SlashCommandChoice(name="A-Fa-f0-9", value=1),
SlashCommandChoice(name="A-Za-z0-9", value=2),
SlashCommandChoice(name="A-Za-z0-9!@#$%^&*", value=3),
],
)
@cooldown(bucket=Buckets.USER, rate=1, interval=15)
async def _pw_gen(self, ctx: InteractionContext, length: int = 32, chars: int = 3) -> None:
if length > 256:
await ctx.send("Please limit password to 256 characters", ephemeral=True)
return
choices = [
string.ascii_letters,
string.hexdigits,
string.ascii_letters + string.digits,
string.ascii_letters + string.digits + "!@#$%^&*",
]
pw = "".join(secrets.choice(choices[chars]) for i in range(length))
await ctx.send(
f"Generated password:\n`{pw}`\n\n"
'**WARNING: Once you press "Dismiss Message", '
"*the password is lost forever***",
ephemeral=True,
)
@slash_command(name="pigpen", description="Encode a string into pigpen")
@slash_option(
name="text", description="Text to encode", opt_type=OptionTypes.STRING, required=True
)
async def _pigpen(self, ctx: InteractionContext, text: str) -> None:
outp = "`"
for c in text:
c = c.lower()
if c.lower() in pigpen.lookup:
c = pigpen.lookup[c.lower()]
elif c == " ":
c = " "
elif c == "`":
continue
outp += c + " "
outp += "`"
await ctx.send(outp[:2000])
@slash_command(
name="timestamp", description="Convert a datetime or timestamp into it's counterpart"
)
@slash_option(
name="string", description="String to convert", opt_type=OptionTypes.STRING, required=True
)
@slash_option(
name="private", description="Respond quietly?", opt_type=OptionTypes.BOOLEAN, required=False
)
async def _timestamp(self, ctx: InteractionContext, string: str, private: bool = False) -> None:
timestamp = parse(string)
if not timestamp:
await ctx.send("Valid time not found, try again", ephemeral=True)
return
if not timestamp.tzinfo:
timestamp = timestamp.replace(tzinfo=get_localzone()).astimezone(tz=timezone.utc)
timestamp_utc = timestamp.astimezone(tz=timezone.utc)
ts = int(timestamp.timestamp())
ts_utc = int(timestamp_utc.timestamp())
fields = [
EmbedField(name="Unix Epoch", value=f"`{ts}`"),
EmbedField(name="Unix Epoch (UTC)", value=f"`{ts_utc}`"),
EmbedField(name="Absolute Time", value=f"<t:{ts_utc}:F>\n`<t:{ts_utc}:F>`"),
EmbedField(name="Relative Time", value=f"<t:{ts_utc}:R>\n`<t:{ts_utc}:R>`"),
EmbedField(name="ISO8601", value=timestamp.isoformat()),
]
embed = build_embed(title="Converted Time", description=f"`{string}`", fields=fields)
await ctx.send(embeds=embed, ephemeral=private)
@slash_command(name="support", description="Got issues?")
async def _support(self, ctx: InteractionContext) -> None:
await ctx.send(
f"""
Run into issues with {self.bot.user.mention}? Please report them here!
https://s.zevs.me/jarvis-support
We'll help as best we can with whatever issues you encounter.
"""
)
@slash_command(name="privacy_terms", description="View Privacy and Terms of Use")
async def _privacy_terms(self, ctx: InteractionContext) -> None:
await ctx.send(
"""
View the privacy statement here: https://s.zevs.me/jarvis-privacy
View the terms of use here: https://s.zevs.me/jarvis-terms
"""
)
def setup(bot: Client) -> None:
"""Add UtilCog to JARVIS"""
UtilCog(bot)