Merge branch 'master' of github.com:nexy7574/lcc-bot

This commit is contained in:
Nexus 2023-12-24 00:15:49 +00:00
commit 3f42a9ca6f
Signed by: nex
GPG key ID: 0FA334385D0B689F
16 changed files with 552 additions and 593 deletions

View file

@ -20,9 +20,9 @@ import discord
import httpx import httpx
import pydantic import pydantic
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from config import guilds
from discord.ext import commands, pages, tasks from discord.ext import commands, pages, tasks
from config import guilds
from utils import Student, console, get_or_none from utils import Student, console, get_or_none
try: try:
@ -90,25 +90,11 @@ class Events(commands.Cog):
self.bot.bridge_queue = asyncio.Queue() self.bot.bridge_queue = asyncio.Queue()
self.fetch_discord_atom_feed.start() self.fetch_discord_atom_feed.start()
self.bridge_health = False self.bridge_health = False
self.log = logging.getLogger("jimmy.cogs.events")
def cog_unload(self): def cog_unload(self):
self.fetch_discord_atom_feed.cancel() self.fetch_discord_atom_feed.cancel()
# noinspection DuplicatedCode
async def analyse_text(self, text: str) -> Optional[Tuple[float, float, float, float]]:
"""Analyse text for positivity, negativity and neutrality."""
def inner():
try:
from utils.sentiment_analysis import intensity_analyser
except ImportError:
return None
scores = intensity_analyser.polarity_scores(text)
return scores["pos"], scores["neu"], scores["neg"], scores["compound"]
async with self.bot.training_lock:
return await self.bot.loop.run_in_executor(None, inner)
@commands.Cog.listener("on_raw_reaction_add") @commands.Cog.listener("on_raw_reaction_add")
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
channel: Optional[discord.TextChannel] = self.bot.get_channel(payload.channel_id) channel: Optional[discord.TextChannel] = self.bot.get_channel(payload.channel_id)
@ -126,16 +112,17 @@ class Events(commands.Cog):
if member.guild is None or member.guild.id not in guilds: if member.guild is None or member.guild.id not in guilds:
return return
student: Optional[Student] = await get_or_none(Student, user_id=member.id) # student: Optional[Student] = await get_or_none(Student, user_id=member.id)
if student and student.id: # if student and student.id:
role = discord.utils.find(lambda r: r.name.lower() == "verified", member.guild.roles) # role = discord.utils.find(lambda r: r.name.lower() == "verified", member.guild.roles)
if role and role < member.guild.me.top_role: # if role and role < member.guild.me.top_role:
await member.add_roles(role, reason="Verified") # await member.add_roles(role, reason="Verified")
channel: discord.TextChannel = discord.utils.get(member.guild.text_channels, name="general") channel: discord.TextChannel = discord.utils.get(member.guild.text_channels, name="general")
if channel and channel.can_send(): if channel and channel.can_send():
await channel.send( await channel.send(
f"{LTR} {member.mention} (`{member}`, {f'{student.id}' if student else 'pending verification'})" # f"{LTR} {member.mention} (`{member}`, {f'{student.id}' if student else 'pending verification'})"
f"{LTR} {member.mention}"
) )
@commands.Cog.listener() @commands.Cog.listener()
@ -143,11 +130,12 @@ class Events(commands.Cog):
if member.guild is None or member.guild.id not in guilds: if member.guild is None or member.guild.id not in guilds:
return return
student: Optional[Student] = await get_or_none(Student, user_id=member.id) # student: Optional[Student] = await get_or_none(Student, user_id=member.id)
channel: discord.TextChannel = discord.utils.get(member.guild.text_channels, name="general") channel: discord.TextChannel = discord.utils.get(member.guild.text_channels, name="general")
if channel and channel.can_send(): if channel and channel.can_send():
await channel.send( await channel.send(
f"{RTL} {member.mention} (`{member}`, {f'{student.id}' if student else 'pending verification'})" # 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): async def process_message_for_github_links(self, message: discord.Message):
@ -248,7 +236,7 @@ class Events(commands.Cog):
) )
region = message.author.voice.channel.rtc_region region = message.author.voice.channel.rtc_region
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
console.log( self.log.warning(
"Timed out connecting to voice channel: {0.name} in {0.guild.name} " "Timed out connecting to voice channel: {0.name} in {0.guild.name} "
"(region {1})".format( "(region {1})".format(
message.author.voice.channel, region.name if region else "auto (unknown)" message.author.voice.channel, region.name if region else "auto (unknown)"
@ -269,7 +257,7 @@ class Events(commands.Cog):
_dc(voice), _dc(voice),
) )
if err is not None: if err is not None:
console.log(f"Error playing audio: {err}") self.log.error(f"Error playing audio: {err}", exc_info=err)
self.bot.loop.create_task(message.add_reaction("\N{speaker with cancellation stroke}")) self.bot.loop.create_task(message.add_reaction("\N{speaker with cancellation stroke}"))
else: else:
self.bot.loop.create_task( self.bot.loop.create_task(
@ -331,15 +319,16 @@ class Events(commands.Cog):
return return
if message.channel.name == "femboy-hole": if message.channel.name == "femboy-hole":
def generate_payload(_message: discord.Message) -> MessagePayload: def generate_payload(_message: discord.Message) -> MessagePayload:
_payload = MessagePayload( _payload = MessagePayload(
message_id=_message.id, message_id=_message.id,
author=_message.author.name, author=_message.author.name,
is_automated=_message.author.bot or _message.author.system, is_automated=_message.author.bot or _message.author.system,
avatar=_message.author.display_avatar.with_static_format("webp").with_size(512).url, avatar=_message.author.display_avatar.with_static_format("webp").with_size(512).url,
content=_message.content or '', content=_message.content or "",
clean_content=str(_message.clean_content or ''), clean_content=str(_message.clean_content or ""),
at=_message.created_at.timestamp() at=_message.created_at.timestamp(),
) )
for attachment in _message.attachments: for attachment in _message.attachments:
_payload.attachments.append( _payload.attachments.append(
@ -350,7 +339,7 @@ class Events(commands.Cog):
size=attachment.size, size=attachment.size,
width=attachment.width, width=attachment.width,
height=attachment.height, height=attachment.height,
content_type=attachment.content_type content_type=attachment.content_type,
) )
) )
if _message.reference is not None and _message.reference.cached_message: if _message.reference is not None and _message.reference.cached_message:
@ -358,11 +347,7 @@ class Events(commands.Cog):
_payload.reply_to = generate_payload(_message.reference.cached_message) _payload.reply_to = generate_payload(_message.reference.cached_message)
except RecursionError: except RecursionError:
_payload.reply_to = None _payload.reply_to = None
logging.warning( logging.warning("Failed to generate reply payload for message %s", _message.id, exc_info=True)
"Failed to generate reply payload for message %s",
_message.id,
exc_info=True
)
return _payload return _payload
payload = generate_payload(message) payload = generate_payload(message)
@ -448,7 +433,7 @@ class Events(commands.Cog):
r"china": {"file": discord.File(assets / "china.m4a")}, r"china": {"file": discord.File(assets / "china.m4a")},
r"drones": {"file": discord.File(assets / "drones.m4a")}, r"drones": {"file": discord.File(assets / "drones.m4a")},
r"pork($|\W+)|markets": {"file": discord.File(assets / "pork.m4a")}, r"pork($|\W+)|markets": {"file": discord.File(assets / "pork.m4a")},
r"common\ssense\W*$|(wo)?man\W*$|(trans(\s)?)?gender\W*$": { r"(wo)?man\sis\sa\s(wo)?man|(trans(\s)?)?gender\W*$": {
"file": discord.File(assets / "common-sense.m4a") "file": discord.File(assets / "common-sense.m4a")
}, },
r"scrapped(\sit)?|((7\s|seven\s)?different\s)?bins|(meat\s|flying\s)?tax": { r"scrapped(\sit)?|((7\s|seven\s)?different\s)?bins|(meat\s|flying\s)?tax": {
@ -458,10 +443,12 @@ class Events(commands.Cog):
r"brush|hair": {"file": discord.File(assets / "hair.m4a")}, r"brush|hair": {"file": discord.File(assets / "hair.m4a")},
r"((cup\s)?of\s)?tea\W*$": {"file": discord.File(assets / "tea.m4a")}, r"((cup\s)?of\s)?tea\W*$": {"file": discord.File(assets / "tea.m4a")},
r"wheat|fields": {"file": discord.File(assets / "wheat.m4a")}, r"wheat|fields": {"file": discord.File(assets / "wheat.m4a")},
r"bus((s)?es)?\W*$": {"file": discord.File(assets / "bus.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"^DoH$": {"content": "DoH: Domain Name Service over Hyper Text Transfer Protocol Secure"},
r"^DoT$": {"content": "DoT: Domain Name Service over Transport Layer Security"}, 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"^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"^(Do)?DTLS$": {"content": "DoDTLS: Domain Name Service over Datagram Transport Layer Security"},
} }
# Stop responding to any bots # Stop responding to any bots
@ -590,14 +577,14 @@ class Events(commands.Cog):
try: try:
response = await self.http.get("https://discordstatus.com/history.atom", headers=headers) response = await self.http.get("https://discordstatus.com/history.atom", headers=headers)
except httpx.HTTPError as e: except httpx.HTTPError as e:
console.log("Failed to fetch discord atom feed:", e) self.log.error("Failed to fetch discord atom feed: %r", e, exc_info=e)
return return
if response.status_code == 304: if response.status_code == 304:
return return
if response.status_code != 200: if response.status_code != 200:
console.log("Failed to fetch discord atom feed:", response.status_code) self.log.error("Failed to fetch discord atom feed: HTTP/%s", response.status_code)
return return
with file.open("wb") as f: with file.open("wb") as f:

View file

@ -138,11 +138,11 @@ class Extremism(commands.Cog):
@commands.slash_command(name="decorate") @commands.slash_command(name="decorate")
async def decorate( async def decorate(
self, self,
ctx: discord.ApplicationContext, ctx: discord.ApplicationContext,
decoration_url: str, decoration_url: str,
user: discord.User = None, user: discord.User = None,
# animated: bool = True # animated: bool = True
): ):
"""Decorates an avatar with a decoration.""" """Decorates an avatar with a decoration."""
if user is None: if user is None:
@ -162,9 +162,7 @@ class Extremism(commands.Cog):
size = 640 size = 640
size = min(640, max(160, size)) size = min(640, max(160, size))
decoration_url = urlparse(decoration_url)._replace( decoration_url = urlparse(decoration_url)._replace(query="?size={!s}&passthrough=true".format(size)).geturl()
query="?size={!s}&passthrough=true".format(size)
).geturl()
# Download the decoration # Download the decoration
try: try:
@ -199,9 +197,7 @@ class Extremism(commands.Cog):
# discord.File(decoration_bio, "decoration.png"), # discord.File(decoration_bio, "decoration.png"),
discord.File(img_bytes, filename="decorated." + ext) discord.File(img_bytes, filename="decorated." + ext)
] ]
await ctx.respond( await ctx.respond(files=files)
files=files
)
def setup(bot): def setup(bot):

View file

@ -1,3 +1,6 @@
import logging
import typing
import discord import discord
import httpx import httpx
from discord.ext import commands from discord.ext import commands
@ -124,9 +127,27 @@ class InfoCog(commands.Cog):
await ctx.respond(embed=embed) await ctx.respond(embed=embed)
@commands.command(name="set-log-level")
@commands.is_owner()
async def set_log_level(self, ctx: commands.Context, logger_name: str, logger_level: typing.Union[str, int]):
"""Sets a module's log level."""
logger_level = logger_level.upper()
if getattr(logging, logger_level, None) is None or logging.getLevelNamesMapping().get(logger_level) is None:
return await ctx.reply(":x: Invalid log level.")
logger = logging.getLogger(logger_name)
old_level = logger.getEffectiveLevel()
def name(level):
return logging.getLevelName(level)
logger.setLevel(logger_level)
return await ctx.reply(
f"\N{white heavy check mark} {logger_name}: {name(old_level)} -> {name(logger.getEffectiveLevel())}"
)
def setup(bot): def setup(bot):
if OAUTH_REDIRECT_URI and OAUTH_ID: if OAUTH_REDIRECT_URI and OAUTH_ID:
bot.add_cog(InfoCog(bot)) bot.add_cog(InfoCog(bot))
else: else:
print("OAUTH_REDIRECT_URI not set, not loading info cog") logging.getLogger("jimmy.cogs.info").warning("OAUTH_REDIRECT_URI not set, not loading info cog")

207
cogs/mcdonalds.py Normal file
View file

@ -0,0 +1,207 @@
import asyncio
import logging
import pathlib
import re
import typing
import aiosqlite
import discord
from discord.ext import commands
class McDataBase:
def __init__(self):
self.db = pathlib.Path.home() / ".cache" / "lcc-bot" / "McDataBase.db"
self._conn: typing.Optional[aiosqlite.Connection] = None
async def init_db(self):
if self._conn:
conn = self._conn
now = round(discord.utils.utcnow().timestamp(), 2)
await conn.execute(
f"""
CREATE TABLE IF NOT EXISTS breaks (
user_id INTEGER PRIMARY KEY,
since FLOAT NOT NULL,
started FLOAT NOT NULL DEFAULT {now}
);
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS cooldowns (
user_id INTEGER PRIMARY KEY,
expires FLOAT NOT NULL
);
"""
)
async def get_break(self, user_id: int) -> typing.Optional[tuple[float, float]]:
async with self._conn.execute(
"""
SELECT since, started FROM breaks WHERE user_id = ?;
""",
(user_id,),
) as cursor:
return await cursor.fetchone()
async def set_break(self, user_id: int, since: float, started: float = None) -> None:
if not started:
started = discord.Object(discord.utils.generate_snowflake()).created_at.timestamp()
await self._conn.execute(
"""
INSERT INTO breaks (user_id, since, started) VALUES (?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET since = excluded.since, started = excluded.started
""",
(user_id, since, started),
)
async def remove_break(self, user_id: int) -> None:
now = discord.utils.utcnow().timestamp()
await self._conn.execute(
"""
DELETE FROM breaks WHERE user_id = ?;
""",
(user_id,),
)
await self.set_cooldown(user_id, now)
async def get_cooldown(self, user_id: int) -> typing.Optional[tuple[float]]:
async with self._conn.execute(
"""
SELECT expires FROM cooldowns WHERE user_id = ?;
""",
(user_id,),
) as cursor:
return await cursor.fetchone()
async def set_cooldown(self, user_id: int, expires: float) -> None:
await self._conn.execute(
"""
INSERT INTO cooldowns (user_id, expires) VALUES (?, ?)
ON CONFLICT(user_id) DO UPDATE SET expires = excluded.expires;
""",
(user_id, expires),
)
async def remove_cooldown(self, user_id: int) -> None:
await self._conn.execute(
"""
DELETE FROM cooldowns WHERE user_id = ?;
""",
(user_id,),
)
async def __aenter__(self) -> "McDataBase":
self._conn = await aiosqlite.connect(self.db)
await self.init_db()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self._conn.commit()
await self._conn.close()
self._conn = None
class McDonaldsCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.lock = asyncio.Lock()
self.log = logging.getLogger("jimmy.cogs.mcdonalds")
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
author = message.author
me = message.guild.me if message.guild else self.bot.user
if not message.channel.permissions_for(me).manage_messages:
self.log.debug("McDonalds disabled in %s, no manage messages permissions.", message.channel)
return
async with self.lock:
async with McDataBase() as db:
if (last_info := await db.get_break(author.id)) is not None:
if message.content.upper() != "MCDONALDS!":
if (message.created_at.timestamp() - last_info[1]) > 300:
self.log.debug("Ad break expired for %s (%s).", author.name, author.id)
await db.remove_break(author.id)
await message.reply(
f"Thank you for your patience during this commercial break. You may now resume your"
f" activity.",
delete_after=120,
)
elif (message.created_at.timestamp() - last_info[0]) > 10:
self.log.info(
"Deleting message %r by %r as they need to skip the ad first.", message, author
)
await message.delete(delay=0)
await message.channel.send(
f"{message.author.mention} Please say `MCDONALDS!` to end commercial.", delete_after=30
)
await db.set_break(author.id, message.created_at.timestamp())
elif message.author.bot is False:
self.log.info("%r skipped the ad break.", author)
await db.remove_break(author.id)
await message.reply("Thank you. You may now resume your activity.", delete_after=120)
@commands.user_command(name="Commercial Break")
@commands.cooldown(2, 60, commands.BucketType.member)
async def commercial_break(self, ctx: discord.ApplicationContext, member: discord.Member):
await ctx.defer(ephemeral=True)
if not ctx.channel.permissions_for(ctx.me).manage_messages:
return await ctx.respond("I don't have permission to manage messages in this channel.", ephemeral=True)
if member.bot or member == ctx.user:
return await ctx.respond("No.", ephemeral=True)
async with McDataBase() as db:
if await db.get_break(member.id) is not None:
await ctx.respond(f"{member.mention} is already in a commercial break.")
return
elif (cooldown := await db.get_cooldown(member.id)) is not None:
expires = cooldown[0] + 300
if expires > discord.utils.utcnow().timestamp():
await ctx.respond(
f"{member.mention} is not due another ad break yet. Their next commercial break will start "
f"<t:{int(expires)}:R> at the earliest."
)
return
else:
await db.remove_cooldown(member.id)
await db.set_break(member.id, discord.utils.utcnow().timestamp())
await ctx.send(
f"{member.mention} Commercial break! Please say `MCDONALDS!` to end commercial.\n"
f"*This commercial break is sponsored by {ctx.user.mention}.*",
delete_after=300,
allowed_mentions=discord.AllowedMentions(users=True, roles=False, everyone=False),
)
await ctx.respond("Commercial break started.", ephemeral=True)
await ctx.delete(delay=120)
@commands.command(name="commercial-break")
@commands.is_owner()
async def _force_com_break(self, ctx: commands.Context, *, member: discord.Member):
"""Forces a member to go on commercial break."""
async with McDataBase() as db:
await db.set_break(member.id, discord.utils.utcnow().timestamp())
await ctx.reply(
f"{member.mention} Commercial break! Please say `MCDONALDS!` to end commercial.\n"
f"*This commercial break is sponsored by {ctx.author.mention}.*",
delete_after=300,
allowed_mentions=discord.AllowedMentions(users=True, roles=False, everyone=False),
)
await ctx.message.delete(delay=120)
@commands.command(name="end-break")
@commands.is_owner()
async def _unforce_com_break(self, ctx: commands.Context, *, member: discord.Member):
"""Forces a member to finish their commercial break."""
async with McDataBase() as db:
await db.remove_break(member.id)
await ctx.reply(f"{member.mention} Commercial break ended.", delete_after=10)
def setup(bot):
bot.add_cog(McDonaldsCog(bot))

View file

@ -2,18 +2,16 @@ import asyncio
import fnmatch import fnmatch
import functools import functools
import glob import glob
import hashlib
import io import io
import pathlib
import openai
import pydub
import json import json
import logging
import math import math
import os import os
import pathlib
import random import random
import re import re
import shutil import shutil
import hashlib
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@ -32,7 +30,9 @@ import aiohttp
import discord import discord
import dns.resolver import dns.resolver
import httpx import httpx
import openai
import psutil import psutil
import pydub
import pytesseract import pytesseract
import pyttsx3 import pyttsx3
from discord import Interaction from discord import Interaction
@ -69,41 +69,19 @@ try:
VOICES = [x.id for x in _engine.getProperty("voices")] VOICES = [x.id for x in _engine.getProperty("voices")]
del _engine del _engine
except Exception as _pyttsx3_err: except Exception as _pyttsx3_err:
print("Failed to load pyttsx3: %s" % _pyttsx3_err, file=sys.stderr) logging.error("Failed to load pyttsx3: %r", _pyttsx3_err, exc_info=True)
pyttsx3 = None pyttsx3 = None
VOICES = [] VOICES = []
# class OllamaStreamReader: async def ollama_stream_reader(response: httpx.Response) -> typing.AsyncGenerator[dict[str, str | int | bool], None]:
# def __init__(self, response: httpx.Response):
# self.response = response
# self.stream = response.aiter_bytes(1)
# self._buffer = b""
#
# async def __aiter__(self):
# return self
#
# async def __anext__(self) -> dict[str, str | int | bool]:
# if self.response.is_stream_consumed:
# raise StopAsyncIteration
# self._buffer = b""
# while not self._buffer.endswith(b"}\n"):
# async for char in self.stream:
# self._buffer += char
#
# return json.loads(self._buffer.decode("utf-8", "replace"))
async def ollama_stream_reader(response: httpx.Response) -> typing.AsyncGenerator[
dict[str, str | int | bool], None
]:
async for chunk in response.aiter_lines(): async for chunk in response.aiter_lines():
# Each line is a JSON string # Each line is a JSON string
try: try:
loaded = json.loads(chunk) loaded = json.loads(chunk)
yield loaded yield loaded
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print("Failed to decode chunk %r: %r" % (chunk, e), file=sys.stderr) logging.warning("Failed to decode chunk %r: %r", chunk, e)
pass pass
@ -134,6 +112,7 @@ class OtherCog(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self.transcribe_lock = asyncio.Lock()
self.http = httpx.AsyncClient() self.http = httpx.AsyncClient()
self._fmt_cache = {} self._fmt_cache = {}
self._fmt_queue = asyncio.Queue() self._fmt_queue = asyncio.Queue()
@ -141,6 +120,7 @@ class OtherCog(commands.Cog):
self.ollama_locks: dict[discord.Message, asyncio.Event] = {} self.ollama_locks: dict[discord.Message, asyncio.Event] = {}
self.context_cache: dict[str, list[int]] = {} self.context_cache: dict[str, list[int]] = {}
self.log = logging.getLogger("jimmy.cogs.other")
def cog_unload(self): def cog_unload(self):
self._worker_task.cancel() self._worker_task.cancel()
@ -257,7 +237,7 @@ class OtherCog(commands.Cog):
return driver, driver_path return driver, driver_path
driver, driver_path = find_driver() driver, driver_path = find_driver()
console.log( self.log.info(
"Using driver '{}' with binary '{}' to screenshot '{}', as requested by {}.".format( "Using driver '{}' with binary '{}' to screenshot '{}', as requested by {}.".format(
driver, driver_path, website, ctx.user driver, driver_path, website, ctx.user
) )
@ -295,7 +275,7 @@ class OtherCog(commands.Cog):
start_init = time() start_init = time()
driver, friendly_url = await asyncio.to_thread(_setup) driver, friendly_url = await asyncio.to_thread(_setup)
end_init = time() end_init = time()
console.log("Driver '{}' initialised in {} seconds.".format(driver_name, round(end_init - start_init, 2))) self.log.info("Driver '{}' initialised in {} seconds.".format(driver_name, round(end_init - start_init, 2)))
def _edit(content: str): def _edit(content: str):
self.bot.loop.create_task(ctx.interaction.edit_original_response(content=content)) self.bot.loop.create_task(ctx.interaction.edit_original_response(content=content))
@ -349,20 +329,6 @@ class OtherCog(commands.Cog):
) )
return result return result
async def analyse_text(self, text: str) -> Optional[Tuple[float, float, float, float]]:
"""Analyse text for positivity, negativity and neutrality."""
def inner():
try:
from utils.sentiment_analysis import intensity_analyser
except ImportError:
return None
scores = intensity_analyser.polarity_scores(text)
return scores["pos"], scores["neu"], scores["neg"], scores["compound"]
async with self.bot.training_lock:
return await self.bot.loop.run_in_executor(None, inner)
@staticmethod @staticmethod
async def get_xkcd(session: aiohttp.ClientSession, n: int) -> dict | None: async def get_xkcd(session: aiohttp.ClientSession, n: int) -> dict | None:
async with session.get("https://xkcd.com/{!s}/info.0.json".format(n)) as response: async with session.get("https://xkcd.com/{!s}/info.0.json".format(n)) as response:
@ -445,40 +411,6 @@ class OtherCog(commands.Cog):
view = self.XKCDGalleryView(number) view = self.XKCDGalleryView(number)
return await ctx.respond(embed=embed, view=view) return await ctx.respond(embed=embed, view=view)
@commands.slash_command()
async def sentiment(self, ctx: discord.ApplicationContext, *, text: str):
"""Attempts to detect a text's tone"""
await ctx.defer()
if not text:
return await ctx.respond("You need to provide some text to analyse.")
result = await self.analyse_text(text)
if result is None:
return await ctx.edit(content="Failed to load sentiment analysis module.")
embed = discord.Embed(title="Sentiment Analysis", color=discord.Colour.embed_background())
embed.add_field(name="Positive", value="{:.2%}".format(result[0]))
embed.add_field(name="Neutral", value="{:.2%}".format(result[2]))
embed.add_field(name="Negative", value="{:.2%}".format(result[1]))
embed.add_field(name="Compound", value="{:.2%}".format(result[3]))
return await ctx.edit(content=None, embed=embed)
@commands.message_command(name="Detect Sentiment")
async def message_sentiment(self, ctx: discord.ApplicationContext, message: discord.Message):
await ctx.defer()
text = str(message.clean_content)
if not text:
return await ctx.respond("You need to provide some text to analyse.")
await ctx.respond("Analyzing (this may take some time)...")
result = await self.analyse_text(text)
if result is None:
return await ctx.edit(content="Failed to load sentiment analysis module.")
embed = discord.Embed(title="Sentiment Analysis", color=discord.Colour.embed_background())
embed.add_field(name="Positive", value="{:.2%}".format(result[0]))
embed.add_field(name="Neutral", value="{:.2%}".format(result[2]))
embed.add_field(name="Negative", value="{:.2%}".format(result[1]))
embed.add_field(name="Compound", value="{:.2%}".format(result[3]))
embed.url = message.jump_url
return await ctx.edit(content=None, embed=embed)
corrupt_file = discord.SlashCommandGroup( corrupt_file = discord.SlashCommandGroup(
name="corrupt-file", name="corrupt-file",
description="Corrupts files.", description="Corrupts files.",
@ -997,15 +929,12 @@ class OtherCog(commands.Cog):
"merge_output_format": "webm/mp4/mov/flv/avi/ogg/m4a/wav/mp3/opus/mka/mkv", "merge_output_format": "webm/mp4/mov/flv/avi/ogg/m4a/wav/mp3/opus/mka/mkv",
"source_address": "0.0.0.0", "source_address": "0.0.0.0",
"cookiefile": str(real_cookies_txt.resolve().absolute()), "cookiefile": str(real_cookies_txt.resolve().absolute()),
"concurrent_fragment_downloads": 4 "concurrent_fragment_downloads": 4,
} }
description = "" description = ""
proxy_url = "socks5://localhost:1090" proxy_url = "socks5://localhost:1090"
try: try:
proxy_down = await asyncio.wait_for( proxy_down = await asyncio.wait_for(self.check_proxy("socks5://localhost:1090"), timeout=10)
self.check_proxy("socks5://localhost:1090"),
timeout=10
)
if proxy_down > 0: if proxy_down > 0:
if proxy_down == 1: if proxy_down == 1:
description += ":warning: (SHRoNK) Proxy check leaked IP - trying backup proxy.\n" description += ":warning: (SHRoNK) Proxy check leaked IP - trying backup proxy.\n"
@ -1014,10 +943,7 @@ class OtherCog(commands.Cog):
else: else:
description += ":warning: (SHRoNK) Unknown proxy error - trying backup proxy.\n" description += ":warning: (SHRoNK) Unknown proxy error - trying backup proxy.\n"
proxy_down = await asyncio.wait_for( proxy_down = await asyncio.wait_for(self.check_proxy("socks5://localhost:1080"), timeout=10)
self.check_proxy("socks5://localhost:1080"),
timeout=10
)
if proxy_down > 0: if proxy_down > 0:
if proxy_down == 1: if proxy_down == 1:
description += ":warning: (NexBox) Proxy check leaked IP..\n" description += ":warning: (NexBox) Proxy check leaked IP..\n"
@ -1072,20 +998,14 @@ class OtherCog(commands.Cog):
"* Chosen format: `%s` (`%s`)" % (chosen_format, chosen_format_id), "* Chosen format: `%s` (`%s`)" % (chosen_format, chosen_format_id),
) )
if format_note: if format_note:
lines.append( lines.append("* Format note: %r" % format_note)
"* Format note: %r" % format_note
)
if final_extension: if final_extension:
lines.append( lines.append("* File extension: " + final_extension)
"* File extension: " + final_extension
)
if resolution: if resolution:
_s = resolution _s = resolution
if fps: if fps:
_s += " @ %s FPS" % fps _s += " @ %s FPS" % fps
lines.append( lines.append("* Resolution: " + _s)
"* Resolution: " + _s
)
if vcodec or acodec: if vcodec or acodec:
lines.append("%s+%s" % (vcodec or "N/A", acodec or "N/A")) lines.append("%s+%s" % (vcodec or "N/A", acodec or "N/A"))
@ -1822,17 +1742,18 @@ class OtherCog(commands.Cog):
@commands.slash_command() @commands.slash_command()
@commands.max_concurrency(3, commands.BucketType.user, wait=False) @commands.max_concurrency(3, commands.BucketType.user, wait=False)
async def ollama( async def ollama(
self, self,
ctx: discord.ApplicationContext, ctx: discord.ApplicationContext,
model: str = "orca-mini", model: str = "orca-mini",
query: str = None, query: str = None,
context: str = None, context: str = None,
server: str = "auto" server: str = "auto",
): ):
""":3""" """:3"""
with open("./assets/ollama-prompt.txt") as file: with open("./assets/ollama-prompt.txt") as file:
system_prompt = file.read().replace("\n", " ").strip() system_prompt = file.read().replace("\n", " ").strip()
if query is None: if query is None:
class InputPrompt(discord.ui.Modal): class InputPrompt(discord.ui.Modal):
def __init__(self, is_owner: bool): def __init__(self, is_owner: bool):
super().__init__( super().__init__(
@ -1844,7 +1765,7 @@ class OtherCog(commands.Cog):
style=discord.InputTextStyle.long, style=discord.InputTextStyle.long,
), ),
title="Enter prompt", title="Enter prompt",
timeout=120 timeout=120,
) )
if is_owner: if is_owner:
self.add_item( self.add_item(
@ -1887,10 +1808,10 @@ class OtherCog(commands.Cog):
try: try:
model, tag = model.split(":", 1) model, tag = model.split(":", 1)
model = model + ":" + tag model = model + ":" + tag
print("Model %r already has a tag") self.log.debug("Model %r already has a tag")
except ValueError: except ValueError:
model = model + ":latest" model = model + ":latest"
print("Resolved model to %r" % model) self.log.debug("Resolved model to %r" % model)
servers: dict[str, dict[str, str, list[str] | int]] = { servers: dict[str, dict[str, str, list[str] | int]] = {
"100.106.34.86:11434": { "100.106.34.86:11434": {
@ -1907,7 +1828,7 @@ class OtherCog(commands.Cog):
"codellama:python", "codellama:python",
"codellama:instruct", "codellama:instruct",
], ],
"owner": 421698654189912064 "owner": 421698654189912064,
}, },
"ollama.shronk.net:11434": { "ollama.shronk.net:11434": {
"name": "Alibaba Cloud", "name": "Alibaba Cloud",
@ -1922,26 +1843,19 @@ class OtherCog(commands.Cog):
"orca-mini:3b", "orca-mini:3b",
"orca-mini:7b", "orca-mini:7b",
], ],
"owner": 421698654189912064 "owner": 421698654189912064,
}, },
} }
H_DEFAULT = { H_DEFAULT = {"name": "Other", "allow": ["*"], "owner": 1019217990111199243}
"name": "Other",
"allow": ["*"],
"owner": 1019217990111199243
}
def model_is_allowed(model_name: str, _srv: dict[str, str | list[str] | int]) -> bool: def model_is_allowed(model_name: str, _srv: dict[str, str | list[str] | int]) -> bool:
if _srv["owner"] == ctx.user.id: if _srv["owner"] == ctx.user.id:
return True return True
for pat in _srv.get("allow", ['*']): for pat in _srv.get("allow", ["*"]):
if not fnmatch.fnmatch(model_name.lower(), pat.lower()): if not fnmatch.fnmatch(model_name.lower(), pat.lower()):
print( self.log.debug(
"Server %r does not support %r (only %r.)" % ( "Server %r does not support %r (only %r.)"
_srv['name'], % (_srv["name"], model_name, ", ".join(_srv["allow"]))
model_name,
', '.join(_srv['allow'])
)
) )
else: else:
break break
@ -1951,9 +1865,7 @@ class OtherCog(commands.Cog):
class ServerSelector(discord.ui.View): class ServerSelector(discord.ui.View):
def __init__(self): def __init__(self):
super().__init__( super().__init__(disable_on_timeout=True)
disable_on_timeout=True
)
self.chosen_server = None self.chosen_server = None
async def interaction_check(self, interaction: Interaction) -> bool: async def interaction_check(self, interaction: Interaction) -> bool:
@ -1963,21 +1875,15 @@ class OtherCog(commands.Cog):
placeholder="Choose a server.", placeholder="Choose a server.",
custom_id="select", custom_id="select",
options=[ options=[
discord.SelectOption( discord.SelectOption(label="%s (%s)" % (y["name"], x), value=x)
label="%s (%s)" % (y['name'], x),
value=x
)
for x, y in servers.items() for x, y in servers.items()
if model_is_allowed(model, y) if model_is_allowed(model, y)
] + [
discord.SelectOption(
label="Custom",
value="custom"
)
] ]
+ [discord.SelectOption(label="Custom", value="custom")],
) )
async def select_callback(self, item: discord.ui.Select, interaction: discord.Interaction): async def select_callback(self, item: discord.ui.Select, interaction: discord.Interaction):
if item.values[0] == "custom": if item.values[0] == "custom":
class ServerSelectionModal(discord.ui.Modal): class ServerSelectionModal(discord.ui.Modal):
def __init__(self): def __init__(self):
super().__init__( super().__init__(
@ -1994,10 +1900,10 @@ class OtherCog(commands.Cog):
min_length=2, min_length=2,
max_length=5, max_length=5,
style=discord.InputTextStyle.short, style=discord.InputTextStyle.short,
value="11434" value="11434",
), ),
title="Enter server details", title="Enter server details",
timeout=120 timeout=120,
) )
self.hostname = None self.hostname = None
self.port = None self.port = None
@ -2016,8 +1922,7 @@ class OtherCog(commands.Cog):
self.chosen_server = item.values[0] self.chosen_server = item.values[0]
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
await interaction.followup.send( await interaction.followup.send(
f"\N{white heavy check mark} Selected server {self.chosen_server}/", f"\N{white heavy check mark} Selected server {self.chosen_server}/", ephemeral=True
ephemeral=True
) )
self.stop() self.stop()
@ -2035,29 +1940,18 @@ class OtherCog(commands.Cog):
if not model_is_allowed(model, srv): if not model_is_allowed(model, srv):
return await ctx.respond( return await ctx.respond(
":x: <@{!s}> does not allow you to run that model on the server {!r}. You can, however, use" ":x: <@{!s}> does not allow you to run that model on the server {!r}. You can, however, use"
" any of the following: {}".format( " any of the following: {}".format(srv["owner"], srv["name"], ", ".join(srv.get("allow", ["*"])))
srv["owner"],
srv["name"],
", ".join(srv.get("allow", ["*"]))
)
) )
content = None content = None
embed = discord.Embed( embed = discord.Embed(colour=discord.Colour.greyple())
colour=discord.Colour.greyple()
)
embed.set_author( embed.set_author(
name=f"Loading {model}", name=f"Loading {model}",
url=f"http://{host}", url=f"http://{host}",
icon_url="https://cdn.discordapp.com/emojis/1101463077586735174.gif" 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
) )
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) msg = await ctx.respond(embed=embed, ephemeral=False)
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
@ -2074,19 +1968,15 @@ class OtherCog(commands.Cog):
error = "GET {0.url} HTTP {0.status_code}: {0.text}".format(e.response) error = "GET {0.url} HTTP {0.status_code}: {0.text}".format(e.response)
return await msg.edit( return await msg.edit(
embed=discord.Embed( embed=discord.Embed(
title="Failed to GET /tags. Offline?", title="Failed to GET /tags. Offline?", description=error, colour=discord.Colour.red()
description=error,
colour=discord.Colour.red()
).set_footer(text=FOOTER_TEXT) ).set_footer(text=FOOTER_TEXT)
) )
except httpx.TransportError as e: except httpx.TransportError as e:
return await msg.edit( return await msg.edit(
embed=discord.Embed( embed=discord.Embed(
title=f"Failed to connect to {host!r}", title=f"Failed to connect to {host!r}",
description="Transport error sending request to {}: {}".format( description="Transport error sending request to {}: {}".format(host, str(e)),
host, str(e) colour=discord.Colour.red(),
),
colour=discord.Colour.red()
).set_footer(text=FOOTER_TEXT) ).set_footer(text=FOOTER_TEXT)
) )
# get models # get models
@ -2094,9 +1984,7 @@ class OtherCog(commands.Cog):
response = await client.post("/show", json={"name": model}) response = await client.post("/show", json={"name": model})
except httpx.TransportError as e: except httpx.TransportError as e:
embed = discord.Embed( embed = discord.Embed(
title="Failed to connect to Ollama.", title="Failed to connect to Ollama.", description=str(e), colour=discord.Colour.red()
description=str(e),
colour=discord.Colour.red()
) )
embed.set_footer(text=FOOTER_TEXT) embed.set_footer(text=FOOTER_TEXT)
return await msg.edit(embed=embed) return await msg.edit(embed=embed)
@ -2105,10 +1993,7 @@ class OtherCog(commands.Cog):
await msg.edit(embed=embed) await msg.edit(embed=embed)
async with ctx.channel.typing(): async with ctx.channel.typing():
async with client.stream( async with client.stream(
"POST", "POST", "/pull", json={"name": model, "stream": True}, timeout=None
"/pull",
json={"name": model, "stream": True},
timeout=None
) as response: ) as response:
if response.status_code != 200: if response.status_code != 200:
error = await response.aread() error = await response.aread()
@ -2116,7 +2001,7 @@ class OtherCog(commands.Cog):
title=f"Failed to download model {model}:", title=f"Failed to download model {model}:",
description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```", description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```",
colour=discord.Colour.red(), colour=discord.Colour.red(),
url=str(response.url) url=str(response.url),
) )
embed.set_footer(text=FOOTER_TEXT) embed.set_footer(text=FOOTER_TEXT)
return await msg.edit(embed=embed) return await msg.edit(embed=embed)
@ -2131,12 +2016,13 @@ class OtherCog(commands.Cog):
percent = round(completed / total * 100, 2) percent = round(completed / total * 100, 2)
total_gigabytes = total / 1024 / 1024 / 1024 total_gigabytes = total / 1024 / 1024 / 1024
completed_gigabytes = completed / 1024 / 1024 / 1024 completed_gigabytes = completed / 1024 / 1024 / 1024
lines[chunk["status"]] = (f"{percent}% " lines[chunk["status"]] = (
f"({completed_gigabytes:.2f}GB/{total_gigabytes:.2f}GB)") f"{percent}% " f"({completed_gigabytes:.2f}GB/{total_gigabytes:.2f}GB)"
)
else: else:
status = chunk.get("status", chunk.get("error", os.urandom(3).hex())) status = chunk.get("status", chunk.get("error", os.urandom(3).hex()))
lines[status] = status lines[status] = status
embed.description = "\n".join(f"`{k}`: {v}" for k, v in lines.items()) embed.description = "\n".join(f"`{k}`: {v}" for k, v in lines.items())
if (time() - last_edit) >= 5: if (time() - last_edit) >= 5:
await msg.edit(embed=embed) await msg.edit(embed=embed)
@ -2151,7 +2037,7 @@ class OtherCog(commands.Cog):
title=f"Failed to download model {model}:", title=f"Failed to download model {model}:",
description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```", description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```",
colour=discord.Colour.red(), colour=discord.Colour.red(),
url=str(response.url) url=str(response.url),
) )
embed.set_footer(text=FOOTER_TEXT) embed.set_footer(text=FOOTER_TEXT)
return await msg.edit(embed=embed) return await msg.edit(embed=embed)
@ -2160,32 +2046,21 @@ class OtherCog(commands.Cog):
title=f"{model} says:", title=f"{model} says:",
description="", description="",
colour=discord.Colour.blurple(), colour=discord.Colour.blurple(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text=FOOTER_TEXT) embed.set_footer(text=FOOTER_TEXT)
await msg.edit(embed=embed) await msg.edit(embed=embed)
async with ctx.channel.typing(): async with ctx.channel.typing():
payload = { payload = {"model": model, "prompt": query, "format": "json", "system": system_prompt, "stream": True}
"model": model,
"prompt": query,
"format": "json",
"system": system_prompt,
"stream": True
}
if context: if context:
payload["context"] = context payload["context"] = context
async with client.stream( async with client.stream("POST", "/generate", json=payload, timeout=None) as response:
"POST",
"/generate",
json=payload,
timeout=None
) as response:
if response.status_code != 200: if response.status_code != 200:
error = await response.aread() error = await response.aread()
embed = discord.Embed( embed = discord.Embed(
title=f"Failed to generate response from {model}:", title=f"Failed to generate response from {model}:",
description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```", description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```",
colour=discord.Colour.red() colour=discord.Colour.red(),
) )
embed.set_footer(text=FOOTER_TEXT) embed.set_footer(text=FOOTER_TEXT)
return await msg.edit(embed=embed) return await msg.edit(embed=embed)
@ -2204,7 +2079,7 @@ class OtherCog(commands.Cog):
embed.set_author( embed.set_author(
name=f"Generating response with {model}", name=f"Generating response with {model}",
url=f"http://{host}", url=f"http://{host}",
icon_url="https://cdn.discordapp.com/emojis/1101463077586735174.gif" icon_url="https://cdn.discordapp.com/emojis/1101463077586735174.gif",
) )
embed.description += chunk["response"] embed.description += chunk["response"]
if (time() - last_edit) >= 5 or chunk["done"] is True: if (time() - last_edit) >= 5 or chunk["done"] is True:
@ -2215,10 +2090,7 @@ class OtherCog(commands.Cog):
embed.colour = discord.Colour.red() embed.colour = discord.Colour.red()
return await msg.edit(embed=embed, view=None) return await msg.edit(embed=embed, view=None)
if len(embed.description) >= 4000: if len(embed.description) >= 4000:
embed.add_field( embed.add_field(name="Aborting early", value="Output exceeded 4000 characters.")
name="Aborting early",
value="Output exceeded 4000 characters."
)
embed.title = embed.title[:-1] + " (Aborted)" embed.title = embed.title[:-1] + " (Aborted)"
embed.colour = discord.Colour.red() embed.colour = discord.Colour.red()
embed.description = embed.description[:4096] embed.description = embed.description[:4096]
@ -2268,28 +2140,22 @@ class OtherCog(commands.Cog):
self.context_cache[key] = context self.context_cache[key] = context
else: else:
context = key = None context = key = None
value = ("* Total: {}\n" value = (
"* Model load: {}\n" "* Total: {}\n"
"* Sample generation: {}\n" "* Model load: {}\n"
"* Prompt eval: {}\n" "* Sample generation: {}\n"
"* Response generation: {}\n").format( "* Prompt eval: {}\n"
"* Response generation: {}\n"
).format(
total_time_spent, total_time_spent,
load_time_spent, load_time_spent,
sample_time_sent, sample_time_sent,
prompt_eval_time_spent, prompt_eval_time_spent,
eval_time_spent, eval_time_spent,
) )
embed.add_field( embed.add_field(name="Timings", value=value, inline=False)
name="Timings",
value=value,
inline=False
)
if context: if context:
embed.add_field( embed.add_field(name="Context Key", value=key, inline=True)
name="Context Key",
value=key,
inline=True
)
embed.set_footer(text=FOOTER_TEXT) embed.set_footer(text=FOOTER_TEXT)
await msg.edit(content=None, embed=embed, view=None) await msg.edit(content=None, embed=embed, view=None)
self.ollama_locks.pop(msg, None) self.ollama_locks.pop(msg, None)
@ -2297,44 +2163,31 @@ class OtherCog(commands.Cog):
@commands.slash_command(name="test-proxies") @commands.slash_command(name="test-proxies")
@commands.max_concurrency(1, commands.BucketType.user, wait=False) @commands.max_concurrency(1, commands.BucketType.user, wait=False)
async def test_proxy( async def test_proxy(
self, self,
ctx: discord.ApplicationContext, ctx: discord.ApplicationContext,
run_speed_test: bool = False, run_speed_test: bool = False,
proxy_name: discord.Option( proxy_name: discord.Option(str, choices=["SHRoNK", "NexBox", "first-working"]) = "first-working",
str, test_time: float = 30,
choices=[
"SHRoNK",
"NexBox",
"first-working"
]
) = "first-working",
test_time: float = 30
): ):
"""Tests proxies.""" """Tests proxies."""
test_time = max(5.0, test_time) test_time = max(5.0, test_time)
await ctx.defer() await ctx.defer()
SPEED_REGIONS = [ SPEED_REGIONS = ["fsn1", "nbg1", "hel1", "ash", "hil"]
"fsn1",
"nbg1",
"hel1",
"ash",
"hil"
]
results = { results = {
"localhost:1090": { "localhost:1090": {
"name": "SHRoNK", "name": "SHRoNK",
"failure": None, "failure": None,
"download_speed": 0.0, "download_speed": 0.0,
"tested": False, "tested": False,
"speedtest": "https://{hetzner_region}-speed.hetzner.com/100M.bin" "speedtest": "https://{hetzner_region}-speed.hetzner.com/100M.bin",
}, },
"localhost:1080": { "localhost:1080": {
"name": "NexBox", "name": "NexBox",
"failure": None, "failure": None,
"download_speed": 0.0, "download_speed": 0.0,
"tested": False, "tested": False,
"speedtest": "http://192.168.0.90:82/100M.bin" "speedtest": "http://192.168.0.90:82/100M.bin",
} },
} }
if proxy_name != "first-working": if proxy_name != "first-working":
for key, value in results.copy().items(): for key, value in results.copy().items():
@ -2342,18 +2195,13 @@ class OtherCog(commands.Cog):
continue continue
else: else:
results.pop(key) results.pop(key)
embed = discord.Embed( embed = discord.Embed(title="\N{white heavy check mark} Proxy available.")
title="\N{white heavy check mark} Proxy available."
)
FAILED = False FAILED = False
proxy_uri = None proxy_uri = None
for proxy_uri in results.keys(): for proxy_uri in results.keys():
name = results[proxy_uri]["name"] name = results[proxy_uri]["name"]
try: try:
proxy_down = await asyncio.wait_for( proxy_down = await asyncio.wait_for(self.check_proxy("socks5://" + proxy_uri), timeout=10)
self.check_proxy("socks5://" + proxy_uri),
timeout=10
)
results[proxy_uri]["tested"] = True results[proxy_uri]["tested"] = True
if proxy_down > 0: if proxy_down > 0:
embed.colour = discord.Colour.red() embed.colour = discord.Colour.red()
@ -2375,19 +2223,12 @@ class OtherCog(commands.Cog):
results[proxy_uri]["failure"] = f"Failed to check {name} proxy (`{e}`)." results[proxy_uri]["failure"] = f"Failed to check {name} proxy (`{e}`)."
results[proxy_uri]["tested"] = True results[proxy_uri]["tested"] = True
else: else:
embed = discord.Embed( embed = discord.Embed(title="\N{cross mark} All proxies failed.", colour=discord.Colour.red())
title="\N{cross mark} All proxies failed.",
colour=discord.Colour.red()
)
FAILED = True FAILED = True
for uri, value in results.items(): for uri, value in results.items():
if value["tested"]: if value["tested"]:
embed.add_field( embed.add_field(name=value["name"], value=value["failure"] or "Proxy is working.", inline=True)
name=value["name"],
value=value["failure"] or "Proxy is working.",
inline=True
)
embed.set_footer(text="No speed test will be run.") embed.set_footer(text="No speed test will be run.")
await ctx.respond(embed=embed) await ctx.respond(embed=embed)
if run_speed_test and FAILED is False: if run_speed_test and FAILED is False:
@ -2396,11 +2237,9 @@ class OtherCog(commands.Cog):
await ctx.edit(embed=embed) await ctx.edit(embed=embed)
chosen_proxy = ("socks5://" + proxy_uri) if proxy_uri else None chosen_proxy = ("socks5://" + proxy_uri) if proxy_uri else None
async with httpx.AsyncClient( async with httpx.AsyncClient(
http2=True, http2=True,
proxies=chosen_proxy, proxies=chosen_proxy,
headers={ headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0"},
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0"
}
) as client: ) as client:
bytes_received = 0 bytes_received = 0
for region in SPEED_REGIONS: for region in SPEED_REGIONS:
@ -2418,9 +2257,7 @@ class OtherCog(commands.Cog):
response.raise_for_status() response.raise_for_status()
end = time() end = time()
now = discord.utils.utcnow() now = discord.utils.utcnow()
embed.set_footer( embed.set_footer(text=embed.footer.text + " | Finished at: " + now.strftime("%X"))
text=embed.footer.text + " | Finished at: " + now.strftime("%X")
)
break break
except Exception as e: except Exception as e:
results[proxy_uri]["failure"] = f"Failed to test {region} speed (`{e}`)." results[proxy_uri]["failure"] = f"Failed to test {region} speed (`{e}`)."
@ -2433,8 +2270,8 @@ class OtherCog(commands.Cog):
embed2 = discord.Embed( embed2 = discord.Embed(
title=f"\U000023f2\U0000fe0f Speed test results (for {proxy_uri})", title=f"\U000023f2\U0000fe0f Speed test results (for {proxy_uri})",
description=f"Downloaded {megabytes:,.1f}MB in {elapsed:,.0f} seconds " description=f"Downloaded {megabytes:,.1f}MB in {elapsed:,.0f} seconds "
f"({megabits_per_second:,.0f}Mbps).\n`{latency:,.0f}ms` latency.", f"({megabits_per_second:,.0f}Mbps).\n`{latency:,.0f}ms` latency.",
colour=discord.Colour.green() if megabits_per_second >= 50 else discord.Colour.red() colour=discord.Colour.green() if megabits_per_second >= 50 else discord.Colour.red(),
) )
embed2.add_field(name="Source", value=used) embed2.add_field(name="Source", value=used)
await ctx.edit(embeds=[embed, embed2]) await ctx.edit(embeds=[embed, embed2])
@ -2442,61 +2279,97 @@ class OtherCog(commands.Cog):
@commands.message_command(name="Transcribe") @commands.message_command(name="Transcribe")
async def transcribe_message(self, ctx: discord.ApplicationContext, message: discord.Message): async def transcribe_message(self, ctx: discord.ApplicationContext, message: discord.Message):
await ctx.defer() await ctx.defer()
if not message.attachments: async with self.transcribe_lock:
return await ctx.respond("No attachments found.") if not message.attachments:
return await ctx.respond("No attachments found.")
_ft = "wav" _ft = "wav"
for attachment in message.attachments: for attachment in message.attachments:
if attachment.content_type.startswith("audio/"): if attachment.content_type.startswith("audio/"):
_ft = attachment.filename.split(".")[-1] _ft = attachment.filename.split(".")[-1]
break break
else: else:
return await ctx.respond("No voice messages.") return await ctx.respond("No voice messages.")
if getattr(config, "OPENAI_KEY", None) is None: if getattr(config, "OPENAI_KEY", None) is None:
return await ctx.respond("Service unavailable.") return await ctx.respond("Service unavailable.")
file_hash = hashlib.sha1(usedforsecurity=False) file_hash = hashlib.sha1(usedforsecurity=False)
file_hash.update(await attachment.read()) file_hash.update(await attachment.read())
file_hash = file_hash.hexdigest() file_hash = file_hash.hexdigest()
cache = Path.home() / ".cache" / "lcc-bot" / ("%s-transcript.txt" % file_hash) cache = Path.home() / ".cache" / "lcc-bot" / ("%s-transcript.txt" % file_hash)
cached = False cached = False
if not cache.exists(): if not cache.exists():
client = openai.OpenAI(api_key=config.OPENAI_KEY) client = openai.OpenAI(api_key=config.OPENAI_KEY)
with tempfile.NamedTemporaryFile("wb+", suffix=".mp4") as f: with tempfile.NamedTemporaryFile("wb+", suffix=".mp4") as f:
with tempfile.NamedTemporaryFile("wb+", suffix="-" + attachment.filename) as f2: with tempfile.NamedTemporaryFile("wb+", suffix="-" + attachment.filename) as f2:
await attachment.save(f2.name) await attachment.save(f2.name)
f2.seek(0) f2.seek(0)
seg: pydub.AudioSegment = await asyncio.to_thread(pydub.AudioSegment.from_file, file=f2, format=_ft) seg: pydub.AudioSegment = await asyncio.to_thread(pydub.AudioSegment.from_file, file=f2, format=_ft)
seg = seg.set_channels(1) seg = seg.set_channels(1)
await asyncio.to_thread( await asyncio.to_thread(seg.export, f.name, format="mp4")
seg.export, f.name, format="mp4" f.seek(0)
transcript = await asyncio.to_thread(
client.audio.transcriptions.create, file=pathlib.Path(f.name), model="whisper-1"
) )
f.seek(0) text = transcript.text
cache.write_text(text)
else:
text = cache.read_text()
cached = True
transcript = await asyncio.to_thread( paginator = commands.Paginator("", "", 4096)
client.audio.transcriptions.create, for line in text.splitlines():
file=pathlib.Path(f.name), paginator.add_line(textwrap.shorten(line, 4096))
model="whisper-1" embeds = list(map(lambda p: discord.Embed(description=p), paginator.pages))
await ctx.respond(embeds=embeds or [discord.Embed(description="No text found.")])
if await self.bot.is_owner(ctx.user):
await ctx.respond(
("Cached response ({})" if cached else "Uncached response ({})").format(file_hash), ephemeral=True
) )
text = transcript.text
cache.write_text(text)
else:
text = cache.read_text()
cached = True
paginator = commands.Paginator("", "", 4096) @commands.slash_command()
for line in text.splitlines(): async def whois(self, ctx: discord.ApplicationContext, domain: str):
paginator.add_line(textwrap.shorten(line, 4096)) """Runs a WHOIS."""
embeds = list(map(lambda p: discord.Embed(description=p), paginator.pages)) await ctx.defer()
await ctx.respond(embeds=embeds or [discord.Embed(description="No text found.")]) url = urlparse("http://" + domain)
if not url.hostname:
if await self.bot.is_owner(ctx.user): return await ctx.respond("Invalid domain.")
await ctx.respond(
("Cached response ({})" if cached else "Uncached response ({})").format( process = await asyncio.create_subprocess_exec(
file_hash "whois", "-H", url.hostname, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
), )
ephemeral=True 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): def setup(bot):

