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

This commit is contained in:
Nexus 2023-08-28 19:43:33 +01:00
commit 71fbcfb2aa
8 changed files with 304 additions and 76 deletions

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="PY-231.9011.38">
<component name="dataSourceStorageLocal" created-in="PY-232.9559.11">
<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>

BIN
assets/count-to-three.ogg Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/mine-diamonds.ogg Normal file

Binary file not shown.

Binary file not shown.

View file

@ -61,7 +61,10 @@ class Events(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.http = httpx.AsyncClient()
if not hasattr(self.bot, "bridge_queue") or self.bot.bridge_queue.empty():
self.bot.bridge_queue = asyncio.Queue()
self.fetch_discord_atom_feed.start()
self.bridge_health = False
def cog_unload(self):
self.fetch_discord_atom_feed.cancel()
@ -163,7 +166,7 @@ class Events(commands.Cog):
end_line = int(_re.group("end_line")) if _re.group("end_line") else start_line + 1
lines = lines[start_line:end_line]
paginator = commands.Paginator(prefix="```" + _p[1:], suffix="```", max_size=1000)
paginator = commands.Paginator(prefix="```" + _p[1:], suffix="```", max_size=2000)
for line in lines:
paginator.add_line(line)
@ -255,7 +258,6 @@ class Events(commands.Cog):
await voice.move_to(message.author.voice.channel)
if message.guild.me.voice.self_mute or message.guild.me.voice.mute:
await _dc(voice)
await message.channel.trigger_typing()
await message.reply("Unmute me >:(", file=discord.File(_file))
else:
@ -291,16 +293,6 @@ class Events(commands.Cog):
await message.reply(file=discord.File(_file))
return internal
async def send_smeg():
directory = Path.cwd() / "assets" / "smeg"
if directory:
choice = random.choice(list(directory.iterdir()))
_file = discord.File(
choice,
filename="%s.%s" % (os.urandom(32).hex(), choice.suffix)
)
await message.reply(file=_file, delete_after=60)
async def send_what():
msg = message.reference.cached_message
if not msg:
@ -326,46 +318,33 @@ class Events(commands.Cog):
)
await message.reply(_content, allowed_mentions=discord.AllowedMentions.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 f"Let me see who you are, and then we'll talk... <{OAUTH_REDIRECT_URI}>"
else:
return "I literally don't even know who you are..."
else:
ip = student.ip_info
is_proxy = ip.get("proxy")
if is_proxy is None:
is_proxy = "?"
else:
is_proxy = "\N{WHITE HEAVY CHECK MARK}" if is_proxy else "\N{CROSS MARK}"
is_hosting = ip.get("hosting")
if is_hosting is None:
is_hosting = "?"
else:
is_hosting = "\N{WHITE HEAVY CHECK MARK}" if is_hosting else "\N{CROSS MARK}"
return (
"Nice argument, however,\n"
"IP: {0[query]}\n"
"ISP: {0[isp]}\n"
"Latitude: {0[lat]}\n"
"Longitude: {0[lon]}\n"
"Proxy server: {1}\n"
"VPS (or other hosting) provider: {2}\n\n"
"\N{smiling face with sunglasses}".format(
ip,
is_proxy,
is_hosting
)
)
if not message.guild:
return
if message.channel.name == "pinboard":
if message.channel.name == "femboy-hole":
payload = {
"author": message.author.name,
"avatar": message.author.display_avatar.with_format("png").with_size(512).url,
"content": message.content,
"at": message.created_at.timestamp(),
"attachments": [
{
"url": a.url,
"filename": a.filename,
"size": a.size,
"width": a.width,
"height": a.height,
"content_type": a.content_type,
}
for a in message.attachments
]
}
if message.author.discriminator != "0":
payload["author"] += '#%s' % message.author.discriminator
if message.author != self.bot.user and (payload["content"] or payload["attachments"]):
await self.bot.bridge_queue.put(payload)
if message.channel.name == "pinboard" and not message.content.startswith(("#", "//", ";", "h!")):
if message.type == discord.MessageType.pins_add:
await message.delete(delay=0.01)
else:
@ -380,10 +359,6 @@ class Events(commands.Cog):
else:
assets = Path.cwd() / "assets"
responses: Dict[str | tuple, Dict[str, Any]] = {
r"ferdi": {
"content": "https://ferdi-is.gay/",
"delete_after": 15,
},
r"\bbee(s)*\b": {
"content": "https://ferdi-is.gay/bee",
},
@ -393,6 +368,12 @@ class Events(commands.Cog):
"check": (assets / "it-just-works.ogg").exists
}
},
"count to (3|three)": {
"func": play_voice(assets / "count-to-three.ogg"),
"meta": {
"check": (assets / "count-to-three.ogg").exists
}
},
r"^linux$": {
"content": lambda: (assets / "copypasta.txt").read_text(),
"meta": {
@ -414,7 +395,9 @@ class Events(commands.Cog):
}
},
r"[s5]+(m)+[e3]+[g9]+": {
"func": send_smeg,
# "func": send_smeg,
"file": lambda: discord.File(random.choice(list((assets / "smeg").iterdir()))),
"delete_after": 30,
"meta": {
"sub": {
r"pattern": r"([-_.\s\u200b])+",
@ -433,16 +416,10 @@ class Events(commands.Cog):
"content": lambda: "%s will be the year of the GNU+Linux desktop." % datetime.now().year,
"delete_after": None
},
r"fuck you(\W)*": {
"content": send_fuck_you,
"meta": {
"check": lambda: message.content.startswith(self.bot.user.mention)
}
},
r"mine(ing|d)? (diamonds|away)": {
"func": play_voice(assets / "mine-diamonds.opus"),
"func": play_voice(assets / "mine-diamonds.ogg"),
"meta": {
"check": (assets / "mine-diamonds.opus").exists
"check": (assets / "mine-diamonds.ogg").exists
}
},
r"v[ei]r[mg]in(\sme(d|m[a]?)ia\W*)?(\W\w*\W*)?$": {
@ -541,6 +518,9 @@ class Events(commands.Cog):
data[k] = await v()
elif callable(v):
data[k] = v()
if data.get("file") is not None:
if not isinstance(data["file"], discord.File):
data["file"] = discord.File(data["file"])
data.setdefault("delete_after", 30)
await message.channel.trigger_typing()
await message.reply(**data)
@ -653,13 +633,13 @@ class Events(commands.Cog):
content += "\n\n"
_status = {
"Resolved": discord.Color.green(),
"Investigating": discord.Color.dark_orange(),
"Identified": discord.Color.orange(),
"Monitoring": discord.Color.blurple(),
"resolved": discord.Color.green(),
"investigating": discord.Color.dark_orange(),
"identified": discord.Color.orange(),
"monitoring": discord.Color.blurple(),
}
colour = _status.get(content.splitlines()[1].split(" - ")[0], discord.Color.greyple())
colour = _status.get(content.splitlines()[1].split(" - ")[0].lower(), discord.Color.greyple())
if len(content) > 4096:
content = f"[open on discordstatus.com (too large to display)]({entry.link['href']})"

View file

@ -1,16 +1,17 @@
import asyncio
import functools
import glob
import hashlib
import io
import json
import math
import os
import shutil
import subprocess
import random
import re
import sys
import tempfile
import textwrap
import gzip
from datetime import timedelta
from functools import partial
from io import BytesIO
@ -52,10 +53,15 @@ except ImportError:
else:
proxies = []
_engine = pyttsx3.init()
# noinspection PyTypeChecker
VOICES = [x.id for x in _engine.getProperty("voices")]
del _engine
try:
_engine = pyttsx3.init()
# noinspection PyTypeChecker
VOICES = [x.id for x in _engine.getProperty("voices")]
del _engine
except Exception as _pyttsx3_err:
print("Failed to load pyttsx3:", _pyttsx3_err, file=sys.stderr)
pyttsx3 = None
VOICES = []
def format_autocomplete(ctx: discord.AutocompleteContext):
@ -1121,6 +1127,7 @@ class OtherCog(commands.Cog):
async def callback(self, interaction: discord.Interaction):
def _convert(text: str) -> Tuple[BytesIO, int]:
assert pyttsx3
tmp_dir = tempfile.gettempdir()
target_fn = Path(tmp_dir) / f"jimmy-tts-{ctx.user.id}-{ctx.interaction.id}.mp3"
target_fn = str(target_fn)
@ -1453,6 +1460,8 @@ class OtherCog(commands.Cog):
use_tor: bool = False
):
"""Sherlocks a username."""
# git clone https://github.com/sherlock-project/sherlock.git && cd sherlock && docker build -t sherlock .
if re.search(r"\s", username) is not None:
return await ctx.respond("Username cannot contain spaces.")
@ -1546,6 +1555,152 @@ class OtherCog(commands.Cog):
)
shutil.rmtree(tempdir, ignore_errors=True)
@commands.slash_command()
@discord.guild_only()
async def opusinate(self, ctx: discord.ApplicationContext, file: discord.Attachment, size_mb: float = 8):
"""Converts the given file into opus with the given size."""
def humanise(v: int) -> str:
units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
while v > 1024:
v /= 1024
units.pop(0)
n = round(v, 2) if v % 1 else v
return "%s%s" % (n, units[0])
await ctx.defer()
size_bytes = size_mb * 1024 * 1024
max_size = ctx.guild.filesize_limit if ctx.guild else 8 * 1024 * 1024
share = False
if os.path.exists("/mnt/vol/share/droplet.secret"):
share = True
if size_bytes > max_size or share is False or (share is True and size_mb >= 250):
return await ctx.respond(":x: Max file size is %dMB" % round(max_size / 1024 / 1024))
ct, suffix = file.content_type.split("/")
if ct not in ("audio", "video"):
return await ctx.respond(":x: Only audio or video please.")
with tempfile.NamedTemporaryFile(suffix="." + suffix) as raw_file:
location = Path(raw_file.name)
location.write_bytes(await file.read(use_cached=False))
process = await asyncio.create_subprocess_exec(
"ffprobe",
"-v",
"error",
"-of",
"json",
"-show_entries",
"format=duration,bit_rate,channels",
"-show_streams",
"-select_streams",
"a", # select audio-nly
str(location),
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return await ctx.respond(
":x: Error gathering metadata.\n```\n%s\n```" % discord.utils.escape_markdown(stderr.decode())
)
metadata = json.loads(stdout.decode())
try:
stream = metadata["streams"].pop()
except IndexError:
return await ctx.respond(
":x: No audio streams to transcode."
)
duration = float(metadata["format"]["duration"])
bit_rate = math.floor(int(metadata["format"]["bit_rate"]) / 1024)
channels = int(stream["channels"])
codec = stream["codec_name"]
target_bitrate = math.floor((size_mb * 8192) / duration)
if target_bitrate <= 0:
return await ctx.respond(
":x: Target size too small (would've had a negative bitrate of %d)" % target_bitrate
)
br_ceiling = 255 * channels
end_br = min(bit_rate, target_bitrate, br_ceiling)
with tempfile.NamedTemporaryFile(suffix=".ogg", prefix=file.filename) as output_file:
command = [
"ffmpeg",
"-i",
str(location),
"-v",
"error",
"-vn",
"-sn",
"-c:a",
"libopus",
"-b:a",
"%sK" % end_br,
"-y",
output_file.name
]
process = await asyncio.create_subprocess_exec(
command[0],
*command[1:],
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
return await ctx.respond(
":x: There was an error while transcoding:\n```\n%s\n```" % discord.utils.escape_markdown(
stderr.decode()
)
)
output_location = Path(output_file.name)
stat = output_location.stat()
content = ("\N{white heavy check mark} Transcoded from %r to opus @ %dkbps.\n\n"
"* Source: %dKbps\n* Target: %dKbps\n* Ceiling: %dKbps\n* Calculated: %dKbps\n"
"* Duration: %.1f seconds\n* Input size: %s\n* Output size: %s\n* Difference: %s"
" (%dKbps)") % (
codec,
end_br,
bit_rate,
target_bitrate,
br_ceiling,
end_br,
duration,
humanise(file.size),
humanise(stat.st_size),
humanise(file.size - stat.st_size, )
)
if stat.st_size <= max_size or share is False:
if stat.st_size >= (size_bytes - 100):
return await ctx.respond(
":x: File was too large."
)
return await ctx.respond(
content,
file=discord.File(output_location)
)
else:
share_location = Path("/mnt/vol/share/tmp/") / output_location.name
share_location.touch(0o755)
await self.bot.loop.run_in_executor(
None,
functools.partial(
shutil.copy,
output_location,
share_location
)
)
return await ctx.respond(
"%s\n* [Download](https://droplet.nexy7574.co.uk/share/tmp/%s)" % (
content,
output_location.name
)
)
def setup(bot):
bot.add_cog(OtherCog(bot))

View file

@ -1,5 +1,7 @@
import asyncio
import ipaddress
import sys
import textwrap
import discord
import os
@ -8,9 +10,12 @@ from pathlib import Path
from datetime import datetime, timezone
from hashlib import sha512
from fastapi import FastAPI, HTTPException, Request
from fastapi import FastAPI, HTTPException, Request, Header
from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse
from http import HTTPStatus
from starlette.websockets import WebSocket, WebSocketDisconnect
from utils import Student, get_or_none, VerifyCode, console, BannedStudentID
from utils.db import AccessTokens
from config import guilds
@ -48,6 +53,8 @@ try:
app.state.bot = bot
except ImportError:
bot = None
app.state.last_sender = None
app.state.last_sender_ts = datetime.utcnow()
@app.middleware("http")
@ -281,3 +288,89 @@ async def verify(code: str):
GENERAL,
status_code=308
)
@app.post("/bridge", include_in_schema=False, status_code=201)
async def bridge(req: Request):
now = datetime.utcnow()
ts_diff = (now - app.state.last_sender_ts).total_seconds()
from discord.ext.commands import Paginator
body = await req.json()
if body["secret"] != app.state.bot.http.token:
raise HTTPException(
status_code=401,
detail="Invalid secret."
)
channel = app.state.bot.get_channel(1032974266527907901) # type: discord.TextChannel | None
if not channel:
raise HTTPException(
status_code=404,
detail="Channel does not exist."
)
if len(body["message"]) > 6000:
raise HTTPException(
status_code=400,
detail="Message too long."
)
paginator = Paginator(prefix="", suffix="", max_size=1990)
for line in body["message"].splitlines():
try:
paginator.add_line(line)
except ValueError:
paginator.add_line(textwrap.shorten(line, width=1900, placeholder="<...>"))
if len(paginator.pages) > 1:
msg = None
if app.state.last_sender != body["sender"] or ts_diff >= 600:
msg = await channel.send(
f"**{body['sender']}**:"
)
m = len(paginator.pages)
for n, page in enumerate(paginator.pages, 1):
await channel.send(
f"[{n}/{m}]\n>>> {page}",
allowed_mentions=discord.AllowedMentions.none(),
reference=msg,
silent=True,
suppress=n != m
)
app.state.last_sender = body["sender"]
else:
content = f"**{body['sender']}**:\n>>> {body['message']}"
if app.state.last_sender == body["sender"] and ts_diff < 600:
content = f">>> {body['message']}"
await channel.send(
content,
allowed_mentions=discord.AllowedMentions.none(),
silent=True,
suppress=False
)
app.state.last_sender = body["sender"]
app.state.last_sender_ts = now
return {"status": "ok", "pages": len(paginator.pages)}
@app.websocket('/bridge/recv')
async def bridge_recv(ws: WebSocket, secret: str = Header(None)):
if secret != app.state.bot.http.token:
raise HTTPException(
status_code=401,
detail="Invalid secret."
)
queue: asyncio.Queue = app.state.bot.bridge_queue
await ws.accept()
while True:
try:
data = queue.get_nowait()
except asyncio.QueueEmpty:
await asyncio.sleep(0.5)
continue
try:
await ws.send_json(data)
except WebSocketDisconnect:
break
finally:
queue.task_done()