Merge branch 'master' of github.com:EEKIM10/LCC-Bot

This commit is contained in:
Nexus 2023-05-25 22:49:42 +01:00
commit 7394f90bfc
9 changed files with 388 additions and 499 deletions

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="PY-231.8770.66">
<component name="dataSourceStorageLocal" created-in="PY-231.9011.38">
<data-source name="main" uuid="28efee07-d306-4126-bf69-01008b4887e2">
<database-info product="SQLite" version="3.39.2" jdbc-version="2.1" driver-name="SQLite JDBC" driver-version="3.39.2.0" dbms="SQLITE" exact-version="3.39.2" exact-driver-version="3.39">
<identifier-quote-string>&quot;</identifier-quote-string>

View file

@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.11 (LCC-bot)" jdkType="Python SDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
assets/visio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -19,6 +19,7 @@ import httpx
from discord.ext import commands, pages, tasks
from utils import Student, get_or_none, console
from config import guilds
from utils.db import AccessTokens
try:
from config import dev
except ImportError:
@ -325,21 +326,13 @@ class Events(commands.Cog):
)
await message.reply(_content, allowed_mentions=discord.AllowedMentions.none())
async def send_fuck_you():
student = await get_or_none(Student, user_id=message.author.id)
if student is None:
return await message.reply("You aren't even verified...", delete_after=10)
elif student.ip_info is None:
async def send_fuck_you() -> str:
student = await get_or_none(AccessTokens, user_id=message.author.id)
if student.ip_info is None or student.expires >= discord.utils.utcnow().timestamp():
if OAUTH_REDIRECT_URI:
return await message.reply(
f"Let me see who you are, and then we'll talk... <{OAUTH_REDIRECT_URI}>",
delete_after=30
)
return f"Let me see who you are, and then we'll talk... <{OAUTH_REDIRECT_URI}>"
else:
return await message.reply(
"I literally don't even know who you are...",
delete_after=10
)
return "I literally don't even know who you are..."
else:
ip = student.ip_info
is_proxy = ip.get("proxy")
@ -354,7 +347,7 @@ class Events(commands.Cog):
else:
is_hosting = "\N{WHITE HEAVY CHECK MARK}" if is_hosting else "\N{CROSS MARK}"
return await message.reply(
return (
"Nice argument, however,\n"
"IP: {0[query]}\n"
"ISP: {0[isp]}\n"
@ -366,8 +359,7 @@ class Events(commands.Cog):
ip,
is_proxy,
is_hosting
),
delete_after=30
)
)
if not message.guild:
@ -396,9 +388,9 @@ class Events(commands.Cog):
"content": "https://ferdi-is.gay/bee",
},
r"it just works": {
"func": play_voice(assets / "it-just-works.mp3"),
"func": play_voice(assets / "it-just-works.ogg"),
"meta": {
"check": (assets / "it-just-works.mp3").exists
"check": (assets / "it-just-works.ogg").exists
}
},
r"^linux$": {
@ -442,7 +434,7 @@ class Events(commands.Cog):
"delete_after": None
},
r"fuck you(\W)*": {
"func": send_fuck_you,
"content": send_fuck_you,
"meta": {
"check": lambda: message.content.startswith(self.bot.user.mention)
}
@ -453,7 +445,7 @@ class Events(commands.Cog):
"check": (assets / "mine-diamonds.opus").exists
}
},
r"v[ei]r[mg]in(\sme(d|m[a]?)ia\W*)?$": {
r"v[ei]r[mg]in(\sme(d|m[a]?)ia\W*)?(\W\w*\W*)?$": {
"content": "Get virgin'd",
"file": lambda: discord.File(
random.choice(list(Path(assets / 'virgin').iterdir()))
@ -461,7 +453,22 @@ class Events(commands.Cog):
"meta": {
"check": (assets / 'virgin').exists
}
}
},
r"richard|(dick\W*$)": {
"file": discord.File(assets / "visio.png"),
"meta": {
"check": (assets / "visio.png").exists
}
},
r"thank(\syou|s)(,)? jimmy": {
"content": "You're welcome, %s!" % message.author.mention,
},
r"(ok )?jimmy (we|i) g[eo]t it": {
"content": "No need to be so rude! Cunt.",
},
r"c(mon|ome on) jimmy": {
"content": "IM TRYING"
},
}
# Stop responding to any bots
if message.author.bot is True:
@ -516,7 +523,7 @@ class Events(commands.Cog):
if "func" in data:
try:
if inspect.iscoroutinefunction(data["func"]):
if inspect.iscoroutinefunction(data["func"]) or inspect.iscoroutine(data["func"]):
await data["func"]()
break
else:
@ -527,9 +534,12 @@ class Events(commands.Cog):
continue
else:
for k, v in data.copy().items():
if callable(v):
if inspect.iscoroutinefunction(data[k]) or inspect.iscoroutine(data[k]):
data[k] = await v()
elif callable(v):
data[k] = v()
data.setdefault("delete_after", 30)
await message.channel.trigger_typing()
await message.reply(**data)
break

