diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml
index 7bda43d..1fde411 100644
--- a/.idea/dataSources.local.xml
+++ b/.idea/dataSources.local.xml
@@ -1,6 +1,6 @@
-
+
"
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 657e23f..c134054 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/assets/boris.jpeg b/assets/boris.jpeg
deleted file mode 100644
index 6fb349b..0000000
Binary files a/assets/boris.jpeg and /dev/null differ
diff --git a/assets/bus.m4a b/assets/bus.m4a
deleted file mode 100644
index 3427c1a..0000000
Binary files a/assets/bus.m4a and /dev/null differ
diff --git a/assets/carat.jpg b/assets/carat.jpg
deleted file mode 100644
index 0e49c87..0000000
Binary files a/assets/carat.jpg and /dev/null differ
diff --git a/assets/china.m4a b/assets/china.m4a
deleted file mode 100644
index 6cff714..0000000
Binary files a/assets/china.m4a and /dev/null differ
diff --git a/assets/common-sense.m4a b/assets/common-sense.m4a
deleted file mode 100644
index dce437a..0000000
Binary files a/assets/common-sense.m4a and /dev/null differ
diff --git a/assets/count-to-three.ogg b/assets/count-to-three.ogg
deleted file mode 100644
index 5e0ac9f..0000000
Binary files a/assets/count-to-three.ogg and /dev/null differ
diff --git a/assets/disgrace.m4a b/assets/disgrace.m4a
deleted file mode 100644
index f4a7d56..0000000
Binary files a/assets/disgrace.m4a and /dev/null differ
diff --git a/assets/drones.m4a b/assets/drones.m4a
deleted file mode 100644
index 477edad..0000000
Binary files a/assets/drones.m4a and /dev/null differ
diff --git a/assets/fedora.webp b/assets/fedora.webp
deleted file mode 100644
index 0ba7de3..0000000
Binary files a/assets/fedora.webp and /dev/null differ
diff --git a/assets/hair.m4a b/assets/hair.m4a
deleted file mode 100644
index b58542a..0000000
Binary files a/assets/hair.m4a and /dev/null differ
diff --git a/assets/it-just-works.ogg b/assets/it-just-works.ogg
deleted file mode 100644
index 31a1400..0000000
Binary files a/assets/it-just-works.ogg and /dev/null differ
diff --git a/assets/lupupa.jpg b/assets/lupupa.jpg
deleted file mode 100644
index 23e2749..0000000
Binary files a/assets/lupupa.jpg and /dev/null differ
diff --git a/assets/mine-diamonds.ogg b/assets/mine-diamonds.ogg
deleted file mode 100644
index 948297c..0000000
Binary files a/assets/mine-diamonds.ogg and /dev/null differ
diff --git a/assets/peppa-pig.m4a b/assets/peppa-pig.m4a
deleted file mode 100644
index 4a35c02..0000000
Binary files a/assets/peppa-pig.m4a and /dev/null differ
diff --git a/assets/pork.m4a b/assets/pork.m4a
deleted file mode 100644
index d64e265..0000000
Binary files a/assets/pork.m4a and /dev/null differ
diff --git a/assets/scrapped.m4a b/assets/scrapped.m4a
deleted file mode 100644
index b8680bc..0000000
Binary files a/assets/scrapped.m4a and /dev/null differ
diff --git a/assets/smeg/smeg-1.jpg b/assets/smeg/smeg-1.jpg
deleted file mode 100644
index 79ed38b..0000000
Binary files a/assets/smeg/smeg-1.jpg and /dev/null differ
diff --git a/assets/smeg/smeg-1.png b/assets/smeg/smeg-1.png
deleted file mode 100644
index ded0ceb..0000000
Binary files a/assets/smeg/smeg-1.png and /dev/null differ
diff --git a/assets/smeg/smeg-2.gif b/assets/smeg/smeg-2.gif
deleted file mode 100644
index 14d3b3b..0000000
Binary files a/assets/smeg/smeg-2.gif and /dev/null differ
diff --git a/assets/smeg/smeg-2.jpg b/assets/smeg/smeg-2.jpg
deleted file mode 100644
index 4c545d9..0000000
Binary files a/assets/smeg/smeg-2.jpg and /dev/null differ
diff --git a/assets/smeg/smeg-3.jpg b/assets/smeg/smeg-3.jpg
deleted file mode 100644
index a188fae..0000000
Binary files a/assets/smeg/smeg-3.jpg and /dev/null differ
diff --git a/assets/smeg/smeg-4.jpg b/assets/smeg/smeg-4.jpg
deleted file mode 100644
index c5c9d39..0000000
Binary files a/assets/smeg/smeg-4.jpg and /dev/null differ
diff --git a/assets/smeg/smeg-5.jpg b/assets/smeg/smeg-5.jpg
deleted file mode 100644
index f9b8160..0000000
Binary files a/assets/smeg/smeg-5.jpg and /dev/null differ
diff --git a/assets/tea.m4a b/assets/tea.m4a
deleted file mode 100644
index f45d51d..0000000
Binary files a/assets/tea.m4a and /dev/null differ
diff --git a/assets/virgin/14330-ping.png b/assets/virgin/14330-ping.png
deleted file mode 100644
index 18c1468..0000000
Binary files a/assets/virgin/14330-ping.png and /dev/null differ
diff --git a/assets/virgin/22847-mc.png b/assets/virgin/22847-mc.png
deleted file mode 100644
index f44bc1f..0000000
Binary files a/assets/virgin/22847-mc.png and /dev/null differ
diff --git a/assets/virgin/23891-mc.png b/assets/virgin/23891-mc.png
deleted file mode 100644
index 03f7adb..0000000
Binary files a/assets/virgin/23891-mc.png and /dev/null differ
diff --git a/assets/virgin/29057-dc.png b/assets/virgin/29057-dc.png
deleted file mode 100644
index 9513990..0000000
Binary files a/assets/virgin/29057-dc.png and /dev/null differ
diff --git a/assets/virgin/6437-mc.png b/assets/virgin/6437-mc.png
deleted file mode 100644
index dafe907..0000000
Binary files a/assets/virgin/6437-mc.png and /dev/null differ
diff --git a/assets/virgin/6857-speedtest.png b/assets/virgin/6857-speedtest.png
deleted file mode 100644
index 8fec6b2..0000000
Binary files a/assets/virgin/6857-speedtest.png and /dev/null differ
diff --git a/assets/virgin/7566-mc.png b/assets/virgin/7566-mc.png
deleted file mode 100644
index 838d2c7..0000000
Binary files a/assets/virgin/7566-mc.png and /dev/null differ
diff --git a/assets/virgin/9967-ping.png b/assets/virgin/9967-ping.png
deleted file mode 100644
index 3655b3a..0000000
Binary files a/assets/virgin/9967-ping.png and /dev/null differ
diff --git a/assets/visio.png b/assets/visio.png
deleted file mode 100644
index ace54cf..0000000
Binary files a/assets/visio.png and /dev/null differ
diff --git a/assets/wheat.m4a b/assets/wheat.m4a
deleted file mode 100644
index 63fc618..0000000
Binary files a/assets/wheat.m4a and /dev/null differ
diff --git a/cogs/mcdonalds.py b/cogs/.mcdonalds.py.old
similarity index 100%
rename from cogs/mcdonalds.py
rename to cogs/.mcdonalds.py.old
diff --git a/cogs/events.py b/cogs/events.py
index 379fde5..b34b5d7 100644
--- a/cogs/events.py
+++ b/cogs/events.py
@@ -110,16 +110,9 @@ class Events(commands.Cog):
if member.guild is None or member.guild.id not in guilds:
return
- # student: Optional[Student] = await get_or_none(Student, user_id=member.id)
- # if student and student.id:
- # role = discord.utils.find(lambda r: r.name.lower() == "verified", member.guild.roles)
- # if role and role < member.guild.me.top_role:
- # await member.add_roles(role, reason="Verified")
-
channel: discord.TextChannel = discord.utils.get(member.guild.text_channels, name="general")
if channel and channel.can_send():
await channel.send(
- # f"{LTR} {member.mention} (`{member}`, {f'{student.id}' if student else 'pending verification'})"
f"{LTR} {member.mention}"
)
@@ -128,77 +121,12 @@ class Events(commands.Cog):
if member.guild is None or member.guild.id not in guilds:
return
- # student: Optional[Student] = await get_or_none(Student, user_id=member.id)
channel: discord.TextChannel = discord.utils.get(member.guild.text_channels, name="general")
if channel and channel.can_send():
await channel.send(
- # f"{RTL} {member.mention} (`{member}`, {f'{student.id}' if student else 'pending verification'})"
f"{RTL} {member.mention}"
)
- async def process_message_for_github_links(self, message: discord.Message):
- RAW_URL = "https://github.com/{repo}/raw/{branch}/{path}"
- _re = re.match(
- r"https://github\.com/(?P[a-zA-Z0-9-]+/[\w.-]+)/blob/(?P[^#>]+)(\?[^#>]+)?"
- r"(#L(?P\d+)(([-~:]|(\.\.))L(?P\d+))?)",
- message.content,
- )
- if _re:
- branch, path = _re.group("path").split("/", 1)
- _p = Path(path).suffix
- url = RAW_URL.format(repo=_re.group("repo"), branch=branch, path=path)
- if all((GITHUB_PASSWORD, GITHUB_USERNAME)):
- auth = (GITHUB_USERNAME, GITHUB_PASSWORD)
- else:
- auth = None
- response = await self.http.get(url, follow_redirects=True, auth=auth)
- if response.status_code == 200:
- ctx = await self.bot.get_context(message)
- lines = response.text.splitlines()
- if _re.group("start_line"):
- start_line = int(_re.group("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]
-
- paginator = commands.Paginator(prefix="```" + _p[1:], suffix="```", max_size=2000)
- for line in lines:
- paginator.add_line(line)
-
- _pages = paginator.pages
- paginator2 = pages.Paginator(_pages, timeout=300)
- # noinspection PyTypeChecker
- await paginator2.send(ctx, reference=message.to_reference())
- if message.channel.permissions_for(message.guild.me).manage_messages:
- await message.edit(suppress=True)
- else:
- RAW_URL = "https://github.com/{repo}/archive/refs/heads/{branch}.zip"
- _full_re = re.finditer(
- r"https://github\.com/(?P[a-zA-Z0-9-]+/[\w.-]+)(/tree/(?P[^#>]+))?\.(git|zip)",
- message.content,
- )
- for _match in _full_re:
- repo = _match.group("repo")
- branch = _match.group("branch") or "master"
- url = RAW_URL.format(
- repo=repo,
- branch=branch,
- )
- if all((GITHUB_PASSWORD, GITHUB_USERNAME)):
- auth = (GITHUB_USERNAME, GITHUB_PASSWORD)
- else:
- auth = None
- async with message.channel.typing():
- response = await self.http.get(url, follow_redirects=True, auth=auth)
- if response.status_code == 200:
- content = response.content
- if len(content) > message.guild.filesize_limit - 1000:
- continue
- _io = io.BytesIO(content)
- fn = f"{repo.replace('/', '-')}-{branch}.zip"
- await message.reply(file=discord.File(_io, filename=fn))
- if message.channel.permissions_for(message.guild.me).manage_messages:
- await message.edit(suppress=True)
-
@commands.Cog.listener()
async def on_voice_state_update(self, member: discord.Member, *_):
me_voice = member.guild.me.voice
@@ -213,106 +141,6 @@ class Events(commands.Cog):
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
- def play_voice(_file):
- async def internal():
- if message.author.voice is not None and message.author.voice.channel is not None:
- voice: discord.VoiceClient | None = None
- if message.guild.voice_client is not None:
- # noinspection PyUnresolvedReferences
- if message.guild.voice_client.is_playing():
- return
- try:
- await _dc(message.guild.voice_client)
- except discord.HTTPException:
- pass
- try:
- voice = await message.author.voice.channel.connect(timeout=10, reconnect=False)
- except asyncio.TimeoutError:
- await message.channel.trigger_typing()
- await message.reply(
- "I'd play the song but discord's voice servers are shit.", file=discord.File(_file)
- )
- region = message.author.voice.channel.rtc_region
- # noinspection PyUnresolvedReferences
- self.log.warning(
- "Timed out connecting to voice channel: {0.name} in {0.guild.name} "
- "(region {1})".format(
- message.author.voice.channel, region.name if region else "auto (unknown)"
- )
- )
- return
-
- if voice.channel != 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:
- await message.channel.trigger_typing()
- await message.reply("Unmute me >:(", file=discord.File(_file))
- else:
-
- def after(err):
- self.bot.loop.create_task(
- _dc(voice),
- )
- if err is not None:
- self.log.error(f"Error playing audio: {err}", exc_info=err)
- self.bot.loop.create_task(message.add_reaction("\N{speaker with cancellation stroke}"))
- else:
- self.bot.loop.create_task(
- message.remove_reaction("\N{speaker with three sound waves}", self.bot.user)
- )
- self.bot.loop.create_task(message.add_reaction("\N{speaker}"))
-
- # noinspection PyTypeChecker
- src = discord.FFmpegPCMAudio(str(_file.resolve()), stderr=subprocess.DEVNULL)
- src = discord.PCMVolumeTransformer(src, volume=0.5)
- voice.play(src, after=after)
- if message.channel.permissions_for(message.guild.me).add_reactions:
- await message.add_reaction("\N{speaker with three sound waves}")
- else:
- await message.channel.trigger_typing()
- await message.reply(file=discord.File(_file))
-
- return internal
-
- async def send_what():
- msg = message.reference.cached_message
- if not msg:
- try:
- msg = await message.channel.fetch_message(message.reference.message_id)
- except discord.HTTPException:
- return
-
- if msg.content.count(f"{self.bot.user.mention} said ") >= 2:
- await message.reply("You really are deaf, aren't you.")
- elif not msg.content:
- await message.reply(
- "Maybe *I* need to get my hearing checked, I have no idea what {} said.".format(msg.author.mention)
- )
- else:
- text = "{0.author.mention} said '{0.content}', you deaf sod.".format(msg)
- _content = textwrap.shorten(text, width=2000, placeholder="[...]")
- await message.reply(_content, allowed_mentions=discord.AllowedMentions.none())
-
- def get_sloc_count():
- root = Path.cwd()
- root_files = list(root.glob("**/*.py"))
- root_files.append(root / "main.py")
- root_files = list(filter(lambda f: "venv" not in f.parents and "venv" not in f.parts, root_files))
- lines = 0
-
- for file in root_files:
- try:
- code = file.read_text()
- except (UnicodeDecodeError, IOError, SystemError):
- continue
- for line in code.splitlines():
- line = line.strip()
- if not line or line.startswith("#"):
- continue
- lines += 1
- return lines, len(root_files)
-
if not message.guild:
return
@@ -352,200 +180,10 @@ class Events(commands.Cog):
if message.author != self.bot.user and (payload.content or payload.attachments):
await self.bot.bridge_queue.put(payload.model_dump())
- 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:
- try:
- await message.pin(reason="Automatic pinboard pinning")
- except discord.HTTPException as e:
- return await message.reply(f"Failed to auto-pin: {e}", delete_after=10)
- elif message.channel.name in ("verify", "timetable") and message.author != self.bot.user:
+ if message.channel.name in ("verify", "timetable") and message.author != self.bot.user:
if message.channel.permissions_for(message.guild.me).manage_messages:
await message.delete(delay=1)
- else:
- assets = Path.cwd() / "assets"
- responses: Dict[str | tuple, Dict[str, Any]] = {
- r"\bbee(s)*\b": {
- "content": "https://ferdi-is.gay/bee",
- },
- r"it just works": {
- "func": play_voice(assets / "it-just-works.ogg"),
- "meta": {"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": {"needs_mention": True, "check": (assets / "copypasta.txt").exists},
- },
- r"carat": {
- "file": discord.File(assets / "carat.jpg"),
- "delete_after": None,
- "meta": {"check": (assets / "carat.jpg").exists},
- },
- r"(lupupa|fuck(ed)? the hell out\W*)": {
- "file": discord.File(assets / "lupupa.jpg"),
- "meta": {"check": (assets / "lupupa.jpg").exists},
- },
- r"[s5]+(m)+[e3]+[g9]+": {
- # "func": send_smeg,
- "file": lambda: discord.File(random.choice(list((assets / "smeg").iterdir()))),
- "delete_after": 30,
- "meta": {"sub": {r"pattern": r"([-_.\s\u200b])+", r"with": ""}, "check": (assets / "smeg").exists},
- },
- r"(what|huh)(\?|!)*$": {"func": send_what, "meta": {"check": lambda: message.reference is not None}},
- ("year", "linux", "desktop"): {
- "content": lambda: "%s will be the year of the GNU+Linux desktop." % datetime.now().year,
- "delete_after": None,
- },
- r"mine(ing|d)? (diamonds|away)": {
- "func": play_voice(assets / "mine-diamonds.ogg"),
- "meta": {"check": (assets / "mine-diamonds.ogg").exists},
- },
- 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()))),
- "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"},
- r"(bor(r)?is|johnson)": {"file": discord.File(assets / "boris.jpeg")},
- r"\W?(s(ource\w)?)?l(ines\s)?o(f\s)?c(ode)?(\W)?$": {
- "content": lambda: "I have {:,} lines of source code across {:,} files!".format(*get_sloc_count())
- },
- r"t(ry\s)?i(t\s)?a(nd\s)?see.*": {"content": "https://tryitands.ee"},
- r"disgrace": {"file": discord.File(assets / "disgrace.m4a")},
- r"china": {"file": discord.File(assets / "china.m4a")},
- r"drones": {"file": discord.File(assets / "drones.m4a")},
- r"pork($|\W+)|markets": {"file": discord.File(assets / "pork.m4a")},
- r"(wo)?man\sis\sa\s(wo)?man|(trans(\s)?)?gender\W*$": {
- "file": discord.File(assets / "common-sense.m4a")
- },
- r"scrapped(\sit)?|((7\s|seven\s)?different\s)?bins|(meat\s|flying\s)?tax": {
- "file": discord.File(assets / "scrapped.m4a")
- },
- r"peppa|pig": {"file": discord.File(assets / "peppa-pig.m4a")},
- r"brush|hair": {"file": discord.File(assets / "hair.m4a")},
- r"((cup\s)?of\s)?tea\W*$": {"file": discord.File(assets / "tea.m4a")},
- r"wheat|fields": {"file": discord.File(assets / "wheat.m4a")},
- r"(\W|^)bus((s)?es)?\W*$": {"file": discord.File(assets / "bus.m4a")},
- r"^DoH$": {"content": "DoH: Domain Name Service over Hyper Text Transfer Protocol Secure"},
- r"^DoT$": {"content": "DoT: Domain Name Service over Transport Layer Security"},
- r"^DoQ$": {
- "content": "DoQ: Domain Name Service over Quick User Datagram Protocol Internet Connections"
- },
- r"^(Do)?DTLS$": {"content": "DoDTLS: Domain Name Service over Datagram Transport Layer Security"},
- r"^(arch|nix(os)?)$": {
- "content": "https://kvinneby.vendicated.dev/@vee/statuses/01HJPVWA7CPFP14YAQ9DTGX1HK",
- "file": discord.File(assets / "fedora.webp")
- }
- }
- # Stop responding to any bots
- if message.author.bot is True:
- return
-
- # Only respond if the message has content...
- if message.content and message.channel.can_send(discord.Embed, discord.File):
- for key, data in responses.items():
- meta = data.pop("meta", {})
- if meta.get("needs_mention"):
- if not self.bot.user.mention not in message.mentions:
- continue
-
- if meta.get("check"):
- try:
- okay = meta["check"]()
- except (Exception, RuntimeError):
- traceback.print_exc()
- okay = False
-
- if not okay:
- continue
- elif meta.get("checks") and isinstance(meta["checks"], list):
- for check in meta["checks"]:
- try:
- okay = check()
- except (Exception, RuntimeError):
- traceback.print_exc()
- okay = False
-
- if not okay:
- break
- else:
- continue
-
- if meta.get("sub") is not None and isinstance(meta["sub"], dict):
- content = re.sub(meta["sub"]["pattern"], meta["sub"]["with"], message.content)
- else:
- content = message.content
-
- if isinstance(key, str):
- regex = re.compile(key.casefold(), re.IGNORECASE)
- if not regex.search(content):
- continue
- elif isinstance(key, tuple):
- if not all(k in content for k in key):
- continue
-
- if "func" in data:
- try:
- if inspect.iscoroutinefunction(data["func"]) or inspect.iscoroutine(data["func"]):
- await data["func"]()
- break
- else:
- data["func"]()
- break
- except (Exception, RuntimeError):
- traceback.print_exc()
- continue
- else:
- for k, v in data.copy().items():
- if inspect.iscoroutinefunction(data[k]) or inspect.iscoroutine(data[k]):
- 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", 300)
- await message.channel.trigger_typing()
- await message.reply(**data)
- break
-
- await self.process_message_for_github_links(message)
-
- T_EMOJI = "\U0001f3f3\U0000fe0f\U0000200d\U000026a7\U0000fe0f"
- G_EMOJI = "\U0001f3f3\U0000fe0f\U0000200d\U0001f308"
- N_EMOJI = "\U0001f922"
- C_EMOJI = "\U0000271d\U0000fe0f"
- reactions = {
- r"mpreg|lupupa|\U0001fac3": "\U0001fac3", # mpreg
- r"(trans(gender)?($|\W+)|%s)" % T_EMOJI: T_EMOJI, # trans
- r"gay|%s" % G_EMOJI: G_EMOJI,
- r"(femboy|trans(gender)?($|\W+))": C_EMOJI,
- }
- if message.channel.permissions_for(message.guild.me).add_reactions:
- is_naus = random.randint(1, 100) == 32
- for key, value in reactions.items():
- if re.search(key, message.content, re.IGNORECASE):
- await message.add_reaction(value)
-
- if is_naus:
- await message.add_reaction(N_EMOJI)
-
@tasks.loop(minutes=10)
async def fetch_discord_atom_feed(self):
if not SPAM_CHANNEL:
diff --git a/cogs/extremism.py b/cogs/extremism.py
index 8ef3873..661c1d2 100644
--- a/cogs/extremism.py
+++ b/cogs/extremism.py
@@ -193,8 +193,6 @@ class Extremism(commands.Cog):
img_bio.seek(0)
decoration_bio.seek(0)
files = [
- # discord.File(img_bio, "avatar.png"),
- # discord.File(decoration_bio, "decoration.png"),
discord.File(img_bytes, filename="decorated." + ext)
]
await ctx.respond(files=files)
diff --git a/cogs/info.py b/cogs/info.py
index 5f2eed0..df12148 100644
--- a/cogs/info.py
+++ b/cogs/info.py
@@ -17,7 +17,7 @@ except ImportError:
class InfoCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
- self.client = httpx.AsyncClient(base_url="https://discord.com/api")
+ self.client = httpx.AsyncClient(base_url="https://discord.com/api/v10")
async def get_user_info(self, token: str):
try:
@@ -45,7 +45,7 @@ class InfoCog(commands.Cog):
@commands.slash_command()
@commands.cooldown(1, 10, commands.BucketType.user)
- async def me(self, ctx: discord.ApplicationCommand):
+ async def me(self, ctx: discord.ApplicationContext):
"""Displays oauth info about you"""
await ctx.defer()
diff --git a/cogs/mod.py b/cogs/mod.py
deleted file mode 100644
index 607e419..0000000
--- a/cogs/mod.py
+++ /dev/null
@@ -1,163 +0,0 @@
-from datetime import datetime
-from typing import Sized
-
-import discord
-from discord.ext import commands
-
-from utils import BannedStudentID, JimmyBans, Student, get_or_none, owner_or_admin
-
-
-class LimitedList(list):
- """FIFO Limited list"""
-
- def __init__(self, iterable: Sized = None, size: int = 5000):
- if iterable:
- assert len(iterable) <= size, "Initial iterable too big."
- super().__init__(iterable or [])
- self._max_size = size
-
- def append(self, __object) -> None:
- if len(self) + 1 >= self._max_size:
- self.pop(0)
- super().append(__object)
-
- def __add__(self, other):
- if len(other) > self._max_size:
- raise ValueError("Other is too large")
- elif len(other) == self._max_size:
- self.clear()
- super().__add__(other)
- else:
- for item in other:
- self.append(item)
-
-
-class Mod(commands.Cog):
- def __init__(self, bot):
- self.bot = bot
- self.cache = LimitedList()
-
- @commands.Cog.listener()
- async def on_message_delete(self, message: discord.Message):
- self.cache.append(message)
-
- @commands.user_command(name="Ban Account's B Number")
- @discord.default_permissions(ban_members=True)
- async def ban_student_id(self, ctx: discord.ApplicationContext, member: discord.Member):
- """Bans a student ID from registering. Also bans an account associated with it."""
- await ctx.defer(ephemeral=True)
- student_id = await get_or_none(Student, user_id=member.id)
- if student_id is None:
- return await ctx.respond("\N{cross mark} Unknown B number (is the user verified yet?)", ephemeral=True)
-
- ban = await get_or_none(BannedStudentID, student_id=student_id.id)
- if ban is None:
- ban = await BannedStudentID.objects.create(
- student_id=student_id.id,
- associated_account=member.id,
- )
-
- await member.ban(reason=f"Banned ID {ban.student_id} by {ctx.user}")
- return await ctx.respond(
- f"\N{white heavy check mark} Banned {ban.student_id} (and {member.mention})", ephemeral=True
- )
-
- @commands.slash_command(name="unban-student-number")
- @discord.default_permissions(ban_members=True)
- async def unban_student_id(self, ctx: discord.ApplicationContext, student_id: str):
- """Unbans a student ID and the account associated with it."""
- student_id = student_id.upper()
- ban = await get_or_none(BannedStudentID, student_id=student_id)
- if not ban:
- return await ctx.respond("\N{cross mark} That student ID isn't banned.")
- await ctx.defer()
- user_id = ban.associated_account
- await ban.delete()
- if not user_id:
- return await ctx.respond(f"\N{white heavy check mark} Unbanned {student_id}. No user to unban.")
- else:
- try:
- await ctx.guild.unban(discord.Object(user_id), reason=f"Unbanned by {ctx.user}")
- except discord.HTTPException as e:
- return await ctx.respond(
- f"\N{white heavy check mark} Unbanned {student_id}. Failed to unban {user_id} - HTTP {e.status}."
- )
- else:
- return await ctx.respond(f"\N{white heavy check mark} Unbanned {student_id}. Unbanned {user_id}.")
-
- @commands.slash_command(name="block")
- @owner_or_admin()
- async def block_user(self, ctx: discord.ApplicationContext, user: discord.Member, reason: str, until: str):
- """Blocks a user from using the bot."""
- await ctx.defer()
- date = datetime.utcnow()
- _time = until
- try:
- date, _time = until.split(" ")
- except ValueError:
- pass
- else:
- try:
- date = datetime.strptime(date, "%d/%m/%Y")
- except ValueError:
- return await ctx.respond("Invalid date format. Use `DD/MM/YYYY`.")
- try:
- hour, minute = map(int, _time.split(":"))
- except ValueError:
- return await ctx.respond("\N{cross mark} Invalid time format. Use HH:MM.")
- end = date.replace(hour=hour, minute=minute)
-
- # get an entry for the user's ID, and if it doesn't exist, create it. Otherwise, alert the user.
- entry = await get_or_none(JimmyBans, user_id=user.id)
- if entry is None:
- await JimmyBans.objects.create(user_id=user.id, reason=reason, until=end.timestamp())
- else:
- return await ctx.respond("\N{cross mark} That user is already blocked.")
- await ctx.respond(f"\N{white heavy check mark} Blocked {user.mention} until {discord.utils.format_dt(end)}.")
-
- @commands.slash_command(name="unblock")
- @owner_or_admin()
- async def unblock_user(self, ctx: discord.ApplicationContext, user: discord.Member):
- """Unblocks a user from using the bot."""
- await ctx.defer()
- entry = await get_or_none(JimmyBans, user_id=user.id)
- if entry is None:
- return await ctx.respond("\N{cross mark} That user isn't blocked.")
- await entry.delete()
- await ctx.respond(f"\N{white heavy check mark} Unblocked {user.mention}.")
-
- @commands.command()
- async def undelete(self, ctx: commands.Context, user: discord.User, *, query: str = None):
- """Searches through the message cache to see if there's any deleted messages."""
- for message in self.cache:
- message: discord.Message
- if message.author == user:
- query_ = query.lower() if query else None
- content_ = str(message.clean_content or "").lower()
- if query_ is not None and (query_ in content_ or content_ in query_):
- break
- else:
- return await ctx.reply("\N{cross mark} No matches in cache.")
-
- embeds = [
- discord.Embed(title="Message found!"),
- discord.Embed(
- description=message.content,
- colour=message.author.colour,
- timestamp=message.created_at,
- fields=[
- discord.EmbedField("Attachment count", str(len(message.attachments)), False),
- discord.EmbedField("Location", str(message.channel.mention)),
- discord.EmbedField(
- "Times",
- f"Created: {discord.utils.format_dt(message.created_at, 'R')} | Edited: "
- f"{'None' if message.edited_at is None else discord.utils.format_dt(message.edited_at, 'R')}",
- ),
- ],
- ).set_author(name=message.author.display_name, icon_url=message.author.display_avatar.url),
- ]
- await ctx.reply(embeds=embeds)
-
-
-def setup(bot):
- bot.add_cog(Mod(bot))
diff --git a/cogs/other.py b/cogs/other.py
index efb48d3..2ba1f4d 100644
--- a/cogs/other.py
+++ b/cogs/other.py
@@ -411,886 +411,32 @@ class OtherCog(commands.Cog):
view = self.XKCDGalleryView(number)
return await ctx.respond(embed=embed, view=view)
- corrupt_file = discord.SlashCommandGroup(
- name="corrupt-file",
- description="Corrupts files.",
- )
-
- @corrupt_file.command(name="generate")
- async def generate_corrupt_file(self, ctx: discord.ApplicationContext, file_name: str, size_in_megabytes: float):
- """Generates a "corrupted" file."""
- limit_mb = round(ctx.guild.filesize_limit / 1024 / 1024)
- if size_in_megabytes > limit_mb:
- return await ctx.respond(
- f"File size must be less than {limit_mb} MB.\n"
- "Want to corrupt larger files? see https://github.com/EEKIM10/cli-utils#installing-the-right-way"
- " (and then run `ruin `)."
- )
- await ctx.defer()
-
- size = max(min(int(size_in_megabytes * 1024 * 1024), ctx.guild.filesize_limit), 1)
-
- file = io.BytesIO()
- file.write(os.urandom(size - 1024))
- file.seek(0)
- return await ctx.respond(file=discord.File(file, file_name))
-
- @staticmethod
- def do_file_corruption(file: io.BytesIO, passes: int, bound_start: int, bound_end: int):
- for _ in range(passes):
- file.seek(random.randint(bound_start, bound_end))
- file.write(os.urandom(random.randint(128, 2048)))
- file.seek(0)
- return file
-
- @corrupt_file.command(name="ruin")
- async def ruin_corrupt_file(
- self,
- ctx: discord.ApplicationContext,
- file: discord.Attachment,
- passes: int = 10,
- metadata_safety_boundary: float = 5,
- ):
- """Takes a file and corrupts parts of it"""
- await ctx.defer()
- attachment = file
- if attachment.size > 8388608:
- return await ctx.respond(
- "File is too large. Max size 8mb.\n"
- "Want to corrupt larger files? see https://github.com/EEKIM10/cli-utils#installing-the-right-way"
- " (and then run `ruin `)."
- )
- bound_pct = attachment.size * (0.01 * metadata_safety_boundary)
- bound_start = round(bound_pct)
- bound_end = round(attachment.size - bound_pct)
- await ctx.respond("Downloading file...")
- file = io.BytesIO(await file.read())
- file.seek(0)
- await ctx.edit(content="Corrupting file...")
- file = await asyncio.to_thread(self.do_file_corruption, file, passes, bound_start, bound_end)
- file.seek(0)
- await ctx.edit(content="Uploading file...")
- await ctx.edit(content="Here's your corrupted file!", file=discord.File(file, attachment.filename))
-
@commands.command(name="kys", aliases=["kill"])
@commands.is_owner()
async def end_your_life(self, ctx: commands.Context):
await ctx.send(":( okay")
await self.bot.close()
- @commands.slash_command()
- async def ip(self, ctx: discord.ApplicationContext, detailed: bool = False, secure: bool = True):
- """Gets current IP"""
- if not await self.bot.is_owner(ctx.user):
- return await ctx.respond("Internal IP: 0.0.0.0\nExternal IP: 0.0.0.0")
-
- await ctx.defer(ephemeral=secure)
- ips = await self.get_interface_ip_addresses()
- root = Tree("IP Addresses")
- internal = root.add("Internal")
- external = root.add("External")
- interfaces = internal.add("Interfaces")
- for interface, addresses in ips.items():
- interface_tree = interfaces.add(interface)
- for address in addresses:
- colour = "green" if address["up"] else "red"
- ip_tree = interface_tree.add(
- f"[{colour}]" + address["ip"] + ((" (up)" if address["up"] else " (down)") if not detailed else "")
- )
- if detailed:
- ip_tree.add(f"IF Up: {'yes' if address['up'] else 'no'}")
- ip_tree.add(f"Netmask: {address['netmask']}")
- ip_tree.add(f"Broadcast: {address['broadcast']}")
-
- async with aiohttp.ClientSession() as session:
- try:
- async with session.get("https://api.ipify.org") as resp:
- external.add(await resp.text())
- except aiohttp.ClientError as e:
- external.add(f" [red]Error: {e}")
-
- with console.capture() as capture:
- console.print(root)
- text = capture.get()
- paginator = commands.Paginator(prefix="```", suffix="```")
- for line in text.splitlines():
- paginator.add_line(line)
- for page in paginator.pages:
- await ctx.respond(page, ephemeral=secure)
-
- @commands.slash_command()
- async def dig(
- self,
- ctx: discord.ApplicationContext,
- domain: str,
- _type: discord.Option(
- str,
- name="type",
- default="A",
- choices=[
- "A",
- "AAAA",
- "ANY",
- "AXFR",
- "CNAME",
- "HINFO",
- "LOC",
- "MX",
- "NS",
- "PTR",
- "SOA",
- "SRV",
- "TXT",
- ],
- ),
- ):
- """Looks up a domain name"""
- await ctx.defer()
- if re.search(r"\s+", domain):
- return await ctx.respond("Domain name cannot contain spaces.")
- try:
- response = await asyncresolver.resolve(
- domain,
- _type.upper(),
- )
- except Exception as e:
- return await ctx.respond(f"Error: {e}")
- res = response
- tree = Tree(f"DNS Lookup for {domain}")
- for record in res:
- record_tree = tree.add(f"{record.rdtype.name} Record")
- record_tree.add(f"Name: {res.name}")
- record_tree.add(f"Value: {record.to_text()}")
- with console.capture() as capture:
- console.print(tree)
- text = capture.get()
- paginator = commands.Paginator(prefix="```", suffix="```")
- for line in text.splitlines():
- paginator.add_line(line)
- paginator.add_line(empty=True)
- paginator.add_line(f"Exit code: {0}")
- paginator.add_line(f"DNS Server used: {res.nameserver}")
- for page in paginator.pages:
- await ctx.respond(page)
-
- @commands.slash_command()
- async def traceroute(
- self,
- ctx: discord.ApplicationContext,
- url: str,
- port: discord.Option(int, description="Port to use", default=None),
- ping_type: discord.Option(
- str,
- name="ping-type",
- description="Type of ping to use. See `traceroute --help`",
- choices=["icmp", "tcp", "udp", "udplite", "dccp", "default"],
- default="default",
- ),
- use_ip_version: discord.Option(
- str, name="ip-version", description="IP version to use.", choices=["ipv4", "ipv6"], default="ipv4"
- ),
- max_ttl: discord.Option(int, name="ttl", description="Max number of hops", default=30),
- ):
- """Performs a traceroute request."""
- await ctx.defer()
- if re.search(r"\s+", url):
- return await ctx.respond("URL cannot contain spaces.")
-
- args = ["sudo", "-E", "-n", "traceroute"]
- flags = {
- "ping_type": {
- "icmp": "-I",
- "tcp": "-T",
- "udp": "-U",
- "udplite": "-UL",
- "dccp": "-D",
- },
- "use_ip_version": {"ipv4": "-4", "ipv6": "-6"},
- }
-
- if ping_type != "default":
- args.append(flags["ping_type"][ping_type])
- else:
- args = args[3:] # removes sudo
- args.append(flags["use_ip_version"][use_ip_version])
- args.append("-m")
- args.append(str(max_ttl))
- if port is not None:
- args.append("-p")
- args.append(str(port))
- args.append(url)
- paginator = commands.Paginator()
- paginator.add_line(f"Running command: {' '.join(args[3 if args[0] == 'sudo' else 0:])}")
- paginator.add_line(empty=True)
- try:
- start = time_ns()
- process = await asyncio.create_subprocess_exec(
- args[0],
- *args[1:],
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- )
- await process.wait()
- stdout, stderr = await process.communicate()
- end = time_ns()
- time_taken_in_ms = (end - start) / 1000000
- if stdout:
- for line in stdout.splitlines():
- paginator.add_line(line.decode())
- if stderr:
- for line in stderr.splitlines():
- paginator.add_line(line.decode())
- paginator.add_line(empty=True)
- paginator.add_line(f"Exit code: {process.returncode}")
- paginator.add_line(f"Time taken: {time_taken_in_ms:,.1f}ms")
- except Exception as e:
- paginator.add_line(f"Error: {e}")
- for page in paginator.pages:
- await ctx.respond(page)
-
- @commands.slash_command()
- @commands.max_concurrency(1, commands.BucketType.user)
- @commands.cooldown(1, 30, commands.BucketType.user)
- async def screenshot(
- self,
- ctx: discord.ApplicationContext,
- url: str,
- browser: discord.Option(str, description="Browser to use", choices=["chrome", "firefox"], default="chrome"),
- render_timeout: discord.Option(int, name="render-timeout", description="Timeout for rendering", default=3),
- load_timeout: discord.Option(int, name="load-timeout", description="Timeout for page load", default=60),
- window_height: discord.Option(
- int, name="window-height", description="the height of the window in pixels", default=2560
- ),
- window_width: discord.Option(
- int, name="window-width", description="the width of the window in pixels", default=1440
- ),
- capture_whole_page: discord.Option(
- bool,
- name="capture-full-page",
- description="(firefox only) whether to capture the full page or just the viewport.",
- default=False,
- ),
- ):
- """Takes a screenshot of a URL"""
- if capture_whole_page and browser != "firefox":
- await ctx.respond(":warning: capture-full-page is only available with firefox; switching browser.")
- browser = "firefox"
- window_width = max(min(8640, window_width), 144)
- window_height = max(min(15360, window_height), 256)
- await ctx.defer()
- url = urlparse(url)
- if not url.scheme:
- if "/" in url.path:
- hostname, path = url.path.split("/", 1)
- else:
- hostname = url.path
- path = ""
- url = url._replace(scheme="http", netloc=hostname, path=path)
-
- friendly_url = textwrap.shorten(url.geturl(), 100)
-
- await ctx.edit(content=f"Preparing to screenshot <{friendly_url}>... (0%, checking filters)")
-
- async def blacklist_check() -> bool | str:
- async with aiofiles.open("./assets/domains.txt") as blacklist:
- for ln in await blacklist.readlines():
- if not ln.strip():
- continue
- if re.match(ln.strip(), url.netloc):
- return "Local blacklist"
- return True
-
- async def dns_check() -> Optional[bool | str]:
- try:
- # noinspection PyTypeChecker
- for response in await asyncio.to_thread(dns.resolver.resolve, url.hostname, "A"):
- if response.address in ["0.0.0.0", "::", "127.0.0.1", "::1"]:
- return "DNS blacklist"
- except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.LifetimeTimeout, AttributeError):
- return "Invalid domain or DNS error"
- return True
-
- done, pending = await asyncio.wait(
- [
- asyncio.create_task(blacklist_check(), name="local"),
- asyncio.create_task(dns_check(), name="dns"),
- ],
- return_when=asyncio.FIRST_COMPLETED,
- )
- done_tasks = done
- try:
- done = done_tasks.pop()
- except KeyError:
- return await ctx.respond("Something went wrong. Try again?\n")
- result = await done
- if not result:
- return await ctx.edit(
- content="That domain is blacklisted, doesn't exist, or there was no answer from the DNS server."
- f" ({result!r})"
- )
-
- await ctx.edit(content=f"Preparing to screenshot <{friendly_url}>... (16%, checking filters)")
- okay = await (pending or done_tasks).pop()
- if not okay:
- return await ctx.edit(
- content="That domain is blacklisted, doesn't exist, or there was no answer from the DNS server."
- f" ({okay!r})"
- )
-
- await ctx.edit(content=f"Screenshotting {textwrap.shorten(url.geturl(), 100)}... (33%, initializing browser)")
- try:
- async with self.lock:
- screenshot, driver, fetch_time, screenshot_time = await self.screenshot_website(
- ctx,
- url.geturl(),
- browser,
- render_timeout,
- load_timeout,
- window_height,
- window_width,
- capture_whole_page,
- )
- except TimeoutError:
- return await ctx.edit(content="Rendering screenshot timed out. Try using a smaller resolution.")
- except WebDriverException as e:
- paginator = commands.Paginator(prefix="```", suffix="```")
- paginator.add_line("WebDriver Error (did you pass extreme or invalid command options?)")
- paginator.add_line("Traceback:", empty=True)
- for line in e.msg.splitlines():
- paginator.add_line(line)
- for page in paginator.pages:
- await ctx.respond(page)
- except Exception as e:
- console.print_exception()
- return await ctx.edit(content=f"Failed: {e}", delete_after=30)
- else:
- await ctx.edit(content=f"Screenshotting <{friendly_url}>... (99%, uploading image)")
- file_size = len(screenshot.fp.read())
- screenshot.fp.seek(0)
- await ctx.edit(
- content="Here's your screenshot!\n"
- "Details:\n"
- f"* Browser: {driver}\n"
- f"* Resolution: {window_height}x{window_width} ({window_width*window_height:,} pixels, "
- f"{file_size / 1024 / 1024:,.1f}MB)\n"
- f"* URL: <{friendly_url}>\n"
- f"* Load time: {fetch_time:.2f}ms\n"
- f"* Screenshot render time: {screenshot_time:.2f}ms\n"
- f"* Total time: {(fetch_time + screenshot_time):.2f}ms",
- file=screenshot,
- )
-
- domains = discord.SlashCommandGroup("domains", "Commands for managing domains")
-
- @domains.command(name="add")
- async def add_domain(self, ctx: discord.ApplicationContext, domain: str):
- """Adds a domain to the blacklist"""
- await ctx.defer()
- if not await self.bot.is_owner(ctx.user):
- return await ctx.respond("You are not allowed to do that.")
- async with aiofiles.open("./assets/domains.txt", "a") as blacklist:
- await blacklist.write(domain.lower() + "\n")
- await ctx.respond("Added domain to blacklist.")
-
- @domains.command(name="remove")
- async def remove_domain(self, ctx: discord.ApplicationContext, domain: str):
- """Removes a domain from the blacklist"""
- await ctx.defer()
- if not await self.bot.is_owner(ctx.user):
- return await ctx.respond("You are not allowed to do that.")
- async with aiofiles.open("./assets/domains.txt") as blacklist:
- lines = await blacklist.readlines()
- async with aiofiles.open("./assets/domains.txt", "w") as blacklist:
- for line in lines:
- if line.strip() != domain.lower():
- await blacklist.write(line)
- await ctx.respond("Removed domain from blacklist.")
-
@staticmethod
async def check_proxy(url: str = "socks5://localhost:1090", *, timeout: float = 3.0):
- client = httpx.AsyncClient(http2=True, timeout=timeout)
- my_ip4 = (await client.get("https://api.ipify.org")).text
- real_ips = [my_ip4]
- await client.aclose()
+ async with httpx.AsyncClient(http2=True, timeout=timeout) as client:
+ my_ip4 = (await client.get("https://api.ipify.org")).text
+ real_ips = [my_ip4]
# Check the proxy
- client = httpx.AsyncClient(http2=True, proxies=url, timeout=timeout)
- try:
- response = await client.get(
- "https://1.1.1.1/cdn-cgi/trace",
- )
- response.raise_for_status()
- for line in response.text.splitlines():
- if line.startswith("ip"):
- if any(x in line for x in real_ips):
- return 1
- except (httpx.TransportError, httpx.HTTPStatusError):
- return 2
- await client.aclose()
- return 0
-
- @commands.slash_command(name="yt-dl")
- @commands.max_concurrency(1, commands.BucketType.user)
- async def yt_dl_2(
- self,
- ctx: discord.ApplicationContext,
- url: discord.Option(description="The URL to download.", type=str),
- _format: discord.Option(
- name="format", description="The format to download.", type=str, autocomplete=format_autocomplete, default=""
- ) = "",
- extract_audio: bool = False,
- cookies_txt: discord.Attachment = None,
- disable_filesize_buffer: bool = False,
- ):
- """Downloads a video using youtube-dl"""
- cookies = io.StringIO()
- cookies.seek(0)
-
- await ctx.defer()
- from urllib.parse import parse_qs
-
- MAX_SIZE_MB = 20
- REAL_MAX_SIZE_MB = 25
- if disable_filesize_buffer is False:
- MAX_SIZE_MB *= 0.8
- BYTES_REMAINING = (MAX_SIZE_MB - 0.256) * 1024 * 1024
- import yt_dlp
-
- with tempfile.TemporaryDirectory(prefix="jimmy-ytdl-") as tempdir_str:
- tempdir = Path(tempdir_str).resolve()
- stdout = tempdir / "stdout.txt"
- stderr = tempdir / "stderr.txt"
-
- default_cookies_txt = Path.cwd() / "jimmy-cookies.txt"
- real_cookies_txt = tempdir / "cookies.txt"
- if cookies_txt is not None:
- await cookies_txt.save(fp=real_cookies_txt)
- else:
- default_cookies_txt.touch()
- shutil.copy(default_cookies_txt, real_cookies_txt)
-
- class Logger:
- def __init__(self):
- self.stdout = open(stdout, "w+")
- self.stderr = open(stderr, "w+")
-
- def __del__(self):
- self.stdout.close()
- self.stderr.close()
-
- def debug(self, msg: str):
- if msg.startswith("[debug]"):
- return
- self.info(msg)
-
- def info(self, msg: str):
- self.stdout.write(msg + "\n")
- self.stdout.flush()
-
- def warning(self, msg: str):
- self.stderr.write(msg + "\n")
- self.stderr.flush()
-
- def error(self, msg: str):
- self.stderr.write(msg + "\n")
- self.stderr.flush()
-
- logger = Logger()
- paths = {
- target: str(tempdir)
- for target in (
- "home",
- "temp",
- )
- }
-
- 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,
- "format_sort": [
- "vcodec:h264",
- "acodec:aac",
- "vcodec:vp9",
- "acodec:opus",
- "acodec:vorbis",
- "vcodec:vp8",
- "ext",
- ],
- "merge_output_format": "webm/mp4/mov/flv/avi/ogg/m4a/wav/mp3/opus/mka/mkv",
- "source_address": "0.0.0.0",
- "cookiefile": str(real_cookies_txt.resolve().absolute()),
- "concurrent_fragment_downloads": 4,
- }
- description = ""
- proxy_url = "socks5://localhost:1090"
+ async with httpx.AsyncClient(http2=True, proxies=url, timeout=timeout) as client:
try:
- proxy_down = await asyncio.wait_for(self.check_proxy("socks5://localhost:1090"), timeout=10)
- if proxy_down > 0:
- if proxy_down == 1:
- description += ":warning: (SHRoNK) Proxy check leaked IP - trying backup proxy.\n"
- elif proxy_down == 2:
- description += ":warning: (SHRoNK) Proxy connection failed - trying backup proxy.\n"
- else:
- description += ":warning: (SHRoNK) Unknown proxy error - trying backup proxy.\n"
-
- proxy_down = await asyncio.wait_for(self.check_proxy("socks5://localhost:1080"), timeout=10)
- if proxy_down > 0:
- if proxy_down == 1:
- description += ":warning: (NexBox) Proxy check leaked IP..\n"
- elif proxy_down == 2:
- description += ":warning: (NexBox) Proxy connection failed.\n"
- else:
- description += ":warning: (NexBox) Unknown proxy error.\n"
- proxy_url = None
- else:
- proxy_url = "socks5://localhost:1080"
- description += "\N{white heavy check mark} Using fallback NexBox proxy."
- else:
- description += "\N{white heavy check mark} Using the SHRoNK proxy."
- except Exception as e:
- traceback.print_exc()
- description += f":warning: Failed to check proxy (`{e}`). Going unproxied."
- if proxy_url:
- args["proxy"] = proxy_url
- if extract_audio:
- args["postprocessors"] = [
- {"key": "FFmpegExtractAudio", "preferredquality": "96", "preferredcodec": "best"}
- ]
- args["format"] = args["format"] or f"(ba/b)[filesize<={MAX_SIZE_MB}M]/ba/b"
-
- if args["format"] is None:
- args["format"] = f"(bv+ba/b)[vcodec!=h265][vcodec!=av01][filesize<={MAX_SIZE_MB}M]/b"
-
- with yt_dlp.YoutubeDL(args) as downloader:
- try:
- extracted_info = await asyncio.to_thread(downloader.extract_info, url, download=False)
- except yt_dlp.utils.DownloadError:
- title = "error"
- thumbnail_url = webpage_url = discord.Embed.Empty
- else:
- title = extracted_info.get("title", url)
- title = textwrap.shorten(title, 100)
- thumbnail_url = extracted_info.get("thumbnail") or discord.Embed.Empty
- webpage_url = extracted_info.get("webpage_url") or discord.Embed.Empty
-
- chosen_format = extracted_info.get("format")
- chosen_format_id = extracted_info.get("format_id")
- final_extension = extracted_info.get("ext")
- format_note = extracted_info.get("format_note", "%s (%s)" % (chosen_format, chosen_format_id))
- resolution = extracted_info.get("resolution")
- fps = extracted_info.get("fps")
- vcodec = extracted_info.get("vcodec")
- acodec = extracted_info.get("acodec")
-
- lines = []
- if chosen_format and chosen_format_id:
- lines.append(
- "* Chosen format: `%s` (`%s`)" % (chosen_format, chosen_format_id),
- )
- if format_note:
- lines.append("* Format note: %r" % format_note)
- if final_extension:
- lines.append("* File extension: " + final_extension)
- if resolution:
- _s = resolution
- if fps:
- _s += " @ %s FPS" % fps
- lines.append("* Resolution: " + _s)
- if vcodec or acodec:
- lines.append("%s+%s" % (vcodec or "N/A", acodec or "N/A"))
-
- if lines:
- description += "\n"
- description += "\n".join(lines)
-
- try:
- embed = discord.Embed(
- title="Downloading %r..." % title,
- description=description,
- colour=discord.Colour.blurple(),
- url=webpage_url,
- )
- embed.set_thumbnail(url=thumbnail_url)
- await ctx.respond(embed=embed)
- await asyncio.to_thread(partial(downloader.download, [url]))
- except yt_dlp.utils.DownloadError as e:
- traceback.print_exc()
- return await ctx.edit(
- embed=discord.Embed(
- title="Error",
- description=f"Download failed:\n```\n{e}\n```",
- colour=discord.Colour.red(),
- url=webpage_url,
- ),
- delete_after=60,
- )
- else:
- parsed_qs = parse_qs(url)
- if "t" in parsed_qs and parsed_qs["t"] and parsed_qs["t"][0].isdigit():
- # Assume is timestamp
- timestamp = round(float(parsed_qs["t"][0]))
- end_timestamp = None
- if len(parsed_qs["t"]) >= 2:
- end_timestamp = round(float(parsed_qs["t"][1]))
- if end_timestamp < timestamp:
- end_timestamp, timestamp = reversed((end_timestamp, timestamp))
- _end = "to %s" % end_timestamp if len(parsed_qs["t"]) == 2 else "onward"
- embed = discord.Embed(
- title="Trimming...",
- description=f"Trimming from {timestamp} seconds {_end}.\nThis may take a while.",
- colour=discord.Colour.blurple(),
- )
- await ctx.edit(embed=embed)
- for file in tempdir.glob("%s-*" % ctx.user.id):
- try:
- bak = file.with_name(file.name + "-" + os.urandom(4).hex())
- shutil.copy(str(file), str(bak))
- minutes, seconds = divmod(timestamp, 60)
- hours, minutes = divmod(minutes, 60)
- _args = [
- "ffmpeg",
- "-i",
- str(bak),
- "-ss",
- "{!s}:{!s}:{!s}".format(*map(round, (hours, minutes, seconds))),
- "-y",
- "-c",
- "copy",
- str(file),
- ]
- if end_timestamp is not None:
- minutes, seconds = divmod(end_timestamp, 60)
- hours, minutes = divmod(minutes, 60)
- _args.insert(5, "-to")
- _args.insert(6, "{!s}:{!s}:{!s}".format(*map(round, (hours, minutes, seconds))))
-
- await self.bot.loop.run_in_executor(
- None, partial(subprocess.run, _args, check=True, capture_output=True)
- )
- bak.unlink(True)
- except subprocess.CalledProcessError as e:
- traceback.print_exc()
- return await ctx.edit(
- embed=discord.Embed(
- title="Error",
- description=f"Trimming failed:\n```\n{e}\n```",
- colour=discord.Colour.red(),
- ),
- delete_after=30,
- )
-
- embed = discord.Embed(
- title="Downloaded %r!" % title, description="", colour=discord.Colour.green(), url=webpage_url
- )
- embed.set_thumbnail(url=thumbnail_url)
- del logger
- files = []
-
- 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 >= REAL_MAX_SIZE_MB:
- 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],
- REAL_MAX_SIZE_MB,
- )
- )
- continue
- else:
- files.append(discord.File(file, file.name))
- BYTES_REMAINING -= st
-
- if not files:
- embed.description += "No files to upload. Directory list:\n%s" % (
- "\n".join(r"\* " + f.name for f in tempdir.iterdir())
- )
- return await ctx.edit(embed=embed)
- else:
- _desc = embed.description
- embed.description += f"Uploading {len(files)} file(s):\n%s" % (
- "\n".join("* `%s`" % f.filename for f in files)
- )
- 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(
- self,
- ctx: discord.ApplicationContext,
- speed: discord.Option(int, "The speed of the voice. Default is 150.", required=False, default=150),
- voice: discord.Option(
- str,
- "The voice to use. Some may cause timeout.",
- autocomplete=discord.utils.basic_autocomplete(VOICES),
- default="default",
- ),
- ):
- """Converts text to MP3. 5 uses per 10 minutes."""
- if voice not in VOICES:
- return await ctx.respond("Invalid voice.")
- speed = min(300, max(50, speed))
- _self = self
- _bot = self.bot
-
- class TextModal(discord.ui.Modal):
- def __init__(self):
- super().__init__(
- discord.ui.InputText(
- label="Text",
- placeholder="Enter text to read",
- min_length=1,
- max_length=4000,
- style=discord.InputTextStyle.long,
- ),
- title="Convert text to an MP3",
+ response = await client.get(
+ "https://1.1.1.1/cdn-cgi/trace",
)
-
- 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)
- engine = pyttsx3.init()
- engine.setProperty("voice", voice)
- engine.setProperty("rate", speed)
- _io = BytesIO()
- engine.save_to_file(text, target_fn)
- engine.runAndWait()
- last_3_sizes = [-3, -2, -1]
- no_exists = 0
-
- def should_loop():
- if not os.path.exists(target_fn):
- nonlocal no_exists
- assert no_exists < 300, "File does not exist for 5 minutes."
- no_exists += 1
- return True
-
- stat = os.stat(target_fn)
- for _result in last_3_sizes:
- if stat.st_size != _result:
- return True
-
- return False
-
- while should_loop():
- if os.path.exists(target_fn):
- last_3_sizes.pop(0)
- last_3_sizes.append(os.stat(target_fn).st_size)
- sleep(1)
-
- with open(target_fn, "rb") as f:
- x = f.read()
- _io.write(x)
- os.remove(target_fn)
- _io.seek(0)
- return _io, len(x)
-
- await interaction.response.defer()
- text_pre = self.children[0].value
- if text_pre.startswith("url:"):
- _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"}, follow_redirects=True
- )
- if response.status_code != 200:
- await _msg.edit(content=f"Failed to download text. Status code: {response.status_code}")
- return
-
- ct = response.headers.get("Content-Type", "application/octet-stream")
- if not ct.startswith("text/plain"):
- await _msg.edit(content=f"Failed to download text. Content-Type is {ct!r}, not text/plain")
- return
- text_pre = response.text
- except (ConnectionError, httpx.HTTPError, httpx.NetworkError) as e:
- await _msg.edit(content="Failed to download text. " + str(e))
- return
-
- else:
- _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 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 "
- f"{ctx.guild.filesize_limit / 1024 / 1024}Mb)"
- )
- return
- fn = ""
- _words = text_pre.split()
- while len(fn) < 28:
- try:
- word = _words.pop(0)
- except IndexError:
- break
- if len(fn) + len(word) + 1 > 28:
- continue
- fn += word + "-"
- fn = fn[:-1]
- fn = fn[:28]
- await _msg.edit(content="Here's your MP3!", file=discord.File(mp3, filename=fn + ".mp3"))
-
- await ctx.send_modal(TextModal())
+ response.raise_for_status()
+ for line in response.text.splitlines():
+ if line.startswith("ip"):
+ if any(x in line for x in real_ips):
+ return 1
+ except (httpx.TransportError, httpx.HTTPStatusError):
+ return 2
+ return 0
@commands.slash_command()
@commands.cooldown(5, 10, commands.BucketType.user)
@@ -1585,699 +731,6 @@ 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),
- bit_rate - end_br,
- )
- 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)
- )
-
- class OllamaKillSwitchView(discord.ui.View):
- def __init__(self, ctx: discord.ApplicationContext, msg: discord.Message):
- super().__init__(timeout=None)
- self.ctx = ctx
- self.msg = msg
-
- async def interaction_check(self, interaction: discord.Interaction) -> bool:
- return interaction.user == self.ctx.author and interaction.channel == self.ctx.channel
-
- @discord.ui.button(
- label="Abort",
- style=discord.ButtonStyle.red,
- emoji="\N{wastebasket}",
- )
- async def abort_button(self, _, interaction: discord.Interaction):
- await interaction.response.defer()
- if self.msg in self.ctx.command.cog.ollama_locks:
- self.ctx.command.cog.ollama_locks[self.msg].set()
- self.disable_all_items()
- await interaction.edit_original_response(view=self)
- self.stop()
-
- @commands.slash_command()
- @commands.max_concurrency(3, commands.BucketType.user, wait=False)
- async def ollama(
- self,
- ctx: discord.ApplicationContext,
- model: str = "orca-mini",
- query: str = None,
- context: str = None,
- server: str = "auto",
- ):
- """:3"""
- with open("./assets/ollama-prompt.txt") as file:
- system_prompt = file.read().replace("\n", " ").strip()
- if query is None:
-
- class InputPrompt(discord.ui.Modal):
- def __init__(self, is_owner: bool):
- super().__init__(
- discord.ui.InputText(
- label="User Prompt",
- placeholder="Enter prompt",
- min_length=1,
- max_length=4000,
- style=discord.InputTextStyle.long,
- ),
- title="Enter prompt",
- timeout=120,
- )
- if is_owner:
- self.add_item(
- discord.ui.InputText(
- label="System Prompt",
- placeholder="Enter prompt",
- min_length=1,
- max_length=4000,
- style=discord.InputTextStyle.long,
- value=system_prompt,
- )
- )
-
- self.user_prompt = None
- self.system_prompt = system_prompt
-
- async def callback(self, interaction: discord.Interaction):
- self.user_prompt = self.children[0].value
- if len(self.children) > 1:
- self.system_prompt = self.children[1].value
- await interaction.response.defer()
- self.stop()
-
- modal = InputPrompt(await self.bot.is_owner(ctx.author))
- await ctx.send_modal(modal)
- await modal.wait()
- query = modal.user_prompt
- if not modal.user_prompt:
- return
- system_prompt = modal.system_prompt or system_prompt
- else:
- await ctx.defer()
-
- if context:
- if context not in self.context_cache:
- return await ctx.respond(":x: Context not found in cache.")
- context = self.context_cache[context]
-
- model = model.casefold()
- try:
- model, tag = model.split(":", 1)
- model = model + ":" + tag
- self.log.debug("Model %r already has a tag")
- except ValueError:
- model = model + ":latest"
- self.log.debug("Resolved model to %r" % model)
-
- servers: dict[str, dict[str, str, list[str] | int]] = {
- "100.106.34.86:11434": {
- "name": "NexTop",
- "allow": [
- "orca-mini",
- "orca-mini:latest",
- "orca-mini:3b",
- "orca-mini:7b",
- "llama2:latest",
- "llama2-uncensored:latest",
- "llama2-uncensored",
- "codellama:latest",
- "codellama:python",
- "codellama:instruct",
- ],
- "owner": 421698654189912064,
- },
- "ollama.shronk.net:11434": {
- "name": "Alibaba Cloud",
- "allow": ["*"],
- "owner": 1019233057519177778,
- },
- "100.66.187.46:11434": {
- "name": "NexBox",
- "allow": [
- "orca-mini",
- "orca-mini:latest",
- "orca-mini:3b",
- "orca-mini:7b",
- ],
- "owner": 421698654189912064,
- },
- }
- H_DEFAULT = {"name": "Other", "allow": ["*"], "owner": 1019217990111199243}
-
- def model_is_allowed(model_name: str, _srv: dict[str, str | list[str] | int]) -> bool:
- if _srv["owner"] == ctx.user.id:
- return True
- for pat in _srv.get("allow", ["*"]):
- if not fnmatch.fnmatch(model_name.lower(), pat.lower()):
- self.log.debug(
- "Server %r does not support %r (only %r.)"
- % (_srv["name"], model_name, ", ".join(_srv["allow"]))
- )
- else:
- break
- else:
- return False
- return True
-
- class ServerSelector(discord.ui.View):
- def __init__(self):
- super().__init__(disable_on_timeout=True)
- self.chosen_server = None
-
- async def interaction_check(self, interaction: Interaction) -> bool:
- return interaction.user == ctx.user
-
- @discord.ui.select(
- placeholder="Choose a server.",
- custom_id="select",
- options=[
- discord.SelectOption(label="%s (%s)" % (y["name"], x), value=x)
- for x, y in servers.items()
- if model_is_allowed(model, y)
- ]
- + [discord.SelectOption(label="Custom", value="custom")],
- )
- async def select_callback(self, item: discord.ui.Select, interaction: discord.Interaction):
- if item.values[0] == "custom":
-
- class ServerSelectionModal(discord.ui.Modal):
- def __init__(self):
- super().__init__(
- discord.ui.InputText(
- label="Domain/IP",
- placeholder="Enter server",
- min_length=1,
- max_length=4000,
- style=discord.InputTextStyle.short,
- ),
- discord.ui.InputText(
- label="Port (only 443 is HTTPS)",
- placeholder="11434",
- min_length=2,
- max_length=5,
- style=discord.InputTextStyle.short,
- value="11434",
- ),
- title="Enter server details",
- timeout=120,
- )
- self.hostname = None
- self.port = None
-
- async def callback(self, _interaction: discord.Interaction):
- await _interaction.response.defer()
- self.stop()
-
- _modal = ServerSelectionModal()
- await interaction.response.send_modal(_modal)
- await _modal.wait()
- if not all((_modal.hostname, _modal.port)):
- return
- self.chosen_server = f"{_modal.hostname}:{_modal.port}"
- else:
- self.chosen_server = item.values[0]
- await interaction.response.defer(ephemeral=True)
- await interaction.followup.send(
- f"\N{white heavy check mark} Selected server {self.chosen_server}/", ephemeral=True
- )
- self.stop()
-
- if server == "auto":
- selector = ServerSelector()
- selector_message = await ctx.respond("Select a server:", view=selector)
- await selector.wait()
- if not selector.chosen_server:
- return
- host = selector.chosen_server
- await selector_message.delete(delay=1)
- else:
- host = server
- srv = servers.get(host, H_DEFAULT)
- if not model_is_allowed(model, srv):
- return await ctx.respond(
- ":x: <@{!s}> does not allow you to run that model on the server {!r}. You can, however, use"
- " any of the following: {}".format(srv["owner"], srv["name"], ", ".join(srv.get("allow", ["*"])))
- )
-
- content = None
- embed = discord.Embed(colour=discord.Colour.greyple())
- embed.set_author(
- name=f"Loading {model}",
- url=f"http://{host}",
- icon_url="https://cdn.discordapp.com/emojis/1101463077586735174.gif",
- )
- FOOTER_TEXT = "Powered by Ollama • Using server {} ({})".format(host, servers.get(host, H_DEFAULT)["name"])
- embed.set_footer(text=FOOTER_TEXT)
-
- msg = await ctx.respond(embed=embed, ephemeral=False)
- async with httpx.AsyncClient(follow_redirects=True) as client:
- try:
- await client.get(f"https://{host}/api/tags")
- client.base_url = f"https://{host}/api"
- except (httpx.ConnectError, Exception):
- client.base_url = f"http://{host}/api"
-
- try:
- response = await client.get("/tags")
- response.raise_for_status()
- except httpx.HTTPStatusError as e:
- error = "GET {0.url} HTTP {0.status_code}: {0.text}".format(e.response)
- return await msg.edit(
- embed=discord.Embed(
- title="Failed to GET /tags. Offline?", description=error, colour=discord.Colour.red()
- ).set_footer(text=FOOTER_TEXT)
- )
- except httpx.TransportError as e:
- return await msg.edit(
- embed=discord.Embed(
- title=f"Failed to connect to {host!r}",
- description="Transport error sending request to {}: {}".format(host, str(e)),
- colour=discord.Colour.red(),
- ).set_footer(text=FOOTER_TEXT)
- )
- # get models
- try:
- response = await client.post("/show", json={"name": model})
- except httpx.TransportError as e:
- embed = discord.Embed(
- title="Failed to connect to Ollama.", description=str(e), colour=discord.Colour.red()
- )
- embed.set_footer(text=FOOTER_TEXT)
- return await msg.edit(embed=embed)
- if response.status_code == 404:
- embed.title = f"Downloading {model}"
- await msg.edit(embed=embed)
- async with ctx.channel.typing():
- async with client.stream(
- "POST", "/pull", json={"name": model, "stream": True}, timeout=None
- ) as response:
- if response.status_code != 200:
- error = await response.aread()
- embed = discord.Embed(
- title=f"Failed to download model {model}:",
- description=f"HTTP {response.status_code}:\n```{error or ''}\n```",
- colour=discord.Colour.red(),
- url=str(response.url),
- )
- embed.set_footer(text=FOOTER_TEXT)
- return await msg.edit(embed=embed)
- lines: dict[str, str] = {}
- last_edit = time()
- async for chunk in ollama_stream_reader(response):
- if "total" in chunk and "completed" in chunk:
- completed = chunk["completed"] or 1 # avoid division by zero
- total = chunk["total"] or 1
- percent = round(completed / total * 100)
- if percent == 100 and completed != total:
- percent = round(completed / total * 100, 2)
- total_gigabytes = total / 1024 / 1024 / 1024
- completed_gigabytes = completed / 1024 / 1024 / 1024
- lines[chunk["status"]] = (
- f"{percent}% " f"({completed_gigabytes:.2f}GB/{total_gigabytes:.2f}GB)"
- )
- else:
- status = chunk.get("status", chunk.get("error", os.urandom(3).hex()))
- lines[status] = status
-
- embed.description = "\n".join(f"`{k}`: {v}" for k, v in lines.items())
- if (time() - last_edit) >= 5:
- await msg.edit(embed=embed)
- embed.title = f"Downloaded {model}!"
- embed.colour = discord.Colour.green()
- await msg.edit(embed=embed)
- while (await client.post("/show", json={"name": model})).status_code != 200:
- await asyncio.sleep(5)
- elif response.status_code != 200:
- error = await response.aread()
- embed = discord.Embed(
- title=f"Failed to download model {model}:",
- description=f"HTTP {response.status_code}:\n```{error or ''}\n```",
- colour=discord.Colour.red(),
- url=str(response.url),
- )
- embed.set_footer(text=FOOTER_TEXT)
- return await msg.edit(embed=embed)
-
- embed = discord.Embed(
- title=f"{model} says:",
- description="",
- colour=discord.Colour.blurple(),
- timestamp=discord.utils.utcnow(),
- )
- embed.set_footer(text=FOOTER_TEXT)
- await msg.edit(embed=embed)
- async with ctx.channel.typing():
- payload = {"model": model, "prompt": query, "format": "json", "system": system_prompt, "stream": True}
- if context:
- payload["context"] = context
- async with client.stream("POST", "/generate", json=payload, timeout=None) as response:
- if response.status_code != 200:
- error = await response.aread()
- embed = discord.Embed(
- title=f"Failed to generate response from {model}:",
- description=f"HTTP {response.status_code}:\n```{error or ''}\n```",
- colour=discord.Colour.red(),
- )
- embed.set_footer(text=FOOTER_TEXT)
- return await msg.edit(embed=embed)
- self.ollama_locks[msg] = asyncio.Event()
- view = self.OllamaKillSwitchView(ctx, msg)
- await msg.edit(view=view)
- last_edit = time()
- async for chunk in ollama_stream_reader(response):
- if "done" not in chunk.keys() or "response" not in chunk.keys():
- continue
- else:
- if chunk["done"] is True:
- content = None
- embed.remove_author()
- else:
- embed.set_author(
- name=f"Generating response with {model}",
- url=f"http://{host}",
- icon_url="https://cdn.discordapp.com/emojis/1101463077586735174.gif",
- )
- embed.description += chunk["response"]
- if (time() - last_edit) >= 5 or chunk["done"] is True:
- await msg.edit(content=content, embed=embed, view=view)
- last_edit = time()
- if self.ollama_locks[msg].is_set():
- embed.title = embed.title[:-1] + " (Aborted)"
- embed.colour = discord.Colour.red()
- return await msg.edit(embed=embed, view=None)
- if len(embed.description) >= 4000:
- embed.add_field(name="Aborting early", value="Output exceeded 4000 characters.")
- embed.title = embed.title[:-1] + " (Aborted)"
- embed.colour = discord.Colour.red()
- embed.description = embed.description[:4096]
- break
- else:
- embed.colour = discord.Colour.green()
- embed.remove_author()
-
- def get_time_spent(nanoseconds: int) -> str:
- hours, minutes, seconds = 0, 0, 0
- seconds = nanoseconds / 1e9
- if seconds >= 60:
- minutes, seconds = divmod(seconds, 60)
- if minutes >= 60:
- hours, minutes = divmod(minutes, 60)
-
- result = []
- if seconds:
- if seconds != 1:
- label = "seconds"
- else:
- label = "second"
- result.append(f"{round(seconds)} {label}")
- if minutes:
- if minutes != 1:
- label = "minutes"
- else:
- label = "minute"
- result.append(f"{round(minutes)} {label}")
- if hours:
- if hours != 1:
- label = "hours"
- else:
- label = "hour"
- result.append(f"{round(hours)} {label}")
- return ", ".join(reversed(result))
-
- total_time_spent = get_time_spent(chunk.get("total_duration", 999999999.0))
- eval_time_spent = get_time_spent(chunk.get("eval_duration", 999999999.0))
- load_time_spent = get_time_spent(chunk.get("load_duration", 999999999.0))
- sample_time_sent = get_time_spent(chunk.get("sample_duration", 999999999.0))
- prompt_eval_time_spent = get_time_spent(chunk.get("prompt_eval_duration", 999999999.0))
- context: Optional[list[int]] = chunk.get("context")
- # noinspection PyTypeChecker
- if context:
- key = os.urandom(8).hex()
- self.context_cache[key] = context
- else:
- context = key = None
- value = (
- "* Total: {}\n"
- "* Model load: {}\n"
- "* Sample generation: {}\n"
- "* Prompt eval: {}\n"
- "* Response generation: {}\n"
- ).format(
- total_time_spent,
- load_time_spent,
- sample_time_sent,
- prompt_eval_time_spent,
- eval_time_spent,
- )
- embed.add_field(name="Timings", value=value, inline=False)
- if context:
- embed.add_field(name="Context Key", value=key, inline=True)
- embed.set_footer(text=FOOTER_TEXT)
- await msg.edit(content=None, embed=embed, view=None)
- self.ollama_locks.pop(msg, None)
-
- @commands.slash_command(name="test-proxies")
- @commands.max_concurrency(1, commands.BucketType.user, wait=False)
- async def test_proxy(
- self,
- ctx: discord.ApplicationContext,
- run_speed_test: bool = False,
- proxy_name: discord.Option(str, choices=["SHRoNK", "NexBox", "first-working"]) = "first-working",
- test_time: float = 30,
- ):
- """Tests proxies."""
- test_time = max(5.0, test_time)
- await ctx.defer()
- results = {
- "localhost:1090": {
- "name": "SHRoNK",
- "failure": None,
- "download_speed": 0.0,
- "tested": False,
- "speedtest": [
- "https://geo.mirror.pkgbuild.com/iso/latest/archlinux-x86_64.iso",
- "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-12.4.0-amd64-netinst.iso",
- "https://mirrors.layeronline.com/linuxmint/stable/21.2/linuxmint-21.2-cinnamon-64bit.iso",
- "https://cdimage.ubuntu.com/kubuntu/releases/jammy/release/kubuntu-22.04.3-desktop-amd64.iso",
- "https://releases.ubuntu.com/22.04.3/ubuntu-22.04.3-desktop-amd64.iso"
- ],
- },
- "localhost:1080": {
- "name": "NexBox",
- "failure": None,
- "download_speed": 0.0,
- "tested": False,
- "speedtest": ["http://192.168.0.90:82/100M.bin"],
- },
- }
- if proxy_name != "first-working":
- for key, value in results.copy().items():
- if value["name"].lower() == proxy_name.lower():
- continue
- else:
- results.pop(key)
- embed = discord.Embed(title="\N{white heavy check mark} Proxy available.")
- failed = False
- proxy_uri = None
- for proxy_uri in results.keys():
- name = results[proxy_uri]["name"]
- try:
- proxy_down = await asyncio.wait_for(self.check_proxy("socks5://" + proxy_uri), timeout=10)
- results[proxy_uri]["tested"] = True
- if proxy_down > 0:
- embed.colour = discord.Colour.red()
- if proxy_down == 1:
- results[proxy_uri]["failure"] = f"{name} Proxy check leaked IP."
- elif proxy_down == 2:
- results[proxy_uri]["failure"] = "Proxy connection failed."
- else:
- results[proxy_uri]["failure"] = "Unknown proxy error."
- else:
- embed.colour = discord.Colour.green()
- break
- except asyncio.TimeoutError as e:
- results[proxy_uri]["failure"] = f"Proxy connection timed out, likely offline. {e}"
- results[proxy_uri]["tested"] = True
- except Exception as e:
- traceback.print_exc()
- embed.colour = discord.Colour.red()
- results[proxy_uri]["failure"] = f"Failed to check {name} proxy (`{e}`)."
- results[proxy_uri]["tested"] = True
- else:
- embed = discord.Embed(title="\N{cross mark} All proxies failed.", colour=discord.Colour.red())
- failed = True
-
- for uri, value in results.items():
- if value["tested"]:
- embed.add_field(name=value["name"], value=value["failure"] or "Proxy is working.", inline=True)
- embed.set_footer(text="No speed test will be run.")
- await ctx.respond(embed=embed)
- if run_speed_test and failed is False:
- now = discord.utils.utcnow()
- embed.set_footer(text="Started speedtest at " + now.strftime("%X"))
- await ctx.edit(embed=embed)
- chosen_proxy = ("socks5://" + proxy_uri) if proxy_uri else None
- async with httpx.AsyncClient(
- http2=True,
- proxies=chosen_proxy,
- headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0"},
- ) as client:
- bytes_received = 0
- try:
- start = time()
- # used = results[proxy_uri]["speedtest"].format(hetzner_region=region)
- used = random.choice(results[proxy_uri]["speedtest"])
- latency_start = time()
- async with client.stream("GET", used) as response:
- latency_end = time()
- async for chunk in response.aiter_bytes():
- bytes_received += len(chunk)
- if (time() - start) > test_time:
- break
- latency = (latency_end - latency_start) * 1000
- response.raise_for_status()
- end = time()
- now = discord.utils.utcnow()
- embed.set_footer(text=embed.footer.text + " | Finished at: " + now.strftime("%X"))
- except Exception as e:
- results[proxy_uri]["failure"] = f"Failed to test speed (`{e}`)."
- megabytes = bytes_received / 1024 / 1024
- elapsed = end - start
- bits_per_second = (bytes_received * 8) / elapsed
- megabits_per_second = bits_per_second / 1024 / 1024
- embed2 = discord.Embed(
- title=f"\U000023f2\U0000fe0f Speed test results (for {proxy_uri})",
- description=f"Downloaded {megabytes:,.1f}MB in {elapsed:,.0f} seconds "
- f"({megabits_per_second:,.0f}Mbps).\n`{latency:,.0f}ms` latency.",
- colour=discord.Colour.green() if megabits_per_second >= 50 else discord.Colour.red(),
- )
- embed2.add_field(name="Source", value=used)
- await ctx.edit(embeds=[embed, embed2])
-
@commands.message_command(name="Transcribe")
async def transcribe_message(self, ctx: discord.ApplicationContext, message: discord.Message):
await ctx.defer()
@@ -2331,48 +784,6 @@ class OtherCog(commands.Cog):
("Cached response ({})" if cached else "Uncached response ({})").format(file_hash), ephemeral=True
)
- @commands.slash_command()
- async def whois(self, ctx: discord.ApplicationContext, domain: str):
- """Runs a WHOIS."""
- await ctx.defer()
- url = urlparse("http://" + domain)
- if not url.hostname:
- return await ctx.respond("Invalid domain.")
-
- process = await asyncio.create_subprocess_exec(
- "whois", "-H", url.hostname, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
- )
- stdout, stderr = await process.communicate()
- await process.wait()
- stdout = stdout.decode("utf-8", "replace")
- stderr = stderr.decode("utf-8", "replace")
- if process.returncode != 0:
- return await ctx.respond(f"Error:\n```{stderr[:3900]}```")
-
- paginator = commands.Paginator()
- redacted = io.BytesIO()
- for line in stdout.splitlines():
- if line.startswith(">>> Last update"):
- break
- if "REDACTED" in line or "Please query the WHOIS server of the owning registrar" in line:
- redacted.write(line.encode() + b"\n")
- else:
- paginator.add_line(line)
-
- if redacted.tell() > 0:
- redacted.seek(0)
- file = discord.File(redacted, "redacted-fields.txt", description="Any discovered redacted fields.")
- else:
- file = None
- if len(paginator.pages) > 1:
- for page in paginator.pages:
- await ctx.respond(page)
- if file:
- await ctx.respond(file=file)
- else:
- kwargs = {"file": file} if file else {}
- await ctx.respond(paginator.pages[0], **kwargs)
-
def setup(bot):
bot.add_cog(OtherCog(bot))
diff --git a/cogs/uptime.py b/cogs/uptime.py
deleted file mode 100644
index b92b78d..0000000
--- a/cogs/uptime.py
+++ /dev/null
@@ -1,656 +0,0 @@
-import asyncio
-import datetime
-import hashlib
-import json
-import logging
-import random
-import time
-from datetime import timedelta
-from pathlib import Path
-from typing import Dict, List, Optional, Tuple
-from urllib.parse import ParseResult, parse_qs, urlparse
-
-import discord
-import httpx
-from discord.ext import commands, pages, tasks
-from httpx import AsyncClient, Response
-
-from utils import UptimeEntry, console
-
-"""
-Notice to anyone looking at this code:
-Don't
-It doesn't look nice
-It's not well written
-It just works
-"""
-
-
-BASE_JSON = """[
- {
- "name": "SHRoNK Bot",
- "id": "SHRONK",
- "uri": "user://994710566612500550/1063875884274163732?online=1&idle=0&dnd=0"
- },
- {
- "name": "Nex's Droplet",
- "id": "NEX_DROPLET",
- "uri": "http://droplet.nexy7574.co.uk:9000/"
- },
- {
- "name": "Nex's Pi",
- "id": "PI",
- "uri": "http://wg.nexy7574.co.uk:9000/"
- },
- {
- "name": "SHRoNK Droplet",
- "id": "SHRONK_DROPLET",
- "uri": "http://shronkservz.tk:9000/"
- }
-]
-"""
-
-
-class UptimeCompetition(commands.Cog):
- class CancelTaskView(discord.ui.View):
- def __init__(self, task: asyncio.Task, expires: datetime.datetime):
- timeout = expires - discord.utils.utcnow()
- super().__init__(timeout=timeout.total_seconds() + 3, disable_on_timeout=True)
- self.task = task
-
- @discord.ui.button(label="Cancel", style=discord.ButtonStyle.red)
- async def cancel(self, _, interaction: discord.Interaction):
- self.stop()
- if self.task:
- self.task.cancel()
- await interaction.response.edit_message(content="Uptime test cancelled.", view=None)
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.log = logging.getLogger("jimmy.cogs.uptime")
- self.http = AsyncClient(verify=False, http2=True)
- self._warning_posted = False
- self.test_uptimes.add_exception_type(Exception)
- self.test_uptimes.start()
- self.last_result: list[UptimeEntry] = []
- self.task_lock = asyncio.Lock()
- self.task_event = asyncio.Event()
- self.path = Path.home() / ".cache" / "lcc-bot" / "targets.json"
- if not self.path.exists():
- self.path.write_text(BASE_JSON)
- self._cached_targets = self.read_targets()
-
- @property
- def cached_targets(self) -> List[Dict[str, str]]:
- return self._cached_targets.copy()
-
- def get_target(self, name: str = None, target_id: str = None) -> Optional[Dict[str, str]]:
- for target in self.cached_targets:
- if name and target["name"].lower() == name.lower():
- return target
- if target["id"] == target_id:
- return target
- return
-
- def stats_autocomplete(self, ctx: discord.ApplicationContext) -> List[str]:
- return [x["name"] for x in self.cached_targets if ctx.value.lower() in x["name"].lower()] + ["ALL"]
-
- @staticmethod
- def parse_user_uri(uri: str) -> Dict[str, str | None | List[str]]:
- parsed = urlparse(uri)
- response = {"guild": parsed.hostname, "user": None, "statuses": []}
- if parsed.path:
- response["user"] = parsed.path.strip("/")
- if parsed.query:
- response["statuses"] = [x for x, y in parse_qs(parsed.query).items() if y[0] == "1"]
- return response
-
- def read_targets(self) -> List[Dict[str, str]]:
- with self.path.open() as f:
- data: list = json.load(f)
- data.sort(key=lambda x: x["name"])
- self._cached_targets = data.copy()
- return data
-
- def write_targets(self, data: List[Dict[str, str]]):
- self._cached_targets = data
- with self.path.open("w") as f:
- json.dump(data, f, indent=4, default=str)
-
- def cog_unload(self):
- self.test_uptimes.cancel()
-
- @staticmethod
- def assert_uptime_server_response(response: Response, expected_hash: str = None):
- assert response.status_code in range(200, 400), f"Status code {response.status_code} is not in range 200-400"
- if expected_hash:
- md5 = hashlib.md5()
- md5.update(response.content)
- resolved = md5.hexdigest()
- assert resolved == expected_hash, f"{resolved}!={expected_hash}"
-
- async def _test_url(
- self, url: str, *, max_retries: int | None = 10, timeout: int | None = 30
- ) -> Tuple[int, Response | Exception]:
- attempts = 1
- if max_retries is None:
- max_retries = 1
- if timeout is None:
- timeout = 10
- err = RuntimeError("Unknown Error")
- while attempts < max_retries:
- try:
- response = await self.http.get(url, timeout=timeout, follow_redirects=True)
- response.raise_for_status()
- except (httpx.TimeoutException, httpx.HTTPStatusError, ConnectionError, TimeoutError) as err2:
- attempts += 1
- err = err2
- await asyncio.sleep(1)
- continue
- else:
- return attempts, response
- return attempts, err
-
- async def do_test_uptimes(self):
- targets = self.cached_targets
- # First we need to check that we are online.
- # If we aren't online, this isn't very fair.
- try:
- await self.http.get("https://google.co.uk/")
- except (httpx.HTTPError, Exception):
- return # Offline :pensive:
-
- create_tasks = []
- # We need to collect the tasks in case the sqlite server is being sluggish
- for target in targets.copy():
- if not target["uri"].startswith("http"):
- continue
- targets.remove(target)
- request_kwargs = {}
- if timeout := target.get("timeout"):
- request_kwargs["timeout"] = timeout
- if max_retries := target.get("max_retries"):
- request_kwargs["max_retries"] = max_retries
- kwargs: Dict[str, str | int | None] = {"target_id": target["id"], "target": target["uri"], "notes": ""}
- try:
- attempts, response = await self._test_url(target["uri"], **request_kwargs)
- except Exception as e:
- response = e
- attempts = 1
- if isinstance(response, Exception):
- kwargs["is_up"] = False
- kwargs["response_time"] = None
- kwargs["notes"] += f"Failed to access page after {attempts:,} attempts: {response}"
- else:
- if attempts > 1:
- kwargs["notes"] += f"After {attempts:,} attempts, "
- try:
- self.assert_uptime_server_response(response, target.get("http_content_hash"))
- except AssertionError as e:
- kwargs["is_up"] = False
- kwargs["notes"] += "content was invalid: " + str(e)
- else:
- kwargs["is_up"] = True
- kwargs["response_time"] = round(response.elapsed.total_seconds() * 1000)
- kwargs["notes"] += "nothing notable."
- create_tasks.append(self.bot.loop.create_task(UptimeEntry.objects.create(**kwargs)))
-
- # We need to check if the shronk bot is online since matthew
- # Won't let us access their server (cough cough)
- if self.bot.intents.presences is True:
- # If we don't have presences this is useless.
- if not self.bot.is_ready():
- await self.bot.wait_until_ready()
- for target in targets:
- parsed = urlparse(target["uri"])
- if parsed.scheme != "user":
- continue
- guild_id = int(parsed.hostname)
- user_id = int(parsed.path.strip("/"))
- okay_statuses = [
- discord.Status.online if "online=1" in parsed.query.lower() else None,
- discord.Status.idle if "idle=1" in parsed.query.lower() else None,
- discord.Status.dnd if "dnd=1" in parsed.query.lower() else None,
- ]
- if "offline=1" in parsed.query:
- okay_statuses = [discord.Status.offline]
- okay_statuses = list(filter(None, okay_statuses))
- guild: discord.Guild = self.bot.get_guild(guild_id)
- if guild is None:
- self.log.warning(
- f"[yellow]:warning: Unable to locate the guild for {target['name']!r}! Can't uptime check."
- )
- else:
- user: discord.Member | None = guild.get_member(user_id)
- if not user:
- # SHRoNK Bot is not in members cache.
- try:
- user = await guild.fetch_member(user_id)
- except discord.HTTPException:
- self.log.warning(f"[yellow]Unable to locate {target['name']!r}! Can't uptime check.")
- user = None
- if user:
- create_tasks.append(
- self.bot.loop.create_task(
- UptimeEntry.objects.create(
- target_id=target["id"],
- target=target["name"],
- is_up=user.status in okay_statuses,
- response_time=None,
- notes="*Unable to monitor response time, not a HTTP request.*",
- )
- )
- )
- else:
- if self._warning_posted is False:
- self.log.warning(
- "[yellow]:warning: Jimmy does not have the presences intent enabled. Uptime monitoring of the"
- " shronk bot is disabled."
- )
- self._warning_posted = True
-
- # Now we have to collect the tasks
- return await asyncio.gather(*create_tasks, return_exceptions=True)
- # All done!
-
- @tasks.loop(minutes=1)
- # @tasks.loop(seconds=30)
- async def test_uptimes(self):
- self.task_event.clear()
- async with self.task_lock:
- try:
- self.last_result = await self.do_test_uptimes()
- except (Exception, ConnectionError):
- print()
- console.print_exception()
- self.task_event.set()
-
- uptime = discord.SlashCommandGroup("uptime", "Commands for the uptime competition.")
-
- @uptime.command(name="stats")
- @commands.max_concurrency(1, commands.BucketType.user, wait=True)
- @commands.cooldown(1, 10, commands.BucketType.user)
- async def stats(
- self,
- ctx: discord.ApplicationContext,
- query_target: discord.Option(
- str,
- name="target",
- description="The target to check the uptime of. Defaults to all.",
- required=False,
- autocomplete=stats_autocomplete,
- default="ALL",
- ),
- look_back: discord.Option(
- int, description="How many days to look back. Defaults to a week.", required=False, default=7
- ),
- ):
- """View collected uptime stats."""
- org_target = query_target
-
- def generate_embed(target, specific_entries: list[UptimeEntry]):
- targ = target
- embed = discord.Embed(
- title=f"Uptime stats for {targ['name']}",
- description=f"Showing uptime stats for the last {look_back:,} days.",
- color=discord.Color.blurple(),
- )
- first_check = datetime.datetime.fromtimestamp(specific_entries[-1].timestamp, datetime.timezone.utc)
- last_offline = last_online = None
- online_count = offline_count = 0
- for entry in reversed(specific_entries):
- if entry.is_up is False:
- last_offline = datetime.datetime.fromtimestamp(entry.timestamp, datetime.timezone.utc)
- offline_count += 1
- else:
- last_online = datetime.datetime.fromtimestamp(entry.timestamp, datetime.timezone.utc)
- online_count += 1
- total_count = online_count + offline_count
- online_avg = (online_count / total_count) * 100
- average_response_time = (
- sum(entry.response_time for entry in entries if entry.response_time is not None) / total_count
- )
- if org_target != "ALL":
- embed.add_field(
- name="\u200b",
- value=f"*Started monitoring {discord.utils.format_dt(first_check, style='R')}, "
- f"{total_count:,} monitoring events collected*\n"
- f"**Online:**\n\t\\* {online_avg:.2f}% of the time ({online_count:,} events)\n\t"
- f"\\* Last seen online: "
- f"{discord.utils.format_dt(last_online, 'R') if last_online else 'Never'}\n"
- f"\n"
- f"**Offline:**\n\t\\* {100 - online_avg:.2f}% of the time ({offline_count:,} events)\n\t"
- f"\\* Last seen offline: "
- f"{discord.utils.format_dt(last_offline, 'R') if last_offline else 'Never'}\n"
- f"\n"
- f"**Average Response Time:**\n\t\\* {average_response_time:.2f}ms",
- )
- else:
- embed.title = None
- embed.description = f"{targ['name']}: {online_avg:.2f}% uptime, last offline: "
- if last_offline:
- embed.description += f"{discord.utils.format_dt(last_offline, 'R')}"
- return embed
-
- await ctx.defer()
- now = discord.utils.utcnow()
- look_back_timestamp = (now - timedelta(days=look_back)).timestamp()
- targets = [query_target]
- if query_target == "ALL":
- targets = [t["id"] for t in self.cached_targets]
-
- total_query_time = rows = 0
- embeds = entries = []
- for _target in targets:
- _target = self.get_target(_target, _target)
- query = UptimeEntry.objects.filter(timestamp__gte=look_back_timestamp).filter(target_id=_target["id"])
- query = query.order_by("-timestamp")
- start = time.time()
- entries = await query.all()
- end = time.time()
- total_query_time += end - start
- if not entries:
- embeds.append(discord.Embed(description=f"No uptime entries found for {_target}."))
- else:
- embeds.append(await asyncio.to_thread(generate_embed, _target, entries))
- embeds[-1].set_footer(text=f"Query took {end - start:.2f}s | {len(entries):,} rows")
-
- if org_target == "ALL":
- new_embed = discord.Embed(
- title="Uptime stats for all monitored targets:",
- description=f"Showing uptime stats for the last {look_back:,} days.\n\n",
- color=discord.Color.blurple(),
- )
- for embed_ in embeds:
- new_embed.description += f"{embed_.description}\n"
- if total_query_time > 60:
- minutes, seconds = divmod(total_query_time, 60)
- new_embed.set_footer(text=f"Total query time: {minutes:.0f}m {seconds:.2f}s")
- else:
- new_embed.set_footer(text=f"Total query time: {total_query_time:.2f}s")
- new_embed.set_footer(text=f"{new_embed.footer.text} | {len(entries):,} rows")
- embeds = [new_embed]
- await ctx.respond(embeds=embeds)
-
- @uptime.command(name="speedtest")
- @commands.is_owner()
- async def stats_speedtest(self, ctx: discord.ApplicationContext):
- """Tests the database's speed"""
- await ctx.defer()
-
- tests = {
- "all": None,
- "lookback_7_day": None,
- "lookback_30_day": None,
- "lookback_90_day": None,
- "lookback_365_day": None,
- "first": None,
- "random": None,
- }
-
- async def run_test(name):
- match name:
- case "all":
- start = time.time()
- e = await UptimeEntry.objects.all()
- end = time.time()
- return (end - start) * 1000, len(e)
- case "lookback_7_day":
- start = time.time()
- e = await UptimeEntry.objects.filter(
- UptimeEntry.columns.timestamp >= (discord.utils.utcnow() - timedelta(days=7)).timestamp()
- ).all()
- end = time.time()
- return (end - start) * 1000, len(e)
- case "lookback_30_day":
- start = time.time()
- e = await UptimeEntry.objects.filter(
- UptimeEntry.columns.timestamp >= (discord.utils.utcnow() - timedelta(days=30)).timestamp()
- ).all()
- end = time.time()
- return (end - start) * 1000, len(e)
- case "lookback_90_day":
- start = time.time()
- e = await UptimeEntry.objects.filter(
- UptimeEntry.columns.timestamp >= (discord.utils.utcnow() - timedelta(days=90)).timestamp()
- ).all()
- end = time.time()
- return (end - start) * 1000, len(e)
- case "lookback_365_day":
- start = time.time()
- e = await UptimeEntry.objects.filter(
- UptimeEntry.columns.timestamp >= (discord.utils.utcnow() - timedelta(days=365)).timestamp()
- ).all()
- end = time.time()
- return (end - start) * 1000, len(e)
- case "first":
- start = time.time()
- e = await UptimeEntry.objects.first()
- end = time.time()
- return (end - start) * 1000, 1
- case "random":
- start = time.time()
- e = await UptimeEntry.objects.offset(random.randint(0, 1000)).first()
- end = time.time()
- return (end - start) * 1000, 1
- case _:
- raise ValueError(f"Unknown test name: {name}")
-
- def gen_embed(_copy):
- embed = discord.Embed(title="\N{HOURGLASS} Speedtest Results", colour=discord.Colour.red())
- for _name in _copy.keys():
- if _copy[_name] is None:
- embed.add_field(name=_name, value="Waiting...")
- else:
- _time, _row = _copy[_name]
- _time /= 1000
- rows_per_second = _row / _time
-
- if _time >= 60:
- minutes, seconds = divmod(_time, 60)
- ts = f"{minutes:.0f}m {seconds:.2f}s"
- else:
- ts = f"{_time:.2f}s"
-
- embed.add_field(name=_name, value=f"{ts}, {_row:,} rows ({rows_per_second:.2f} rows/s)")
- return embed
-
- embed = gen_embed(tests)
- ctx = await ctx.respond(embed=embed)
-
- for test_key in tests.keys():
- tests[test_key] = await run_test(test_key)
- embed = gen_embed(tests)
- await ctx.edit(embed=embed)
-
- embed.colour = discord.Colour.green()
- await ctx.edit(embed=embed)
-
- @uptime.command(name="view-next-run")
- async def view_next_run(self, ctx: discord.ApplicationContext):
- """View when the next uptime test will run."""
- await ctx.defer()
- next_run = self.test_uptimes.next_iteration
- if next_run is None:
- return await ctx.respond("The uptime test is not running!")
- else:
- _wait = self.bot.loop.create_task(discord.utils.sleep_until(next_run))
- view = self.CancelTaskView(_wait, next_run)
- await ctx.respond(
- f"The next uptime test will run in {discord.utils.format_dt(next_run, 'R')}. Waiting...", view=view
- )
- await _wait
- if not self.task_event.is_set():
- await ctx.edit(content="Uptime test running! Waiting for results...", view=None)
- await self.task_event.wait()
- embeds = []
- for result in self.last_result:
- if isinstance(result, Exception):
- embed = discord.Embed(
- title="Error",
- description=f"An error occurred while running an uptime test: {result}",
- color=discord.Color.red(),
- )
- else:
- result = await UptimeEntry.objects.get(entry_id=result.entry_id)
- target = self.get_target(target_id=result.target_id) or {"name": result.target_id}
- embed = discord.Embed(
- title="Uptime for: " + target["name"],
- description="Is up: {0.is_up!s}\n"
- "Response time: {1:,.2f}ms\n"
- "Notes: {0.notes!s}".format(result, result.response_time or -1),
- color=discord.Color.green(),
- )
- embeds.append(embed)
- if len(embeds) >= 3:
- paginator = pages.Paginator(embeds, loop_pages=True)
- await ctx.delete(delay=0.1)
- await paginator.respond(ctx.interaction)
- else:
- await ctx.edit(content="Uptime test complete! Results are in:", embeds=embeds, view=None)
-
- monitors = uptime.create_subgroup(name="monitors", description="Manage uptime monitors.")
-
- @monitors.command(name="add")
- @commands.is_owner()
- async def add_monitor(
- self,
- ctx: discord.ApplicationContext,
- name: discord.Option(str, description="The name of the monitor."),
- uri: discord.Option(
- str,
- description="The URI to monitor. Enter HELP for more info.",
- ),
- http_max_retries: discord.Option(
- int,
- description="The maximum number of HTTP retries to make if the request fails.",
- required=False,
- default=None,
- ),
- http_timeout: discord.Option(
- int, description="The timeout for the HTTP request.", required=False, default=None
- ),
- http_md5_content_hash: discord.Option(
- str, description="The hash of the content to check for.", required=False, default=None
- ),
- ):
- """Creates a monitor to... monitor"""
- await ctx.defer()
- name: str
- uri: str
- http_max_retries: Optional[int]
- http_timeout: Optional[int]
-
- uri: ParseResult = urlparse(uri.lower())
- if uri.scheme == "" and uri.path.lower() == "help":
- return await ctx.respond(
- "URI can be either `HTTP(S)`, or the custom `USER` scheme.\n"
- "Examples:\n"
- "`http://example.com` - HTTP GET request to example.com\n"
- "`https://example.com` - HTTPS GET request to example.com\n\n"
- f"`user://{ctx.guild.id}/{ctx.user.id}?dnd=1` - Checks if user `{ctx.user.id}` (you) is"
- f" in `dnd` (the red status) in the server `{ctx.guild.id}` (this server)."
- f"\nQuery options are: `dnd`, `idle`, `online`, `offline`. `1` means the status is classed as "
- f"'online' - anything else means offline.\n"
- f"The format is `user://{{server_id}}/{{user_id}}?{{status}}={{1|0}}`\n"
- f"Setting `server_id` to `$GUILD` will auto-fill in the current server ID."
- )
-
- options = {
- "name": name,
- "id": name.upper().strip().replace(" ", "_"),
- }
-
- if uri.scheme == "user":
- uri: ParseResult = uri._replace(netloc=uri.hostname.replace("$guild", str(ctx.guild.id)))
- data = self.parse_user_uri(uri.geturl())
- if data["guild"] is None or data["guild"].isdigit() is False:
- return await ctx.respond("Invalid guild ID in URI.")
- if data["user"] is None or data["user"].isdigit() is False:
- return await ctx.respond("Invalid user ID in URI.")
- if not data["statuses"] or any(
- x.lower() not in ("dnd", "idle", "online", "offline") for x in data["statuses"]
- ):
- return await ctx.respond("Invalid status query string in URI. Did you forget to supply one?")
-
- guild = self.bot.get_guild(int(data["guild"]))
- if guild is None:
- return await ctx.respond("Invalid guild ID in URI (not found).")
-
- try:
- guild.get_member(int(data["user"])) or await guild.fetch_member(int(data["user"]))
- except discord.HTTPException:
- return await ctx.respond("Invalid user ID in URI (not found).")
-
- options["uri"] = uri.geturl()
-
- elif uri.scheme in ["http", "https"]:
- attempts, response = await self._test_url(uri.geturl(), max_retries=http_max_retries, timeout=http_timeout)
- if response is None:
- return await ctx.respond(
- f"Failed to connect to {uri.geturl()!r} after {attempts} attempts. Please ensure the target page"
- f" is up, and try again."
- )
-
- options["uri"] = uri.geturl()
- options["http_max_retries"] = http_max_retries
- options["http_timeout"] = http_timeout
-
- if http_md5_content_hash == "GEN":
- http_md5_content_hash = hashlib.md5(response.content).hexdigest()
- options["http_content_hash"] = http_md5_content_hash
- else:
- return await ctx.respond("Invalid URI scheme. Supported: HTTP[S], USER.")
-
- targets = self.cached_targets
- for target in targets:
- if target["uri"] == options["uri"] or target["name"] == options["name"]:
- return await ctx.respond("This monitor already exists.")
-
- targets.append(options)
- self.write_targets(targets)
- await ctx.respond("Monitor added!")
-
- @monitors.command(name="dump")
- async def show_monitor(
- self, ctx: discord.ApplicationContext, name: discord.Option(str, description="The name of the monitor.")
- ):
- """Shows a monitor's data."""
- await ctx.defer()
- name: str
-
- target = self.get_target(name)
- if target:
- return await ctx.respond(f"```json\n{json.dumps(target, indent=4)}```")
- await ctx.respond("Monitor not found.")
-
- @monitors.command(name="remove")
- @commands.is_owner()
- async def remove_monitor(
- self,
- ctx: discord.ApplicationContext,
- name: discord.Option(str, description="The name of the monitor."),
- ):
- """Removes a monitor."""
- await ctx.defer()
- name: str
-
- targets = self.cached_targets
- for target in targets:
- if target["name"] == name or target["id"] == name:
- targets.remove(target)
- self.write_targets(targets)
- return await ctx.respond("Monitor removed.")
- await ctx.respond("Monitor not found.")
-
- @monitors.command(name="reload")
- @commands.is_owner()
- async def reload_monitors(self, ctx: discord.ApplicationContext):
- """Reloads monitors data file from disk, overriding cache."""
- await ctx.defer()
- self.read_targets()
- await ctx.respond("Monitors reloaded.")
-
-
-def setup(bot):
- bot.add_cog(UptimeCompetition(bot))
diff --git a/main.py b/main.py
index e61efe7..285d192 100644
--- a/main.py
+++ b/main.py
@@ -1,3 +1,4 @@
+import asyncio
import logging
import os
import signal
@@ -76,7 +77,11 @@ async def on_application_command_error(ctx: discord.ApplicationContext, error: E
return
if ctx.user.id == 1019233057519177778:
- await ctx.respond("Uh oh! I did a fucky wucky >.< I'll make sure to let important peoplez know straight away!!")
+ await ctx.respond("Something happened!")
+ await asyncio.sleep(5)
+ await ctx.edit(
+ content="Command Error: `%s`" % repr(error)[:1950],
+ )
else:
text = "Application Command Error: `%r`" % error
await ctx.respond(textwrap.shorten(text, 2000))
@@ -117,20 +122,19 @@ async def ping(ctx: discord.ApplicationContext):
gateway = round(ctx.bot.latency * 1000, 2)
return await ctx.respond(f"\N{white heavy check mark} Pong! `{gateway}ms`.")
-
-@bot.check_once
-async def check_not_banned(ctx: discord.ApplicationContext | commands.Context):
- if await bot.is_owner(ctx.author) or ctx.command.name in ("block", "unblock", "timetable", "verify", "kys"):
- return True
- user = ctx.author
- ban: JimmyBans = await get_or_none(JimmyBans, user_id=user.id)
- if ban:
- dt = datetime.fromtimestamp(ban.until, timezone.utc)
- if dt < discord.utils.utcnow():
- await ban.delete()
- else:
- raise JimmyBanException(dt, ban.reason)
- return True
+# @bot.check_once
+# async def check_not_banned(ctx: discord.ApplicationContext | commands.Context):
+# if await bot.is_owner(ctx.author) or ctx.command.name in ("block", "unblock", "timetable", "verify", "kys"):
+# return True
+# user = ctx.author
+# ban: JimmyBans = await get_or_none(JimmyBans, user_id=user.id)
+# if ban:
+# dt = datetime.fromtimestamp(ban.until, timezone.utc)
+# if dt < discord.utils.utcnow():
+# await ban.delete()
+# else:
+# raise JimmyBanException(dt, ban.reason)
+# return True
if __name__ == "__main__":