View file

@ -1,13 +1,14 @@
import json import json
import logging
import random import random
from datetime import datetime, time, timedelta, timezone from datetime import datetime, time, timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Dict, Optional, Union from typing import Dict, Optional, Union
import config
import discord import discord
from discord.ext import commands, tasks from discord.ext import commands, tasks
import config
from utils import TimeTableDaySwitcherView, console from utils import TimeTableDaySwitcherView, console
@ -22,6 +23,7 @@ def schedule_times():
class TimeTableCog(commands.Cog): class TimeTableCog(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.log = logging.getLogger("jimmy.cogs.timetable")
with (Path.cwd() / "utils" / "timetable.json").open() as file: with (Path.cwd() / "utils" / "timetable.json").open() as file:
self.timetable = json.load(file) self.timetable = json.load(file)
self.update_status.start() self.update_status.start()
@ -159,7 +161,7 @@ class TimeTableCog(commands.Cog):
try: try:
next_lesson = self.absolute_next_lesson(date + timedelta(days=1)) next_lesson = self.absolute_next_lesson(date + timedelta(days=1))
except RuntimeError: except RuntimeError:
print("Failed to fetch absolute next lesson. Is this the end?") self.log.critical("Failed to fetch absolute next lesson. Is this the end?")
return return
next_lesson.setdefault("name", "unknown") next_lesson.setdefault("name", "unknown")
next_lesson.setdefault("tutor", "unknown") next_lesson.setdefault("tutor", "unknown")

View file

@ -2,6 +2,7 @@ import asyncio
import datetime import datetime
import hashlib import hashlib
import json import json
import logging
import random import random
import time import time
from datetime import timedelta from datetime import timedelta
@ -66,15 +67,17 @@ class UptimeCompetition(commands.Cog):
def __init__(self, bot: commands.Bot): def __init__(self, bot: commands.Bot):
self.bot = bot self.bot = bot
self.http = AsyncClient(verify=False) self.log = logging.getLogger("jimmy.cogs.uptime")
self.http = AsyncClient(verify=False, http2=True)
self._warning_posted = False self._warning_posted = False
self.test_uptimes.add_exception_type(Exception) self.test_uptimes.add_exception_type(Exception)
self.test_uptimes.start() self.test_uptimes.start()
self.last_result: list[UptimeEntry] = [] self.last_result: list[UptimeEntry] = []
self.task_lock = asyncio.Lock() self.task_lock = asyncio.Lock()
self.task_event = asyncio.Event() self.task_event = asyncio.Event()
if not (pth := Path("targets.json")).exists(): self.path = Path.home() / ".cache" / "lcc-bot" / "targets.json"
pth.write_text(BASE_JSON) if not self.path.exists():
self.path.write_text(BASE_JSON)
self._cached_targets = self.read_targets() self._cached_targets = self.read_targets()
@property @property
@ -103,7 +106,7 @@ class UptimeCompetition(commands.Cog):
return response return response
def read_targets(self) -> List[Dict[str, str]]: def read_targets(self) -> List[Dict[str, str]]:
with open("targets.json") as f: with self.path.open() as f:
data: list = json.load(f) data: list = json.load(f)
data.sort(key=lambda x: x["name"]) data.sort(key=lambda x: x["name"])
self._cached_targets = data.copy() self._cached_targets = data.copy()
@ -111,7 +114,7 @@ class UptimeCompetition(commands.Cog):
def write_targets(self, data: List[Dict[str, str]]): def write_targets(self, data: List[Dict[str, str]]):
self._cached_targets = data self._cached_targets = data
with open("targets.json", "w") as f: with self.path.open("w") as f:
json.dump(data, f, indent=4, default=str) json.dump(data, f, indent=4, default=str)
def cog_unload(self): def cog_unload(self):
@ -214,7 +217,7 @@ class UptimeCompetition(commands.Cog):
okay_statuses = list(filter(None, okay_statuses)) okay_statuses = list(filter(None, okay_statuses))
guild: discord.Guild = self.bot.get_guild(guild_id) guild: discord.Guild = self.bot.get_guild(guild_id)
if guild is None: if guild is None:
console.log( self.log.warning(
f"[yellow]:warning: Unable to locate the guild for {target['name']!r}! Can't uptime check." f"[yellow]:warning: Unable to locate the guild for {target['name']!r}! Can't uptime check."
) )
else: else:
@ -224,7 +227,7 @@ class UptimeCompetition(commands.Cog):
try: try:
user = await guild.fetch_member(user_id) user = await guild.fetch_member(user_id)
except discord.HTTPException: except discord.HTTPException:
console.log(f"[yellow]:warning: Unable to locate {target['name']!r}! Can't uptime check.") self.log.warning(f"[yellow]Unable to locate {target['name']!r}! Can't uptime check.")
user = None user = None
if user: if user:
create_tasks.append( create_tasks.append(
@ -240,7 +243,7 @@ class UptimeCompetition(commands.Cog):
) )
else: else:
if self._warning_posted is False: if self._warning_posted is False:
console.log( self.log.warning(
"[yellow]:warning: Jimmy does not have the presences intent enabled. Uptime monitoring of the" "[yellow]:warning: Jimmy does not have the presences intent enabled. Uptime monitoring of the"
" shronk bot is disabled." " shronk bot is disabled."
) )

62
main.py
View file

@ -1,28 +1,57 @@
import asyncio
import sys
import logging import logging
import os
import signal
import sys
import textwrap import textwrap
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import config
import discord import discord
from discord.ext import commands from discord.ext import commands
from rich.logging import RichHandler
import config
from utils import JimmyBanException, JimmyBans, console, get_or_none from utils import JimmyBanException, JimmyBans, console, get_or_none
from utils.client import bot from utils.client import bot
logging.basicConfig( logging.basicConfig(
filename="jimmy.log", format="%(asctime)s:%(levelname)s:%(name)s: %(message)s",
filemode="a",
format="%(asctime)s:%(level)s:%(name)s: %(message)s",
datefmt="%Y-%m-%d:%H:%M", datefmt="%Y-%m-%d:%H:%M",
level=logging.INFO level=logging.DEBUG,
force=True,
handlers=[
RichHandler(
getattr(config, "LOG_LEVEL", logging.INFO),
console=console,
markup=True,
rich_tracebacks=True,
show_path=False,
show_time=False,
),
logging.FileHandler("jimmy.log", "a"),
],
) )
logging.getLogger("discord.gateway").setLevel(logging.WARNING)
for _ln in [
"discord.client",
"httpcore.connection",
"httpcore.http2",
"hpack.hpack",
"discord.http",
"discord.bot",
"httpcore.http11",
"aiosqlite",
"httpx",
]:
logging.getLogger(_ln).setLevel(logging.INFO)
if os.name != "nt":
signal.signal(signal.SIGTERM, lambda: bot.loop.run_until_complete(bot.close()))
@bot.listen() @bot.listen()
async def on_connect(): async def on_connect():
console.log("[green]Connected to discord!") bot.log.info("[green]Connected to discord!")
@bot.listen("on_application_command_error") @bot.listen("on_application_command_error")
@ -60,13 +89,13 @@ async def on_command_error(ctx: commands.Context, error: Exception):
return return
elif isinstance(error, JimmyBanException): elif isinstance(error, JimmyBanException):
return await ctx.reply(str(error)) return await ctx.reply(str(error))
await ctx.reply("Command Error: `%r`" % error) await ctx.reply(textwrap.shorten("Command Error: `%r`" % error, 2000))
raise error raise error
@bot.listen("on_application_command") @bot.listen("on_application_command")
async def on_application_command(ctx: discord.ApplicationContext): async def on_application_command(ctx: discord.ApplicationContext):
console.log( bot.log.info(
"{0.author} ({0.author.id}) used application command /{0.command.qualified_name} in " "{0.author} ({0.author.id}) used application command /{0.command.qualified_name} in "
"[blue]#{0.channel}[/], {0.guild}".format(ctx) "[blue]#{0.channel}[/], {0.guild}".format(ctx)
) )
@ -74,9 +103,9 @@ async def on_application_command(ctx: discord.ApplicationContext):
@bot.event @bot.event
async def on_ready(): async def on_ready():
console.log("Logged in as", bot.user) bot.log.info("(READY) Logged in as %r", bot.user)
if getattr(config, "CONNECT_MODE", None) == 1: if getattr(config, "CONNECT_MODE", None) == 1:
console.log("Bot is now ready and exit target 1 is set, shutting down.") bot.log.critical("Bot is now ready and exit target 1 is set, shutting down.")
await bot.close() await bot.close()
sys.exit(0) sys.exit(0)
@ -105,10 +134,11 @@ async def check_not_banned(ctx: discord.ApplicationContext | commands.Context):
if __name__ == "__main__": if __name__ == "__main__":
console.log("Starting...") bot.log.info("Starting...")
bot.started_at = discord.utils.utcnow() bot.started_at = discord.utils.utcnow()
if getattr(config, "WEB_SERVER", True): if getattr(config, "WEB_SERVER", True):
bot.log.info("Web server is enabled (WEB_SERVER=True in config.py), initialising.")
import uvicorn import uvicorn
from web.server import app from web.server import app
@ -119,11 +149,11 @@ if __name__ == "__main__":
app, app,
host=getattr(config, "HTTP_HOST", "127.0.0.1"), host=getattr(config, "HTTP_HOST", "127.0.0.1"),
port=getattr(config, "HTTP_PORT", 3762), port=getattr(config, "HTTP_PORT", 3762),
loop="asyncio",
**getattr(config, "UVICORN_CONFIG", {}), **getattr(config, "UVICORN_CONFIG", {}),
) )
bot.log.info("Web server will listen on %s:%s", http_config.host, http_config.port)
server = uvicorn.Server(http_config) server = uvicorn.Server(http_config)
console.log("Starting web server...") bot.log.info("Starting web server...")
loop = bot.loop loop = bot.loop
http_server_task = loop.create_task(server.serve()) http_server_task = loop.create_task(server.serve())
bot.web = { bot.web = {
@ -131,5 +161,5 @@ if __name__ == "__main__":
"config": http_config, "config": http_config,
"task": http_server_task, "task": http_server_task,
} }
bot.log.info("Beginning main loop.")
bot.run(config.token) bot.run(config.token)

View file

@ -4,7 +4,6 @@ from urllib.parse import urlparse
from discord.ext import commands from discord.ext import commands
from ._email import *
from .console import * from .console import *
from .db import * from .db import *
from .views import * from .views import *

View file

@ -1,52 +0,0 @@
import secrets
from email.message import EmailMessage
import aiosmtplib as smtp
import config
import discord
gmail_cfg = {"addr": "smtp.gmail.com", "username": config.email, "password": config.email_password, "port": 465}
TOKEN_LENGTH = 16
class _FakeUser:
def __init__(self):
with open("/etc/dictionaries-common/words") as file:
names = file.readlines()
names = [x.strip() for x in names if not x.strip().endswith("'s")]
self.names = names
def __str__(self):
import random
return f"{random.choice(self.names)}#{str(random.randint(1, 9999)).zfill(4)}"
async def send_verification_code(user: discord.User, student_number: str, **kwargs) -> str:
"""Sends a verification code, returning said verification code, to the student."""
code = secrets.token_hex(TOKEN_LENGTH)
text = (
f"Hey {user} ({student_number})! The code to join Unscrupulous Nonsense is '{code}'.\n\n"
f"Go back to the #verify channel, and click 'I have a verification code!', and put {code} in the modal"
f" that pops up\n\n"
f"If you have any issues getting in, feel free to reply to this email, or DM eek#7574.\n"
f"~Nex\n\n\n"
f"(P.S you can now go to http://droplet.nexy7574.co.uk/jimmy/verify/{code} instead)"
)
msg = EmailMessage()
msg["From"] = msg["bcc"] = "B593764@my.leedscitycollege.ac.uk"
msg["To"] = f"{student_number}@my.leedscitycollege.ac.uk"
msg["Subject"] = "Server Verification"
msg.set_content(text)
kwargs.setdefault("hostname", gmail_cfg["addr"])
kwargs.setdefault("port", gmail_cfg["port"])
kwargs.setdefault("use_tls", True)
kwargs.setdefault("username", gmail_cfg["username"])
kwargs.setdefault("password", gmail_cfg["password"])
kwargs.setdefault("start_tls", not kwargs["use_tls"])
assert kwargs["start_tls"] != kwargs["use_tls"]
await smtp.send(msg, **kwargs)
return code

View file

@ -1,14 +1,16 @@
import asyncio import asyncio
import logging
import sys import sys
from asyncio import Lock from asyncio import Lock
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Dict, Optional, Union from typing import TYPE_CHECKING, Dict, Optional, Union
import config
import discord import discord
from discord.ext import commands from discord.ext import commands
import config
if TYPE_CHECKING: if TYPE_CHECKING:
from asyncio import Task from asyncio import Task
@ -30,37 +32,41 @@ class Bot(commands.Bot):
super().__init__( super().__init__(
command_prefix=commands.when_mentioned_or(*prefixes), command_prefix=commands.when_mentioned_or(*prefixes),
debug_guilds=guilds, debug_guilds=guilds,
allowed_mentions=discord.AllowedMentions.none(), allowed_mentions=discord.AllowedMentions(everyone=False, users=True, roles=False, replied_user=True),
intents=intents, intents=intents,
max_messages=5000, max_messages=5000,
case_insensitive=True, case_insensitive=True,
) )
self.loop.run_until_complete(registry.create_all()) self.loop.run_until_complete(registry.create_all())
self.training_lock = Lock() self.training_lock = Lock()
self.started_at = datetime.now(tz=timezone.utc) self.started_at = discord.utils.utcnow()
self.console = console self.console = console
self.incidents = {} self.log = log = logging.getLogger("jimmy.client")
self.debug = log.debug
self.info = log.info
self.warning = self.warn = log.warning
self.error = self.log.error
self.critical = self.log.critical
for ext in extensions: for ext in extensions:
try: try:
self.load_extension(ext) self.load_extension(ext)
except discord.ExtensionNotFound: except discord.ExtensionNotFound:
console.log(f"[red]Failed to load extension {ext}: Extension not found.") log.error(f"[red]Failed to load extension {ext}: Extension not found.")
except (discord.ExtensionFailed, OSError) as e: except (discord.ExtensionFailed, OSError) as e:
console.log(f"[red]Failed to load extension {ext}: {e}") log.error(f"[red]Failed to load extension {ext}: {e}", exc_info=True)
if getattr(config, "dev", False):
console.print_exception()
else: else:
console.log(f"Loaded extension [green]{ext}") log.info(f"Loaded extension [green]{ext}")
if getattr(config, "CONNECT_MODE", None) == 2: if getattr(config, "CONNECT_MODE", None) == 2:
async def connect(self, *, reconnect: bool = True) -> None: async def connect(self, *, reconnect: bool = True) -> None:
self.console.log("Exit target 2 reached, shutting down (not connecting to discord).") self.log.critical("Exit target 2 reached, shutting down (not connecting to discord).")
return return
async def on_error(self, event: str, *args, **kwargs): async def on_error(self, event: str, *args, **kwargs):
e_type, e, tb = sys.exc_info() e_type, e, tb = sys.exc_info()
if isinstance(e, discord.NotFound) and e.code == 10062: # invalid interaction if isinstance(e, discord.NotFound) and e.code == 10062: # invalid interaction
self.log.warning(f"Invalid interaction received, ignoring. {e!r}")
return return
if isinstance(e, discord.CheckFailure) and "The global check once functions failed." in str(e): if isinstance(e, discord.CheckFailure) and "The global check once functions failed." in str(e):
return return
@ -69,25 +75,27 @@ class Bot(commands.Bot):
async def close(self) -> None: async def close(self) -> None:
await self.http.close() await self.http.close()
if getattr(self, "web", None) is not None: if getattr(self, "web", None) is not None:
self.console.log("Closing web server...") self.log.info("Closing web server...")
await self.web["server"].shutdown() try:
if hasattr(self, "web"): await asyncio.wait_for(self.web["server"].shutdown(), timeout=5)
self.web["task"].cancel() self.web["task"].cancel()
self.console.log("Web server closed.") self.console.log("Web server closed.")
try: try:
await self.web["task"] await asyncio.wait_for(self.web["task"], timeout=5)
except asyncio.CancelledError: except (asyncio.CancelledError, asyncio.TimeoutError):
pass pass
del self.web["server"] del self.web["server"]
del self.web["config"] del self.web["config"]
del self.web["task"] del self.web["task"]
del self.web del self.web
except asyncio.TimeoutError:
pass
try: try:
await super().close() await super().close()
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.console.log("Timed out while closing, forcing shutdown.") self.log.critical("Timed out while closing, forcing shutdown.")
sys.exit(1) sys.exit(1)
self.console.log("Finished shutting down.") self.log.info("Finished shutting down.")
try: try:

View file

@ -8,4 +8,4 @@ if _col == 80:
__all__ = ("console",) __all__ = ("console",)
console = Console(width=_col, soft_wrap=True, tab_size=4) console = Console(width=_col, soft_wrap=False, tab_size=4)

View file

@ -1,112 +0,0 @@
# I have NO idea how this works
# I copied it from the tutorial
# However it works
import random
import re
import string
from nltk import FreqDist, NaiveBayesClassifier, classify
from nltk.corpus import movie_reviews, stopwords, twitter_samples
from nltk.sentiment.vader import SentimentIntensityAnalyzer
from nltk.stem.wordnet import WordNetLemmatizer
from nltk.tag import pos_tag
positive_tweets = twitter_samples.strings("positive_tweets.json")
negative_tweets = twitter_samples.strings("negative_tweets.json")
positive_reviews = movie_reviews.categories("pos")
negative_reviews = movie_reviews.categories("neg")
positive_tweets += positive_reviews
# negative_tweets += negative_reviews
positive_tweet_tokens = twitter_samples.tokenized("positive_tweets.json")
negative_tweet_tokens = twitter_samples.tokenized("negative_tweets.json")
text = twitter_samples.strings("tweets.20150430-223406.json")
tweet_tokens = twitter_samples.tokenized("positive_tweets.json")
stop_words = stopwords.words("english")
def lemmatize_sentence(_tokens):
lemmatizer = WordNetLemmatizer()
lemmatized_sentence = []
for word, tag in pos_tag(_tokens):
if tag.startswith("NN"):
pos = "n"
elif tag.startswith("VB"):
pos = "v"
else:
pos = "a"
lemmatized_sentence.append(lemmatizer.lemmatize(word, pos))
return lemmatized_sentence
def remove_noise(_tweet_tokens, _stop_words=()):
cleaned_tokens = []
for token, tag in pos_tag(_tweet_tokens):
token = re.sub("https?://(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*(),]|" "%[0-9a-fA-F][0-9a-fA-F])+", "", token)
token = re.sub("(@[A-Za-z0-9_]+)", "", token)
if tag.startswith("NN"):
pos = "n"
elif tag.startswith("VB"):
pos = "v"
else:
pos = "a"
lemmatizer = WordNetLemmatizer()
token = lemmatizer.lemmatize(token, pos)
if len(token) > 0 and token not in string.punctuation and token.lower() not in _stop_words:
cleaned_tokens.append(token.lower())
return cleaned_tokens
positive_cleaned_tokens_list = []
negative_cleaned_tokens_list = []
for tokens in positive_tweet_tokens:
positive_cleaned_tokens_list.append(remove_noise(tokens, stop_words))
for tokens in negative_tweet_tokens:
negative_cleaned_tokens_list.append(remove_noise(tokens, stop_words))
def get_all_words(cleaned_tokens_list):
for _tokens in cleaned_tokens_list:
for token in _tokens:
yield token
all_pos_words = get_all_words(positive_cleaned_tokens_list)
freq_dist_pos = FreqDist(all_pos_words)
def get_tweets_for_model(cleaned_tokens_list):
for _tweet_tokens in cleaned_tokens_list:
yield {token: True for token in _tweet_tokens}
positive_tokens_for_model = get_tweets_for_model(positive_cleaned_tokens_list)
negative_tokens_for_model = get_tweets_for_model(negative_cleaned_tokens_list)
positive_dataset = [(tweet_dict, "Positive") for tweet_dict in positive_tokens_for_model]
negative_dataset = [(tweet_dict, "Negative") for tweet_dict in negative_tokens_for_model]
dataset = positive_dataset + negative_dataset
random.shuffle(dataset)
train_data = dataset[:7000]
test_data = dataset[7000:]
classifier = NaiveBayesClassifier.train(train_data)
intensity_analyser = SentimentIntensityAnalyzer()
if __name__ == "__main__":
while True:
try:
ex = input("> ")
except KeyboardInterrupt:
break
else:
print(classifier.classify({token: True for token in remove_noise(ex.split())}))
print(intensity_analyser.polarity_scores(ex))

View file

@ -8,15 +8,9 @@ import discord
import orm import orm
from discord.ui import View from discord.ui import View
from utils import ( from utils import BannedStudentID, Student, VerifyCode, console, get_or_none
TOKEN_LENGTH,
BannedStudentID, TOKEN_LENGTH = 16
Student,
VerifyCode,
console,
get_or_none,
send_verification_code,
)
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from cogs.timetable import TimeTableCog from cogs.timetable import TimeTableCog
@ -133,7 +127,8 @@ class VerifyView(View):
) )
try: try:
_code = await send_verification_code(interaction.user, st) # _code = await send_verification_code(interaction.user, st)
raise RuntimeError("Disabled.")
except Exception as e: except Exception as e:
return await interaction.followup.send(f"\N{cross mark} Failed to send email - {e}. Try again?") return await interaction.followup.send(f"\N{cross mark} Failed to send email - {e}. Try again?")
console.log(f"Sending verification email to {interaction.user} ({interaction.user.id}/{st})...") console.log(f"Sending verification email to {interaction.user} ({interaction.user.id}/{st})...")

View file

@ -1,9 +1,9 @@
import asyncio import asyncio
import ipaddress import ipaddress
import logging
import os import os
import sys
import textwrap import textwrap
from rich import print from asyncio import Lock
from datetime import datetime, timezone from datetime import datetime, timezone
from hashlib import sha512 from hashlib import sha512
from http import HTTPStatus from http import HTTPStatus
@ -11,13 +11,13 @@ from pathlib import Path
import discord import discord
import httpx import httpx
from config import guilds from fastapi import FastAPI, Header, HTTPException, Request
from asyncio import Lock from fastapi import WebSocketException as _WSException
from fastapi import FastAPI, Header, HTTPException, Request, WebSocketException as _WSException
from websockets.exceptions import WebSocketException
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from starlette.websockets import WebSocket, WebSocketDisconnect from starlette.websockets import WebSocket, WebSocketDisconnect
from websockets.exceptions import WebSocketException
from config import guilds
from utils import BannedStudentID, Student, VerifyCode, console, get_or_none from utils import BannedStudentID, Student, VerifyCode, console, get_or_none
from utils.db import AccessTokens from utils.db import AccessTokens
@ -37,7 +37,9 @@ try:
except ImportError: except ImportError:
WEB_ROOT_PATH = "" WEB_ROOT_PATH = ""
GENERAL = "https://ptb.discord.com/channels/994710566612500550/1018915342317277215/" log = logging.getLogger("jimmy.api")
GENERAL = "https://discord.com/channels/994710566612500550/"
OAUTH_ENABLED = OAUTH_ID and OAUTH_SECRET and OAUTH_REDIRECT_URI OAUTH_ENABLED = OAUTH_ID and OAUTH_SECRET and OAUTH_REDIRECT_URI
@ -87,7 +89,7 @@ async def authenticate(req: Request, code: str = None, state: str = None):
if not (code and state) or state not in app.state.states: if not (code and state) or state not in app.state.states:
value = os.urandom(4).hex() value = os.urandom(4).hex()
if value in app.state.states: if value in app.state.states:
print("Generated a state that already exists. Cleaning up", file=sys.stderr) log.warning("Generated a state that already exists. Cleaning up")
# remove any states older than 5 minutes # remove any states older than 5 minutes
removed = 0 removed = 0
for _value in list(app.state.states): for _value in list(app.state.states):
@ -95,17 +97,17 @@ async def authenticate(req: Request, code: str = None, state: str = None):
del app.state.states[_value] del app.state.states[_value]
removed += 1 removed += 1
value = os.urandom(4).hex() value = os.urandom(4).hex()
print(f"Removed {removed} states.", file=sys.stderr) log.warning(f"Removed {removed} old states.")
if value in app.state.states: if value in app.state.states:
print("Critical: Generated a state that already exists and could not free any slots.", file=sys.stderr) log.critical("Generated a state that already exists and could not free any slots.")
raise HTTPException( raise HTTPException(
HTTPStatus.SERVICE_UNAVAILABLE, HTTPStatus.SERVICE_UNAVAILABLE,
"Could not generate a state token (state container full, potential (D)DOS attack?). " "Could not generate a state token (state container full, potential (D)DOS attack?). "
"Please try again later.", "Please try again later.",
# Saying a suspected DDOS makes sense, there are 4,294,967,296 possible states, the likelyhood of a # Saying a suspected DDOS makes sense, there are 4,294,967,296 possible states, the likelyhood of a
# collision is 1 in 4,294,967,296. # collision is 1 in 4,294,967,296.
headers={"Retry-After": "300"}, headers={"Retry-After": "60"},
) )
app.state.states[value] = datetime.now() app.state.states[value] = datetime.now()
return RedirectResponse( return RedirectResponse(
@ -239,7 +241,7 @@ async def verify(code: str):
# And delete the code # And delete the code
await verify_code.delete() await verify_code.delete()
console.log(f"[green]{verify_code.bind} verified ({verify_code.bind}/{verify_code.student_id})") log.info(f"[green]{verify_code.bind} verified ({verify_code.bind}/{verify_code.student_id})")
return RedirectResponse(GENERAL, status_code=308) return RedirectResponse(GENERAL, status_code=308)
@ -293,12 +295,12 @@ async def bridge(req: Request):
@app.websocket("/bridge/recv") @app.websocket("/bridge/recv")
async def bridge_recv(ws: WebSocket, secret: str = Header(None)): async def bridge_recv(ws: WebSocket, secret: str = Header(None)):
await ws.accept() await ws.accept()
print("Websocket %r accepted.", ws) log.info("Websocket %s:%s accepted.", ws.client.host, ws.client.port)
if secret != app.state.bot.http.token: if secret != app.state.bot.http.token:
print("Closing websocket %r, invalid secret." % ws) log.warning("Closing websocket %r, invalid secret.", ws.client.host)
raise _WSException(code=1008, reason="Invalid Secret") raise _WSException(code=1008, reason="Invalid Secret")
if app.state.ws_connected.locked(): if app.state.ws_connected.locked():
print("Closing websocket %r, already connected." % ws) log.warning("Closing websocket %r, already connected." % ws)
raise _WSException(code=1008, reason="Already connected.") raise _WSException(code=1008, reason="Already connected.")
queue: asyncio.Queue = app.state.bot.bridge_queue queue: asyncio.Queue = app.state.bot.bridge_queue
@ -307,7 +309,7 @@ async def bridge_recv(ws: WebSocket, secret: str = Header(None)):
try: try:
await ws.send_json({"status": "ping"}) await ws.send_json({"status": "ping"})
except (WebSocketDisconnect, WebSocketException): except (WebSocketDisconnect, WebSocketException):
print("Websocket %r disconnected." % ws) log.info("Websocket %r disconnected.", ws)
break break
try: try:
@ -316,10 +318,10 @@ async def bridge_recv(ws: WebSocket, secret: str = Header(None)):
continue continue
try: try:
print("Sent data %r to websocket %r." % (data, ws))
await ws.send_json(data) await ws.send_json(data)
log.debug("Sent data %r to websocket %r.", data, ws)
except (WebSocketDisconnect, WebSocketException): except (WebSocketDisconnect, WebSocketException):
print("Websocket %r disconnected." % ws) log.info("Websocket %r disconnected." % ws)
break break
finally: finally:
queue.task_done() queue.task_done()