View file

@ -1,195 +1,112 @@
import asyncio
import datetime
import os
import sys
import time
from typing import List
import discord
import humanize
import psutil
from functools import partial
import httpx
from discord.ext import commands
from pathlib import Path
from utils import get_or_none
from utils.db import AccessTokens
try:
from config import OAUTH_ID, OAUTH_REDIRECT_URI
except ImportError:
OAUTH_REDIRECT_URI = OAUTH_ID = None
class InfoCog(commands.Cog):
EMOJIS = {
"CPU": "\N{brain}",
"RAM": "\N{ram}",
"SWAP": "\N{swan}",
"DISK": "\N{minidisc}",
"NETWORK": "\N{satellite antenna}",
"SENSORS": "\N{thermometer}",
"UPTIME": "\N{alarm clock}",
"ON": "\N{large green circle}",
"OFF": "\N{large red circle}",
}
def __init__(self, bot):
self.bot = bot
self.client = httpx.AsyncClient(base_url="https://discord.com/api")
async def get_user_info(self, token: str):
try:
response = await self.client.get("/users/@me", headers={"Authorization": f"Bearer {token}"})
response.raise_for_status()
except (httpx.HTTPError, httpx.RequestError, ConnectionError):
return
return response.json()
async def get_user_guilds(self, token: str):
try:
response = await self.client.get("/users/@me/guilds", headers={"Authorization": f"Bearer {token}"})
response.raise_for_status()
except (httpx.HTTPError, httpx.RequestError, ConnectionError):
return
return response.json()
async def run_subcommand(self, *args: str):
"""Runs a command in a shell in the background, asynchronously, returning status, stdout, and stderr."""
proc = await asyncio.create_subprocess_exec(
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
loop=self.bot.loop,
)
stdout, stderr = await proc.communicate()
return proc.returncode, stdout.decode("utf-8", "replace"), stderr.decode("utf-8", "replace")
async def unblock(self, function: callable, *args, **kwargs):
"""Runs a function in the background, asynchronously, returning the result."""
return await self.bot.loop.run_in_executor(None, partial(function, *args, **kwargs))
@staticmethod
def bar_fill(
filled: int,
total: int,
bar_width: int = 10,
char: str = "\N{black large square}",
unfilled_char: str = "\N{white large square}"
):
"""Returns a progress bar with the given length and fill character."""
if filled == 0:
filled = 0.01
if total == 0:
total = 0.01
percent = filled / total
to_fill = int(bar_width * percent)
return f"{char * to_fill}{unfilled_char * (bar_width - to_fill)}"
@commands.slash_command(name="system-info")
@commands.max_concurrency(1, commands.BucketType.user)
async def system_info(self, ctx: discord.ApplicationContext):
"""Gather statistics on the current host."""
bar_emojis = {
75: "\N{large yellow square}",
80: "\N{large orange square}",
90: "\N{large red square}",
}
async def get_user_connections(self, token: str):
try:
response = await self.client.get("/users/@me/connections", headers={"Authorization": f"Bearer {token}"})
response.raise_for_status()
except (httpx.HTTPError, httpx.RequestError, ConnectionError):
return
return response.json()
@commands.slash_command()
@commands.cooldown(1, 10, commands.BucketType.user)
async def me(self, ctx: discord.ApplicationCommand):
"""Displays oauth info about you"""
await ctx.defer()
root_drive = Path(__file__).root
temperature = fans = {}
binary = os.name != "nt"
# Gather statistics
start = time.time()
cpu: List[float] = await self.unblock(psutil.cpu_percent, interval=1.0, percpu=True)
ram = await self.unblock(psutil.virtual_memory)
swap = await self.unblock(psutil.swap_memory)
disk = await self.unblock(psutil.disk_usage, root_drive)
network = await self.unblock(psutil.net_io_counters)
if getattr(psutil, "sensors_temperatures", None):
temperature = await self.unblock(psutil.sensors_temperatures)
if getattr(psutil, "sensors_fans", None):
fans = await self.unblock(psutil.sensors_fans)
uptime = datetime.datetime.fromtimestamp(await self.unblock(psutil.boot_time), datetime.timezone.utc)
end = time.time()
user = await get_or_none(AccessTokens, user_id=ctx.author.id)
if not user:
url = OAUTH_REDIRECT_URI
return await ctx.respond(
embed=discord.Embed(
title="You must link your account first!",
description="Don't worry, [I only store your IP information. And the access token.](%s)" % url,
url=url
)
)
user_data = await self.get_user_info(user.access_token)
guilds = await self.get_user_guilds(user.access_token)
connections = await self.get_user_connections(user.access_token)
embed = discord.Embed(
title="System Statistics",
description=f"Collected in {humanize.precisedelta(datetime.timedelta(seconds=end - start))}.",
color=discord.Color.blurple(),
title="Your info",
)
# Format statistics
per_core = "\n".join(f"{i}: {c:.2f}%" for i, c in enumerate(cpu))
total_cpu = sum(cpu)
pct = total_cpu / len(cpu)
cpu_bar_emoji = "\N{large green square}"
for threshold, emoji in bar_emojis.items():
if pct >= threshold:
cpu_bar_emoji = emoji
bar = self.bar_fill(sum(cpu), len(cpu) * 100, 16, cpu_bar_emoji, "\u2581")
embed.add_field(
name=f"{self.EMOJIS['CPU']} CPU",
value=f"**Usage:** {sum(cpu):.2f}%\n"
f"**Cores:** {len(cpu)}\n"
f"**Usage Per Core:**\n{per_core}\n"
f"{bar}",
inline=False,
)
if "coretemp" in temperature:
if user_data:
for field in ("bot", "system", "mfa_enabled", "banner", "accent_color", "mfa_enabled", "locale",
"verified", "email", "flags", "premium_type", "public_flags"):
user_data.setdefault(field, None)
lines = [
"ID: {0[id]}",
"Username: {0[username]}",
"Discriminator: #{0[discriminator]}",
"Avatar: {0[avatar]}",
"Bot: {0[bot]}",
"System: {0[system]}",
"MFA Enabled: {0[mfa_enabled]}",
"Banner: {0[banner]}",
"Banner Color: {0[banner_color]}",
"Locale: {0[locale]}",
"Email Verified: {0[verified]}",
"Email: {1}",
"Flags: {0[flags]}",
"Premium Type: {0[premium_type]}",
"Public Flags: {0[public_flags]}",
]
email = user_data["email"]
if email:
email = email.replace("@", "\u200b@\u200b").replace(".", "\u200b.\u200b")
embed.add_field(
name=f"{self.EMOJIS['SENSORS']} Temperature (coretemp)",
value="\n".join(f"{s.label}: {s.current:.2f}°C" for s in temperature["coretemp"]),
inline=True,
name="User Info",
value="\n".join(lines).format(user_data, email),
)
elif "acpitz" in temperature:
if guilds:
guilds = sorted(guilds, key=lambda x: x["name"])
embed.add_field(
name=f"{self.EMOJIS['SENSORS']} Temperature (acpitz)",
value="\n".join(f"{s.label}: {s.current:.2f}°C" for s in temperature["acpitz"]),
inline=True,
name="Guilds (%d):" % len(guilds),
value="\n".join(f"{guild['name']} ({guild['id']})" for guild in guilds),
)
elif "cpu_thermal" in temperature:
if connections:
embed.add_field(
name=f"{self.EMOJIS['SENSORS']} Temperature (cpu_thermal)",
value="\n".join(f"{s.label}: {s.current:.2f}°C" for s in temperature["cpu_thermal"]),
inline=True,
name="Connections (%d):" % len(connections),
value="\n".join(f"{connection['type'].title()} ({connection['id']})" for connection in connections),
)
if fans:
embed.add_field(
name=f"{self.EMOJIS['SENSORS']} Fans",
value="\n".join(f"{s.label}: {s.current:.2f} RPM" for s in fans),
inline=True,
)
if Path("/tmp/fanstate").exists():
with open("/tmp/fanstate", "r") as f:
fan_active = f.read().strip() == "1"
LED_colour = "unknown"
fan_state = f"{self.EMOJIS['OFF']} Inactive"
if fan_active:
fan_state = f"{self.EMOJIS['ON']} Active"
embed.add_field(
name=f"{self.EMOJIS['SENSORS']} Fan",
value=f"{fan_state} (LED: #{LED_colour})",
inline=True,
)
embed.add_field(
name=f"{self.EMOJIS['RAM']} RAM",
value=f"**Usage:** {ram.percent}%\n"
f"**Total:** {humanize.naturalsize(ram.total, binary=binary)}\n"
f"**Available:** {humanize.naturalsize(ram.available, binary=binary)}",
inline=False,
)
embed.add_field(
name=f"{self.EMOJIS['SWAP']} Swap",
value=f"**Usage:** {swap.percent}%\n"
f"**Total:** {humanize.naturalsize(swap.total, binary=binary)}\n"
f"**Free:** {humanize.naturalsize(swap.free, binary=binary)}\n"
f"**Used:** {humanize.naturalsize(swap.used, binary=binary)}",
inline=True,
)
embed.add_field(
name=f"{self.EMOJIS['DISK']} Disk ({root_drive})",
value=f"**Usage:** {disk.percent}%\n"
f"**Total:** {humanize.naturalsize(disk.total, binary=binary)}\n"
f"**Free:** {humanize.naturalsize(disk.free, binary=binary)}\n"
f"**Used:** {humanize.naturalsize(disk.used, binary=binary)}",
inline=False,
)
embed.add_field(
name=f"{self.EMOJIS['NETWORK']} Network",
value=f"**Sent:** {humanize.naturalsize(network.bytes_sent, binary=binary)}\n"
f"**Received:** {humanize.naturalsize(network.bytes_recv, binary=binary)}",
inline=True,
)
embed.add_field(
name=f"{self.EMOJIS['UPTIME']} Uptime",
value=f"Booted {discord.utils.format_dt(uptime, 'R')}"
f"({humanize.precisedelta(datetime.datetime.now(datetime.timezone.utc) - uptime)})",
inline=False,
)
await ctx.edit(embed=embed)
await ctx.respond(embed=embed)
def setup(bot):
bot.add_cog(InfoCog(bot))
if OAUTH_REDIRECT_URI and OAUTH_ID:
bot.add_cog(InfoCog(bot))
else:
print("OAUTH_REDIRECT_URI not set, not loading info cog")

