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"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <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"> <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"> <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> <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): def __init__(self, bot):
self.bot = bot self.bot = bot
self.http = httpx.AsyncClient() 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.fetch_discord_atom_feed.start()
self.bridge_health = False
def cog_unload(self): def cog_unload(self):
self.fetch_discord_atom_feed.cancel() 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 end_line = int(_re.group("end_line")) if _re.group("end_line") else start_line + 1
lines = lines[start_line:end_line] 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: for line in lines:
paginator.add_line(line) paginator.add_line(line)
@ -255,7 +258,6 @@ class Events(commands.Cog):
await voice.move_to(message.author.voice.channel) await voice.move_to(message.author.voice.channel)
if message.guild.me.voice.self_mute or message.guild.me.voice.mute: if message.guild.me.voice.self_mute or message.guild.me.voice.mute:
await _dc(voice)
await message.channel.trigger_typing() await message.channel.trigger_typing()
await message.reply("Unmute me >:(", file=discord.File(_file)) await message.reply("Unmute me >:(", file=discord.File(_file))
else: else:
@ -291,16 +293,6 @@ class Events(commands.Cog):
await message.reply(file=discord.File(_file)) await message.reply(file=discord.File(_file))
return internal 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(): async def send_what():
msg = message.reference.cached_message msg = message.reference.cached_message
if not msg: if not msg:
@ -326,46 +318,33 @@ class Events(commands.Cog):
) )
await message.reply(_content, allowed_mentions=discord.AllowedMentions.none()) 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: if not message.guild:
return 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: if message.type == discord.MessageType.pins_add:
await message.delete(delay=0.01) await message.delete(delay=0.01)
else: else:
@ -380,10 +359,6 @@ class Events(commands.Cog):
else: else:
assets = Path.cwd() / "assets" assets = Path.cwd() / "assets"
responses: Dict[str | tuple, Dict[str, Any]] = { responses: Dict[str | tuple, Dict[str, Any]] = {
r"ferdi": {
"content": "https://ferdi-is.gay/",
"delete_after": 15,
},
r"\bbee(s)*\b": { r"\bbee(s)*\b": {
"content": "https://ferdi-is.gay/bee", "content": "https://ferdi-is.gay/bee",
}, },
@ -393,6 +368,12 @@ class Events(commands.Cog):
"check": (assets / "it-just-works.ogg").exists "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$": { r"^linux$": {
"content": lambda: (assets / "copypasta.txt").read_text(), "content": lambda: (assets / "copypasta.txt").read_text(),
"meta": { "meta": {
@ -414,7 +395,9 @@ class Events(commands.Cog):
} }
}, },
r"[s5]+(m)+[e3]+[g9]+": { 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": { "meta": {
"sub": { "sub": {
r"pattern": r"([-_.\s\u200b])+", 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, "content": lambda: "%s will be the year of the GNU+Linux desktop." % datetime.now().year,
"delete_after": None "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)": { r"mine(ing|d)? (diamonds|away)": {
"func": play_voice(assets / "mine-diamonds.opus"), "func": play_voice(assets / "mine-diamonds.ogg"),
"meta": { "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*)?$": { 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() data[k] = await v()
elif callable(v): elif callable(v):
data[k] = 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) data.setdefault("delete_after", 30)
await message.channel.trigger_typing() await message.channel.trigger_typing()
await message.reply(**data) await message.reply(**data)
@ -653,13 +633,13 @@ class Events(commands.Cog):
content += "\n\n" content += "\n\n"
_status = { _status = {
"Resolved": discord.Color.green(), "resolved": discord.Color.green(),
"Investigating": discord.Color.dark_orange(), "investigating": discord.Color.dark_orange(),
"Identified": discord.Color.orange(), "identified": discord.Color.orange(),
"Monitoring": discord.Color.blurple(), "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: if len(content) > 4096:
content = f"[open on discordstatus.com (too large to display)]({entry.link['href']})" content = f"[open on discordstatus.com (too large to display)]({entry.link['href']})"

View file

@ -1,16 +1,17 @@
import asyncio import asyncio
import functools
import glob import glob
import hashlib
import io import io
import json import json
import math
import os import os
import shutil import shutil
import subprocess
import random import random
import re import re
import sys
import tempfile import tempfile
import textwrap import textwrap
import gzip
from datetime import timedelta
from functools import partial from functools import partial
from io import BytesIO from io import BytesIO
@ -52,10 +53,15 @@ except ImportError:
else: else:
proxies = [] proxies = []
_engine = pyttsx3.init() try:
# noinspection PyTypeChecker _engine = pyttsx3.init()
VOICES = [x.id for x in _engine.getProperty("voices")] # noinspection PyTypeChecker
del _engine 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): def format_autocomplete(ctx: discord.AutocompleteContext):
@ -1121,6 +1127,7 @@ class OtherCog(commands.Cog):
async def callback(self, interaction: discord.Interaction): async def callback(self, interaction: discord.Interaction):
def _convert(text: str) -> Tuple[BytesIO, int]: def _convert(text: str) -> Tuple[BytesIO, int]:
assert pyttsx3
tmp_dir = tempfile.gettempdir() tmp_dir = tempfile.gettempdir()
target_fn = Path(tmp_dir) / f"jimmy-tts-{ctx.user.id}-{ctx.interaction.id}.mp3" target_fn = Path(tmp_dir) / f"jimmy-tts-{ctx.user.id}-{ctx.interaction.id}.mp3"
target_fn = str(target_fn) target_fn = str(target_fn)
@ -1453,6 +1460,8 @@ class OtherCog(commands.Cog):
use_tor: bool = False use_tor: bool = False
): ):
"""Sherlocks a username.""" """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: if re.search(r"\s", username) is not None:
return await ctx.respond("Username cannot contain spaces.") return await ctx.respond("Username cannot contain spaces.")
@ -1546,6 +1555,152 @@ class OtherCog(commands.Cog):
) )
shutil.rmtree(tempdir, ignore_errors=True) 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): def setup(bot):
bot.add_cog(OtherCog(bot)) bot.add_cog(OtherCog(bot))

View file

@ -1,5 +1,7 @@
import asyncio
import ipaddress import ipaddress
import sys import sys
import textwrap
import discord import discord
import os import os
@ -8,9 +10,12 @@ from pathlib import Path
from datetime import datetime, timezone from datetime import datetime, timezone
from hashlib import sha512 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 fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse
from http import HTTPStatus from http import HTTPStatus
from starlette.websockets import WebSocket, WebSocketDisconnect
from utils import Student, get_or_none, VerifyCode, console, BannedStudentID from utils import Student, get_or_none, VerifyCode, console, BannedStudentID
from utils.db import AccessTokens from utils.db import AccessTokens
from config import guilds from config import guilds
@ -48,6 +53,8 @@ try:
app.state.bot = bot app.state.bot = bot
except ImportError: except ImportError:
bot = None bot = None
app.state.last_sender = None
app.state.last_sender_ts = datetime.utcnow()
@app.middleware("http") @app.middleware("http")
@ -281,3 +288,89 @@ async def verify(code: str):
GENERAL, GENERAL,
status_code=308 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()