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__":