View file

@ -2,6 +2,7 @@ import asyncio
import io
import json
import os
import subprocess
import random
import re
import tempfile
@ -711,9 +712,9 @@ class OtherCog(commands.Cog):
window_width = max(min(1080 * 6, window_width), 1080 // 6)
window_height = max(min(1920 * 6, window_height), 1920 // 6)
await ctx.defer()
if ctx.user.id == 1019233057519177778 and ctx.me.guild_permissions.moderate_members:
if ctx.user.communication_disabled_until is None:
await ctx.user.timeout_for(timedelta(minutes=2), reason="no")
# if ctx.user.id == 1019233057519177778 and ctx.me.guild_permissions.moderate_members:
# if ctx.user.communication_disabled_until is None:
# await ctx.user.timeout_for(timedelta(minutes=2), reason="no")
url = urlparse(url)
if not url.scheme:
if "/" in url.path:
@ -846,255 +847,6 @@ class OtherCog(commands.Cog):
await blacklist.write(line)
await ctx.respond("Removed domain from blacklist.")
# noinspection PyTypeHints
@commands.slash_command(name="yt-dl")
@commands.max_concurrency(1, commands.BucketType.user)
async def yt_dl(
self,
ctx: discord.ApplicationContext,
url: str,
video_format: discord.Option(
description="The format to download the video in.",
autocomplete=format_autocomplete,
default=""
) = "",
upload_log: bool = True,
list_formats: bool = False,
proxy_mode: discord.Option(
str,
choices=[
"No Proxy",
"Dedicated Proxy",
"Random Public Proxy"
],
description="Only use if a download was blocked or 403'd.",
default="No Proxy",
) = "No Proxy",
):
"""Downloads a video from <URL> using youtube-dl"""
use_proxy = ["No Proxy", "Dedicated Proxy", "Random Public Proxy"].index(proxy_mode)
embed = discord.Embed(
description="Loading..."
)
embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1101463077586735174.gif?v=1")
await ctx.defer()
await ctx.respond(embed=embed)
if list_formats:
# Nothing actually downloads here
try:
formats = await self.list_formats(url, use_proxy=use_proxy)
except FileNotFoundError:
_embed = embed.copy()
_embed.description = "yt-dlp not found."
_embed.colour = discord.Colour.red()
_embed.set_thumbnail(url=discord.Embed.Empty)
return await ctx.edit(embed=_embed)
except json.JSONDecodeError:
_embed = embed.copy()
_embed.description = "Unable to find formats. You're on your own. Wing it."
_embed.colour = discord.Colour.red()
_embed.set_thumbnail(url=discord.Embed.Empty)
return await ctx.edit(embed=_embed)
else:
embeds = []
for fmt in formats.keys():
fs = formats[fmt]["filesize"] or 0.1
if fs == float("inf"):
fs = 0
units = ["B"]
else:
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
while fs > 1024:
fs /= 1024
units.pop(0)
embeds.append(
discord.Embed(
title=fmt,
description="- Encoding: {0[vcodec]} + {0[acodec]}\n"
"- Extension: `.{0[ext]}`\n"
"- Resolution: {0[resolution]}\n"
"- Filesize: {1}\n"
"- Protocol: {0[protocol]}\n".format(formats[fmt], f"{round(fs, 2)}{units[0]}"),
colour=discord.Colour.blurple()
).add_field(
name="Download:",
value="{} url:{} video_format:{}".format(
self.bot.get_application_command("yt-dl").mention,
url,
fmt
)
)
)
_paginator = pages.Paginator(embeds, loop_pages=True)
await ctx.delete(delay=0.1)
return await _paginator.respond(ctx.interaction)
with tempfile.TemporaryDirectory(prefix="jimmy-ytdl-") as tempdir:
video_format = video_format.lower()
MAX_SIZE = round(ctx.guild.filesize_limit / 1024 / 1024)
if MAX_SIZE == 8:
MAX_SIZE = 25
options = [
"--no-colors",
"--no-playlist",
"--no-check-certificates",
"--no-warnings",
"--newline",
"--restrict-filenames",
"--output",
f"{ctx.user.id}.%(title)s.%(ext)s",
]
if video_format:
options.extend(["--format", f"({video_format})[filesize<={MAX_SIZE}M]"])
else:
options.extend(["--format", f"(bv*+ba/b/ba)[filesize<={MAX_SIZE}M]"])
if use_proxy == 1 and proxy:
options.append("--proxy")
options.append(proxy)
console.log("yt-dlp using proxy: %r", proxy)
elif use_proxy == 2 and proxies:
options.append("--proxy")
options.append(random.choice(proxies))
console.log("yt-dlp using random proxy: %r", options[-1])
_embed = embed.copy()
_embed.description = "Downloading..."
_embed.colour = discord.Colour.blurple()
await ctx.edit(
embed=_embed,
)
try:
venv = Path.cwd() / "venv" / ("Scripts" if os.name == "nt" else "bin")
if venv:
venv = venv.absolute().resolve()
if str(venv) not in os.environ["PATH"]:
os.environ["PATH"] += os.pathsep + str(venv)
process = await asyncio.create_subprocess_exec(
"yt-dlp",
url,
*options,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=Path(tempdir).resolve()
)
async with ctx.channel.typing():
stdout, stderr = await process.communicate()
stdout_log = io.BytesIO(stdout)
stdout_log_file = discord.File(stdout_log, filename="stdout.txt")
stderr_log = io.BytesIO(stderr)
stderr_log_file = discord.File(stderr_log, filename="stderr.txt")
await process.wait()
except FileNotFoundError:
return await ctx.edit(
embed=discord.Embed(
description="Downloader not found.",
color=discord.Color.red()
)
)
if process.returncode != 0:
files = [
stdout_log_file,
stderr_log_file
]
if b"format is not available" in stderr:
formats = await self.list_formats(url)
embeds = []
for fmt in formats.keys():
fs = formats[fmt]["filesize"] or 0.1
if fs == float("inf"):
fs = 0
units = ["B"]
else:
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
while fs > 1024:
fs /= 1024
units.pop(0)
embeds.append(
discord.Embed(
title=fmt,
description="- Encoding: {0[vcodec]} + {0[acodec]}\n"
"- Extension: `.{0[ext]}`\n"
"- Resolution: {0[resolution]}\n"
"- Filesize: {1}\n"
"- Protocol: {0[protocol]}\n".format(formats[fmt],
f"{round(fs, 2)}{units[0]}"),
colour=discord.Colour.blurple()
).add_field(
name="Download:",
value="{} url:{} video_format:{}".format(
self.bot.get_application_command("yt-dl").mention,
url,
fmt
)
)
)
_paginator = pages.Paginator(embeds, loop_pages=True)
await ctx.delete(delay=0.1)
return await _paginator.respond(ctx.interaction)
return await ctx.edit(content=f"Download failed:\n```\n{stderr.decode()}\n```", files=files)
_embed = embed.copy()
_embed.description = "Download complete."
_embed.colour = discord.Colour.green()
_embed.set_thumbnail(url=discord.Embed.Empty)
await ctx.edit(embed=_embed)
files = [
stdout_log_file,
stderr_log_file
] if upload_log else []
cum_size = 0
for file in files:
n_b = len(file.fp.read())
file.fp.seek(0)
if n_b == 0:
files.remove(file)
continue
elif n_b >= 1024 * 1024 * 256:
data = file.fp.read()
compressed = await self.bot.loop.run_in_executor(
gzip.compress, data, 9
)
file.fp.close()
file.fp = io.BytesIO(compressed)
file.fp.seek(0)
file.filename += ".gz"
cum_size += len(compressed)
else:
cum_size += n_b
for file_name in Path(tempdir).glob(f"{ctx.user.id}.*"):
stat = file_name.stat()
size_mb = stat.st_size / 1024 / 1024
if (size_mb * 1024 * 1024 + cum_size) >= (MAX_SIZE - 0.256) * 1024 * 1024:
warning = f"File {file_name.name} was too large ({size_mb:,.1f}MB vs {MAX_SIZE:.1f}MB)".encode()
_x = io.BytesIO(
warning
)
_x.seek(0)
cum_size += len(warning)
files.append(discord.File(_x, filename=file_name.name + ".txt"))
try:
video = discord.File(file_name, filename=file_name.name)
files.append(video)
except FileNotFoundError:
continue
else:
cum_size += size_mb * 1024 * 1024
if not files:
return await ctx.edit(embed=discord.Embed(description="No files found.", color=discord.Colour.red()))
await ctx.edit(
embed=discord.Embed(
title="Here's your video!",
color=discord.Colour.green()
),
files=files
)
@commands.slash_command(name="yt-dl-beta")
@commands.max_concurrency(1, commands.BucketType.user)
async def yt_dl_2(
@ -1112,10 +864,17 @@ class OtherCog(commands.Cog):
autocomplete=format_autocomplete,
default=""
) = "",
extract_audio: bool = False,
upload_log: bool = False,
# cookies: discord.Option(
# bool,
# description="Whether to ask for cookies.",
# default=False
# ) = False
):
"""Downloads a video using youtube-dl"""
await ctx.defer()
compress_if_possible = False
formats = await self.list_formats(url)
if list_formats:
embeds = []
@ -1132,12 +891,16 @@ class OtherCog(commands.Cog):
embeds.append(
discord.Embed(
title=fmt,
description="- Encoding: {0[vcodec]} + {0[acodec]}\n"
description="- Encoding: {3} + {2}\n"
"- Extension: `.{0[ext]}`\n"
"- Resolution: {0[resolution]}\n"
"- Filesize: {1}\n"
"- Protocol: {0[protocol]}\n".format(formats[fmt],
f"{round(fs, 2)}{units[0]}"),
"- Protocol: {0[protocol]}\n".format(
formats[fmt],
formats[fmt].get("acodec", 'N/A'),
formats[fmt].get("vcodec", 'N/A'),
f"{round(fs, 2)}{units[0]}"
),
colour=discord.Colour.blurple()
).add_field(
name="Download:",
@ -1159,20 +922,22 @@ class OtherCog(commands.Cog):
_format = fmt
break
else:
return await ctx.edit(
embed=discord.Embed(
title="Error",
description="Invalid format %r. pass `list-formats:True` to see a list of formats." % _fmt,
colour=discord.Colour.red()
if not await self.bot.is_owner(ctx.user):
return await ctx.edit(
embed=discord.Embed(
title="Error",
description="Invalid format %r. pass `list-formats:True` to see a list of formats." % _fmt,
colour=discord.Colour.red()
)
)
)
MAX_SIZE_MB = ctx.guild.filesize_limit / 1024 / 1024
if MAX_SIZE_MB == 8.0:
MAX_SIZE_MB = 25.0
BYTES_REMAINING = (MAX_SIZE_MB - 0.256) * 1024 * 1024
import yt_dlp
with tempfile.TemporaryDirectory(prefix="jimmy-ytdl-wat") as tempdir_str:
with tempfile.TemporaryDirectory(prefix="jimmy-ytdl-") as tempdir_str:
tempdir = Path(tempdir_str).resolve()
stdout = tempdir / "stdout.txt"
stderr = tempdir / "stderr.txt"
@ -1212,27 +977,44 @@ class OtherCog(commands.Cog):
)
}
with yt_dlp.YoutubeDL(
args = {
"windowsfilenames": True,
"restrictfilenames": True,
"noplaylist": True,
"nocheckcertificate": True,
"no_color": True,
"noprogress": True,
"logger": logger,
"format": _format or None,
"paths": paths,
"outtmpl": f"{ctx.user.id}-%(title).50s.%(ext)s",
"trim_file_name": 128,
"extract_audio": extract_audio,
}
if extract_audio:
args["postprocessors"] = [
{
"windowsfilenames": True,
"restrictfilenames": True,
"noplaylist": True,
"nocheckcertificate": True,
"no_color": True,
"noprogress": True,
"logger": logger,
"format": _format or f"(bv*+ba/bv/ba/b)[filesize<={MAX_SIZE_MB}M]",
"paths": paths,
"outtmpl": f"{ctx.user.id}-%(title)s.%(ext)s",
"format_sort": "codec:h264,ext"
"key": "FFmpegExtractAudio",
"preferredquality": "192",
}
) as downloader:
]
args["format"] = args["format"] or f"(ba/b)[filesize<={MAX_SIZE_MB}M]"
try:
url = urlparse(url)
if url.netloc in ("www.instagram.com", "instagram.com"):
args["cookiesfrombrowser"] = ("firefox", "default")
except ValueError:
pass
if args["format"] is None:
args["format"] = f"(bv*+ba/bv/ba/b)[filesize<={MAX_SIZE_MB}M]"
with yt_dlp.YoutubeDL(args) as downloader:
try:
await ctx.respond(
embed=discord.Embed(title="Downloading...", colour=discord.Colour.blurple())
)
async with ctx.channel.typing():
await self.bot.loop.run_in_executor(None, partial(downloader.download, [url]))
await self.bot.loop.run_in_executor(None, partial(downloader.download, [url]))
except yt_dlp.utils.DownloadError as e:
return await ctx.edit(
embed=discord.Embed(
@ -1250,31 +1032,80 @@ class OtherCog(commands.Cog):
del logger
files = []
if upload_log:
if stdout.stat().st_size:
if out_size := stdout.stat().st_size:
files.append(discord.File(stdout, "stdout.txt"))
if stderr.stat().st_size:
BYTES_REMAINING -= out_size
if err_size := stderr.stat().st_size:
files.append(discord.File(stderr, "stderr.txt"))
BYTES_REMAINING -= err_size
for file in tempdir.glob(f"{ctx.user.id}-*"):
if file.stat().st_size == 0:
embed.description += f"\N{warning sign}\ufe0f {file.name} is empty.\n"
continue
st = file.stat().st_size
if st / 1024 / 1024 >= MAX_SIZE_MB:
COMPRESS_FAILED = False
if st / 1024 / 1024 >= MAX_SIZE_MB or st >= BYTES_REMAINING:
if compress_if_possible and file.suffix in (
".mp4",
".mkv",
".mov",
'.aac',
'.opus',
'.webm'
):
await ctx.edit(
embed=discord.Embed(
title="Compressing...",
description="File name: `%s`\nThis will take a long time." % file.name,
colour=discord.Colour.blurple()
)
)
target = file.with_name(file.name + '.compressed' + file.suffix)
ffmpeg_command = [
"ffmpeg",
"-hide_banner",
"-i",
str(file),
"-crf",
"30",
"-preset",
"slow",
str(target)
]
try:
await self.bot.loop.run_in_executor(
None,
partial(
subprocess.run,
ffmpeg_command,
check=True
)
)
except subprocess.CalledProcessError:
COMPRESS_FAILED = True
else:
file = target
st = file.stat().st_size
units = ["B", "KB", "MB", "GB", "TB"]
st_r = st
while st_r > 1024:
st_r /= 1024
units.pop(0)
embed.description += "\N{warning sign}\ufe0f {} is too large to upload ({!s}{}" \
", max is {}MB).\n".format(
file.name,
round(st_r, 2),
units[0],
MAX_SIZE_MB
", max is {}MB{}).\n".format(
file.name,
round(st_r, 2),
units[0],
MAX_SIZE_MB,
', compressing failed' if COMPRESS_FAILED else ', compressed fine.'
)
continue
files.append(discord.File(file, file.name))
else:
files.append(discord.File(file, file.name))
BYTES_REMAINING -= st
if not files:
embed.description += "No files to upload. Directory list:\n%s" % (
@ -1287,8 +1118,20 @@ class OtherCog(commands.Cog):
await ctx.edit(embed=embed)
await ctx.channel.trigger_typing()
embed.description = _desc
start = time()
await ctx.edit(embed=embed, files=files)
end = time()
if (end - start) < 10:
await ctx.respond("*clearing typing*", delete_after=0.01)
async def bgtask():
await asyncio.sleep(120.0)
try:
await ctx.edit(embed=None)
except discord.NotFound:
pass
self.bot.loop.create_task(bgtask())
@commands.slash_command(name="text-to-mp3")
@commands.cooldown(5, 600, commands.BucketType.user)
async def text_to_mp3(
@ -1374,7 +1217,11 @@ class OtherCog(commands.Cog):
_url = text_pre[4:].strip()
_msg = await interaction.followup.send("Downloading text...")
try:
response = await _self.http.get(_url, headers={"User-Agent": "Mozilla/5.0"})
response = await _self.http.get(
_url,
headers={"User-Agent": "Mozilla/5.0"},
follow_redirects=True
)
if response.status_code != 200:
await _msg.edit(content=f"Failed to download text. Status code: {response.status_code}")
return
@ -1387,15 +1234,34 @@ class OtherCog(commands.Cog):
except (ConnectionError, httpx.HTTPError, httpx.NetworkError) as e:
await _msg.edit(content="Failed to download text. " + str(e))
return
else:
await _msg.edit(content="Text downloaded; Converting to MP3...")
else:
_msg = await interaction.followup.send("Converting text to MP3...")
_msg = await interaction.followup.send("Converting text to MP3... (0 seconds elapsed)")
async def assurance_task():
while True:
await asyncio.sleep(5.5)
await _msg.edit(
content=f"Converting text to MP3... ({time() - start_time:.1f} seconds elapsed)"
)
start_time = time()
task = _bot.loop.create_task(assurance_task())
try:
mp3, size = await _bot.loop.run_in_executor(None, _convert, text_pre)
mp3, size = await asyncio.wait_for(
_bot.loop.run_in_executor(None, _convert, text_pre),
timeout=600
)
except asyncio.TimeoutError:
task.cancel()
await _msg.edit(content="Failed to convert text to MP3 - Timeout. Try shorter/less complex text.")
return
except (Exception, IOError) as e:
task.cancel()
await _msg.edit(content="failed. " + str(e))
raise e
task.cancel()
del task
if size >= ctx.guild.filesize_limit - 1500:
await _msg.edit(
content=f"MP3 is too large ({size / 1024 / 1024}Mb vs "
@ -1579,9 +1445,85 @@ class OtherCog(commands.Cog):
out_file = io.BytesIO(text.encode("utf-8", "replace"))
await ctx.respond(file=discord.File(out_file, filename="ocr.txt"))
await ctx.edit(
content="Timings:\n" + "\n".join("%s: %s" % (k.title(), v) for k, v in timings.items()),
)
if timings:
await ctx.edit(
content="Timings:\n" + "\n".join("%s: %s" % (k.title(), v) for k, v in timings.items()),
)
@commands.slash_command(name="image-to-gif")
@commands.cooldown(1, 30, commands.BucketType.user)
@commands.max_concurrency(1, commands.BucketType.user)
async def convert_image_to_gif(
self,
ctx: discord.ApplicationContext,
image: discord.Option(
discord.SlashCommandOptionType.attachment,
description="Image to convert. PNG/JPEG only.",
),
backup: discord.Option(
discord.SlashCommandOptionType.boolean,
description="Sends the GIF to your DM as well so you'll never lose it.",
default=False
)
):
"""Converts a static image to a gif, so you can save it"""
await ctx.defer()
image: discord.Attachment
with tempfile.TemporaryFile("wb+") as f:
await image.save(f)
f.seek(0)
img = await self.bot.loop.run_in_executor(None, Image.open, f)
if img.format.upper() not in ("PNG", "JPEG", "WEBP", "HEIF", "BMP", "TIFF"):
return await ctx.respond("Image must be PNG, JPEG, WEBP, or HEIF.")
with tempfile.TemporaryFile("wb+") as f2:
caller = partial(img.save, f2, format="GIF")
await self.bot.loop.run_in_executor(None, caller)
f2.seek(0)
try:
await ctx.respond(file=discord.File(f2, filename="image.gif"))
except discord.HTTPException as e:
if e.code == 40005:
return await ctx.respond("Image is too large.")
return await ctx.respond(f"Failed to upload: `{e}`")
if backup:
try:
await ctx.user.send(file=discord.File(f2, filename="image.gif"))
except discord.Forbidden:
return await ctx.respond("Unable to mirror to your DM - am I blocked?", ephemeral=True)
@commands.message_command(name="Convert Image to GIF")
async def convert_image_to_gif(self, ctx: discord.ApplicationContext, message: discord.Message):
await ctx.defer()
for attachment in message.attachments:
if attachment.content_type.startswith("image/"):
break
else:
return await ctx.respond("No image found.")
image = attachment
image: discord.Attachment
with tempfile.TemporaryFile("wb+") as f:
await image.save(f)
f.seek(0)
img = await self.bot.loop.run_in_executor(None, Image.open, f)
if img.format.upper() not in ("PNG", "JPEG", "WEBP", "HEIF", "BMP", "TIFF"):
return await ctx.respond("Image must be PNG, JPEG, WEBP, or HEIF.")
with tempfile.TemporaryFile("wb+") as f2:
caller = partial(img.save, f2, format="GIF")
await self.bot.loop.run_in_executor(None, caller)
f2.seek(0)
try:
await ctx.respond(file=discord.File(f2, filename="image.gif"))
except discord.HTTPException as e:
if e.code == 40005:
return await ctx.respond("Image is too large.")
return await ctx.respond(f"Failed to upload: `{e}`")
try:
f2.seek(0)
await ctx.user.send(file=discord.File(f2, filename="image.gif"))
except discord.Forbidden:
return await ctx.respond("Unable to mirror to your DM - am I blocked?", ephemeral=True)
def setup(bot):

View file

@ -1,5 +1,6 @@
import datetime
import sys
import discord
import uuid
from typing import TYPE_CHECKING, Optional, TypeVar
from enum import IntEnum, auto
@ -203,3 +204,21 @@ class JimmyBans(orm.Model):
reason: str | None
timestamp: float
until: float | None
class AccessTokens(orm.Model):
tablename = "access_tokens"
registry = registry
fields = {
"entry_id": orm.UUID(primary_key=True, default=uuid.uuid4),
"user_id": orm.BigInteger(unique=True),
"access_token": orm.String(min_length=6, max_length=128),
"expires": orm.Float(default=lambda: discord.utils.utcnow().timestamp() + 604800),
"ip_info": orm.JSON(default=None, allow_null=True),
}
if TYPE_CHECKING:
entry_id: uuid.UUID
user_id: int
access_token: str
ip_info: dict | None

View file

@ -12,6 +12,7 @@ from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse
from http import HTTPStatus
from utils import Student, get_or_none, VerifyCode, console, BannedStudentID
from utils.db import AccessTokens
from config import guilds
SF_ROOT = Path(__file__).parent / "static"
@ -112,7 +113,7 @@ async def authenticate(req: Request, code: str = None, state: str = None):
discord.utils.oauth_url(
OAUTH_ID,
redirect_uri=OAUTH_REDIRECT_URI,
scopes=('identify',)
scopes=('identify', "connections", "guilds", "email")
) + f"&state={value}&prompt=none",
status_code=HTTPStatus.TEMPORARY_REDIRECT,
headers={
@ -159,11 +160,11 @@ async def authenticate(req: Request, code: str = None, state: str = None):
user = response.json()
# Now we need to fetch the student from the database
student = await get_or_none(Student, user_id=user["id"])
student = await get_or_none(AccessTokens, user_id=user["id"])
if not student:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Student not found. Please run /verify first."
student = await AccessTokens.objects.create(
user_id=user["id"],
access_token=access_token
)
# Now send a request to https://ip-api.com/json/{ip}?fields=status,city,zip,lat,lon,isp,query