mirror of
https://github.com/nexy7574/LCC-bot.git
synced 2024-09-19 18:16:34 +01:00
Merge branch 'master' of github.com:EEKIM10/LCC-Bot
This commit is contained in:
commit
71fbcfb2aa
8 changed files with 304 additions and 76 deletions
|
@ -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>"</identifier-quote-string>
|
||||
|
|
BIN
assets/count-to-three.ogg
Normal file
BIN
assets/count-to-three.ogg
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/mine-diamonds.ogg
Normal file
BIN
assets/mine-diamonds.ogg
Normal file
Binary file not shown.
Binary file not shown.
114
cogs/events.py
114
cogs/events.py
|
@ -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']})"
|
||||
|
|
169
cogs/other.py
169
cogs/other.py
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue