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 pydantic
from bs4 import BeautifulSoup
from config import guilds
from discord.ext import commands, pages, tasks
from config import guilds
from utils import Student, console, get_or_none
try:
@ -90,25 +90,11 @@ class Events(commands.Cog):
self.bot.bridge_queue = asyncio.Queue()
self.fetch_discord_atom_feed.start()
self.bridge_health = False
self.log = logging.getLogger("jimmy.cogs.events")
def cog_unload(self):
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")
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
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:
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")
# 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} (`{member}`, {f'{student.id}' if student else 'pending verification'})"
f"{LTR} {member.mention}"
)
@commands.Cog.listener()
@ -143,11 +130,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)
# 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} (`{member}`, {f'{student.id}' if student else 'pending verification'})"
f"{RTL} {member.mention}"
)
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
# noinspection PyUnresolvedReferences
console.log(
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)"
@ -269,7 +257,7 @@ class Events(commands.Cog):
_dc(voice),
)
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}"))
else:
self.bot.loop.create_task(
@ -331,15 +319,16 @@ class Events(commands.Cog):
return
if message.channel.name == "femboy-hole":
def generate_payload(_message: discord.Message) -> MessagePayload:
_payload = MessagePayload(
message_id=_message.id,
author=_message.author.name,
is_automated=_message.author.bot or _message.author.system,
avatar=_message.author.display_avatar.with_static_format("webp").with_size(512).url,
content=_message.content or '',
clean_content=str(_message.clean_content or ''),
at=_message.created_at.timestamp()
content=_message.content or "",
clean_content=str(_message.clean_content or ""),
at=_message.created_at.timestamp(),
)
for attachment in _message.attachments:
_payload.attachments.append(
@ -350,7 +339,7 @@ class Events(commands.Cog):
size=attachment.size,
width=attachment.width,
height=attachment.height,
content_type=attachment.content_type
content_type=attachment.content_type,
)
)
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)
except RecursionError:
_payload.reply_to = None
logging.warning(
"Failed to generate reply payload for message %s",
_message.id,
exc_info=True
)
logging.warning("Failed to generate reply payload for message %s", _message.id, exc_info=True)
return _payload
payload = generate_payload(message)
@ -448,7 +433,7 @@ class Events(commands.Cog):
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"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")
},
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"((cup\s)?of\s)?tea\W*$": {"file": discord.File(assets / "tea.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"^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"},
}
# Stop responding to any bots
@ -590,14 +577,14 @@ class Events(commands.Cog):
try:
response = await self.http.get("https://discordstatus.com/history.atom", headers=headers)
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
if response.status_code == 304:
return
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
with file.open("wb") as f:

View file

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

View file

@ -1,3 +1,6 @@
import logging
import typing
import discord
import httpx
from discord.ext import commands
@ -124,9 +127,27 @@ class InfoCog(commands.Cog):
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):
if OAUTH_REDIRECT_URI and OAUTH_ID:
bot.add_cog(InfoCog(bot))
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 functools
import glob
import hashlib
import io
import pathlib
import openai
import pydub
import json
import logging
import math
import os
import pathlib
import random
import re
import shutil
import hashlib
import subprocess
import sys
import tempfile
@ -32,7 +30,9 @@ import aiohttp
import discord
import dns.resolver
import httpx
import openai
import psutil
import pydub
import pytesseract
import pyttsx3
from discord import Interaction
@ -69,41 +69,19 @@ try:
VOICES = [x.id for x in _engine.getProperty("voices")]
del _engine
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
VOICES = []
# class OllamaStreamReader:
# 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 def ollama_stream_reader(response: httpx.Response) -> typing.AsyncGenerator[dict[str, str | int | bool], None]:
async for chunk in response.aiter_lines():
# Each line is a JSON string
try:
loaded = json.loads(chunk)
yield loaded
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
@ -134,6 +112,7 @@ class OtherCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.lock = asyncio.Lock()
self.transcribe_lock = asyncio.Lock()
self.http = httpx.AsyncClient()
self._fmt_cache = {}
self._fmt_queue = asyncio.Queue()
@ -141,6 +120,7 @@ class OtherCog(commands.Cog):
self.ollama_locks: dict[discord.Message, asyncio.Event] = {}
self.context_cache: dict[str, list[int]] = {}
self.log = logging.getLogger("jimmy.cogs.other")
def cog_unload(self):
self._worker_task.cancel()
@ -257,7 +237,7 @@ class OtherCog(commands.Cog):
return driver, driver_path
driver, driver_path = find_driver()
console.log(
self.log.info(
"Using driver '{}' with binary '{}' to screenshot '{}', as requested by {}.".format(
driver, driver_path, website, ctx.user
)
@ -295,7 +275,7 @@ class OtherCog(commands.Cog):
start_init = time()
driver, friendly_url = await asyncio.to_thread(_setup)
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):
self.bot.loop.create_task(ctx.interaction.edit_original_response(content=content))
@ -349,20 +329,6 @@ class OtherCog(commands.Cog):
)
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
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:
@ -445,40 +411,6 @@ class OtherCog(commands.Cog):
view = self.XKCDGalleryView(number)
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(
name="corrupt-file",
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",
"source_address": "0.0.0.0",
"cookiefile": str(real_cookies_txt.resolve().absolute()),
"concurrent_fragment_downloads": 4
"concurrent_fragment_downloads": 4,
}
description = ""
proxy_url = "socks5://localhost:1090"
try:
proxy_down = await asyncio.wait_for(
self.check_proxy("socks5://localhost:1090"),
timeout=10
)
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"
@ -1014,10 +943,7 @@ class OtherCog(commands.Cog):
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
)
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"
@ -1072,20 +998,14 @@ class OtherCog(commands.Cog):
"* Chosen format: `%s` (`%s`)" % (chosen_format, chosen_format_id),
)
if format_note:
lines.append(
"* Format note: %r" % format_note
)
lines.append("* Format note: %r" % format_note)
if final_extension:
lines.append(
"* File extension: " + final_extension
)
lines.append("* File extension: " + final_extension)
if resolution:
_s = resolution
if fps:
_s += " @ %s FPS" % fps
lines.append(
"* Resolution: " + _s
)
lines.append("* Resolution: " + _s)
if vcodec or acodec:
lines.append("%s+%s" % (vcodec or "N/A", acodec or "N/A"))
@ -1822,17 +1742,18 @@ class OtherCog(commands.Cog):
@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"
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__(
@ -1844,7 +1765,7 @@ class OtherCog(commands.Cog):
style=discord.InputTextStyle.long,
),
title="Enter prompt",
timeout=120
timeout=120,
)
if is_owner:
self.add_item(
@ -1887,10 +1808,10 @@ class OtherCog(commands.Cog):
try:
model, tag = model.split(":", 1)
model = model + ":" + tag
print("Model %r already has a tag")
self.log.debug("Model %r already has a tag")
except ValueError:
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]] = {
"100.106.34.86:11434": {
@ -1907,7 +1828,7 @@ class OtherCog(commands.Cog):
"codellama:python",
"codellama:instruct",
],
"owner": 421698654189912064
"owner": 421698654189912064,
},
"ollama.shronk.net:11434": {
"name": "Alibaba Cloud",
@ -1922,26 +1843,19 @@ class OtherCog(commands.Cog):
"orca-mini:3b",
"orca-mini:7b",
],
"owner": 421698654189912064
"owner": 421698654189912064,
},
}
H_DEFAULT = {
"name": "Other",
"allow": ["*"],
"owner": 1019217990111199243
}
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", ['*']):
for pat in _srv.get("allow", ["*"]):
if not fnmatch.fnmatch(model_name.lower(), pat.lower()):
print(
"Server %r does not support %r (only %r.)" % (
_srv['name'],
model_name,
', '.join(_srv['allow'])
)
self.log.debug(
"Server %r does not support %r (only %r.)"
% (_srv["name"], model_name, ", ".join(_srv["allow"]))
)
else:
break
@ -1951,9 +1865,7 @@ class OtherCog(commands.Cog):
class ServerSelector(discord.ui.View):
def __init__(self):
super().__init__(
disable_on_timeout=True
)
super().__init__(disable_on_timeout=True)
self.chosen_server = None
async def interaction_check(self, interaction: Interaction) -> bool:
@ -1963,21 +1875,15 @@ class OtherCog(commands.Cog):
placeholder="Choose a server.",
custom_id="select",
options=[
discord.SelectOption(
label="%s (%s)" % (y['name'], x),
value=x
)
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"
)
]
+ [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__(
@ -1994,10 +1900,10 @@ class OtherCog(commands.Cog):
min_length=2,
max_length=5,
style=discord.InputTextStyle.short,
value="11434"
value="11434",
),
title="Enter server details",
timeout=120
timeout=120,
)
self.hostname = None
self.port = None
@ -2016,8 +1922,7 @@ class OtherCog(commands.Cog):
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
f"\N{white heavy check mark} Selected server {self.chosen_server}/", ephemeral=True
)
self.stop()
@ -2035,29 +1940,18 @@ class OtherCog(commands.Cog):
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", ["*"]))
)
" any of the following: {}".format(srv["owner"], srv["name"], ", ".join(srv.get("allow", ["*"])))
)
content = None
embed = discord.Embed(
colour=discord.Colour.greyple()
)
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
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:
@ -2074,19 +1968,15 @@ class OtherCog(commands.Cog):
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()
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()
description="Transport error sending request to {}: {}".format(host, str(e)),
colour=discord.Colour.red(),
).set_footer(text=FOOTER_TEXT)
)
# get models
@ -2094,9 +1984,7 @@ class OtherCog(commands.Cog):
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()
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)
@ -2105,10 +1993,7 @@ class OtherCog(commands.Cog):
await msg.edit(embed=embed)
async with ctx.channel.typing():
async with client.stream(
"POST",
"/pull",
json={"name": model, "stream": True},
timeout=None
"POST", "/pull", json={"name": model, "stream": True}, timeout=None
) as response:
if response.status_code != 200:
error = await response.aread()
@ -2116,7 +2001,7 @@ class OtherCog(commands.Cog):
title=f"Failed to download model {model}:",
description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```",
colour=discord.Colour.red(),
url=str(response.url)
url=str(response.url),
)
embed.set_footer(text=FOOTER_TEXT)
return await msg.edit(embed=embed)
@ -2131,8 +2016,9 @@ class OtherCog(commands.Cog):
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)")
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
@ -2151,7 +2037,7 @@ class OtherCog(commands.Cog):
title=f"Failed to download model {model}:",
description=f"HTTP {response.status_code}:\n```{error or '<no body>'}\n```",
colour=discord.Colour.red(),
url=str(response.url)
url=str(response.url),
)
embed.set_footer(text=FOOTER_TEXT)
return await msg.edit(embed=embed)
@ -2160,32 +2046,21 @@ class OtherCog(commands.Cog):
title=f"{model} says:",
description="",
colour=discord.Colour.blurple(),
timestamp=discord.utils.utcnow()
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
}
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:
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 '<no body>'}\n```",
colour=discord.Colour.red()
colour=discord.Colour.red(),
)
embed.set_footer(text=FOOTER_TEXT)
return await msg.edit(embed=embed)
@ -2204,7 +2079,7 @@ class OtherCog(commands.Cog):
embed.set_author(
name=f"Generating response with {model}",
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"]
if (time() - last_edit) >= 5 or chunk["done"] is True:
@ -2215,10 +2090,7 @@ class OtherCog(commands.Cog):
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.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]
@ -2268,28 +2140,22 @@ class OtherCog(commands.Cog):
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(
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
)
embed.add_field(name="Timings", value=value, inline=False)
if context:
embed.add_field(
name="Context Key",
value=key,
inline=True
)
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)
@ -2297,44 +2163,31 @@ class OtherCog(commands.Cog):
@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
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()
SPEED_REGIONS = [
"fsn1",
"nbg1",
"hel1",
"ash",
"hil"
]
SPEED_REGIONS = ["fsn1", "nbg1", "hel1", "ash", "hil"]
results = {
"localhost:1090": {
"name": "SHRoNK",
"failure": None,
"download_speed": 0.0,
"tested": False,
"speedtest": "https://{hetzner_region}-speed.hetzner.com/100M.bin"
"speedtest": "https://{hetzner_region}-speed.hetzner.com/100M.bin",
},
"localhost:1080": {
"name": "NexBox",
"failure": None,
"download_speed": 0.0,
"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":
for key, value in results.copy().items():
@ -2342,18 +2195,13 @@ class OtherCog(commands.Cog):
continue
else:
results.pop(key)
embed = discord.Embed(
title="\N{white heavy check mark} Proxy available."
)
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
)
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()
@ -2375,19 +2223,12 @@ class OtherCog(commands.Cog):
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()
)
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.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:
@ -2396,11 +2237,9 @@ class OtherCog(commands.Cog):
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"
}
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
for region in SPEED_REGIONS:
@ -2418,9 +2257,7 @@ class OtherCog(commands.Cog):
response.raise_for_status()
end = time()
now = discord.utils.utcnow()
embed.set_footer(
text=embed.footer.text + " | Finished at: " + now.strftime("%X")
)
embed.set_footer(text=embed.footer.text + " | Finished at: " + now.strftime("%X"))
break
except Exception as e:
results[proxy_uri]["failure"] = f"Failed to test {region} speed (`{e}`)."
@ -2433,8 +2270,8 @@ class OtherCog(commands.Cog):
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()
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])
@ -2442,61 +2279,97 @@ class OtherCog(commands.Cog):
@commands.message_command(name="Transcribe")
async def transcribe_message(self, ctx: discord.ApplicationContext, message: discord.Message):
await ctx.defer()
if not message.attachments:
return await ctx.respond("No attachments found.")
async with self.transcribe_lock:
if not message.attachments:
return await ctx.respond("No attachments found.")
_ft = "wav"
for attachment in message.attachments:
if attachment.content_type.startswith("audio/"):
_ft = attachment.filename.split(".")[-1]
break
else:
return await ctx.respond("No voice messages.")
if getattr(config, "OPENAI_KEY", None) is None:
return await ctx.respond("Service unavailable.")
file_hash = hashlib.sha1(usedforsecurity=False)
file_hash.update(await attachment.read())
file_hash = file_hash.hexdigest()
_ft = "wav"
for attachment in message.attachments:
if attachment.content_type.startswith("audio/"):
_ft = attachment.filename.split(".")[-1]
break
else:
return await ctx.respond("No voice messages.")
if getattr(config, "OPENAI_KEY", None) is None:
return await ctx.respond("Service unavailable.")
file_hash = hashlib.sha1(usedforsecurity=False)
file_hash.update(await attachment.read())
file_hash = file_hash.hexdigest()
cache = Path.home() / ".cache" / "lcc-bot" / ("%s-transcript.txt" % file_hash)
cached = False
if not cache.exists():
client = openai.OpenAI(api_key=config.OPENAI_KEY)
with tempfile.NamedTemporaryFile("wb+", suffix=".mp4") as f:
with tempfile.NamedTemporaryFile("wb+", suffix="-" + attachment.filename) as f2:
await attachment.save(f2.name)
f2.seek(0)
seg: pydub.AudioSegment = await asyncio.to_thread(pydub.AudioSegment.from_file, file=f2, format=_ft)
seg = seg.set_channels(1)
await asyncio.to_thread(
seg.export, f.name, format="mp4"
cache = Path.home() / ".cache" / "lcc-bot" / ("%s-transcript.txt" % file_hash)
cached = False
if not cache.exists():
client = openai.OpenAI(api_key=config.OPENAI_KEY)
with tempfile.NamedTemporaryFile("wb+", suffix=".mp4") as f:
with tempfile.NamedTemporaryFile("wb+", suffix="-" + attachment.filename) as f2:
await attachment.save(f2.name)
f2.seek(0)
seg: pydub.AudioSegment = await asyncio.to_thread(pydub.AudioSegment.from_file, file=f2, format=_ft)
seg = seg.set_channels(1)
await asyncio.to_thread(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(
client.audio.transcriptions.create,
file=pathlib.Path(f.name),
model="whisper-1"
paginator = commands.Paginator("", "", 4096)
for line in text.splitlines():
paginator.add_line(textwrap.shorten(line, 4096))
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)
@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:
text = cache.read_text()
cached = True
paginator = commands.Paginator("", "", 4096)
for line in text.splitlines():
paginator.add_line(textwrap.shorten(line, 4096))
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
)
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):

View file

@ -1,13 +1,14 @@
import json
import logging
import random
from datetime import datetime, time, timedelta, timezone
from pathlib import Path
from typing import Dict, Optional, Union
import config
import discord
from discord.ext import commands, tasks
import config
from utils import TimeTableDaySwitcherView, console
@ -22,6 +23,7 @@ def schedule_times():
class TimeTableCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.log = logging.getLogger("jimmy.cogs.timetable")
with (Path.cwd() / "utils" / "timetable.json").open() as file:
self.timetable = json.load(file)
self.update_status.start()
@ -159,7 +161,7 @@ class TimeTableCog(commands.Cog):
try:
next_lesson = self.absolute_next_lesson(date + timedelta(days=1))
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
next_lesson.setdefault("name", "unknown")
next_lesson.setdefault("tutor", "unknown")

View file

@ -2,6 +2,7 @@ import asyncio
import datetime
import hashlib
import json
import logging
import random
import time
from datetime import timedelta
@ -66,15 +67,17 @@ class UptimeCompetition(commands.Cog):
def __init__(self, bot: commands.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.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()
if not (pth := Path("targets.json")).exists():
pth.write_text(BASE_JSON)
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
@ -103,7 +106,7 @@ class UptimeCompetition(commands.Cog):
return response
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.sort(key=lambda x: x["name"])
self._cached_targets = data.copy()
@ -111,7 +114,7 @@ class UptimeCompetition(commands.Cog):
def write_targets(self, data: List[Dict[str, str]]):
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)
def cog_unload(self):
@ -214,7 +217,7 @@ class UptimeCompetition(commands.Cog):
okay_statuses = list(filter(None, okay_statuses))
guild: discord.Guild = self.bot.get_guild(guild_id)
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."
)
else:
@ -224,7 +227,7 @@ class UptimeCompetition(commands.Cog):
try:
user = await guild.fetch_member(user_id)
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
if user:
create_tasks.append(
@ -240,7 +243,7 @@ class UptimeCompetition(commands.Cog):
)
else:
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"
" shronk bot is disabled."
)

62
main.py
View file

@ -1,28 +1,57 @@
import asyncio
import sys
import logging
import os
import signal
import sys
import textwrap
from datetime import datetime, timedelta, timezone
import config
import discord
from discord.ext import commands
from rich.logging import RichHandler
import config
from utils import JimmyBanException, JimmyBans, console, get_or_none
from utils.client import bot
logging.basicConfig(
filename="jimmy.log",
filemode="a",
format="%(asctime)s:%(level)s:%(name)s: %(message)s",
format="%(asctime)s:%(levelname)s:%(name)s: %(message)s",
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()
async def on_connect():
console.log("[green]Connected to discord!")
bot.log.info("[green]Connected to discord!")
@bot.listen("on_application_command_error")
@ -60,13 +89,13 @@ async def on_command_error(ctx: commands.Context, error: Exception):
return
elif isinstance(error, JimmyBanException):
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
@bot.listen("on_application_command")
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 "
"[blue]#{0.channel}[/], {0.guild}".format(ctx)
)
@ -74,9 +103,9 @@ async def on_application_command(ctx: discord.ApplicationContext):
@bot.event
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:
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()
sys.exit(0)
@ -105,10 +134,11 @@ async def check_not_banned(ctx: discord.ApplicationContext | commands.Context):
if __name__ == "__main__":
console.log("Starting...")
bot.log.info("Starting...")
bot.started_at = discord.utils.utcnow()
if getattr(config, "WEB_SERVER", True):
bot.log.info("Web server is enabled (WEB_SERVER=True in config.py), initialising.")
import uvicorn
from web.server import app
@ -119,11 +149,11 @@ if __name__ == "__main__":
app,
host=getattr(config, "HTTP_HOST", "127.0.0.1"),
port=getattr(config, "HTTP_PORT", 3762),
loop="asyncio",
**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)
console.log("Starting web server...")
bot.log.info("Starting web server...")
loop = bot.loop
http_server_task = loop.create_task(server.serve())
bot.web = {
@ -131,5 +161,5 @@ if __name__ == "__main__":
"config": http_config,
"task": http_server_task,
}
bot.log.info("Beginning main loop.")
bot.run(config.token)

View file

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

View file

@ -8,4 +8,4 @@ if _col == 80:
__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
from discord.ui import View
from utils import (
TOKEN_LENGTH,
BannedStudentID,
Student,
VerifyCode,
console,
get_or_none,
send_verification_code,
)
from utils import BannedStudentID, Student, VerifyCode, console, get_or_none
TOKEN_LENGTH = 16
if typing.TYPE_CHECKING:
from cogs.timetable import TimeTableCog
@ -133,7 +127,8 @@ class VerifyView(View):
)
try:
_code = await send_verification_code(interaction.user, st)
# _code = await send_verification_code(interaction.user, st)
raise RuntimeError("Disabled.")
except Exception as e:
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})...")

View file

@ -1,9 +1,9 @@
import asyncio
import ipaddress
import logging
import os
import sys
import textwrap
from rich import print
from asyncio import Lock
from datetime import datetime, timezone
from hashlib import sha512
from http import HTTPStatus
@ -11,13 +11,13 @@ from pathlib import Path
import discord
import httpx
from config import guilds
from asyncio import Lock
from fastapi import FastAPI, Header, HTTPException, Request, WebSocketException as _WSException
from websockets.exceptions import WebSocketException
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi import WebSocketException as _WSException
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
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.db import AccessTokens
@ -37,7 +37,9 @@ try:
except ImportError:
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
@ -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:
value = os.urandom(4).hex()
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
removed = 0
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]
removed += 1
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:
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(
HTTPStatus.SERVICE_UNAVAILABLE,
"Could not generate a state token (state container full, potential (D)DOS attack?). "
"Please try again later.",
# 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.
headers={"Retry-After": "300"},
headers={"Retry-After": "60"},
)
app.state.states[value] = datetime.now()
return RedirectResponse(
@ -239,7 +241,7 @@ async def verify(code: str):
# And delete the code
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)
@ -293,12 +295,12 @@ async def bridge(req: Request):
@app.websocket("/bridge/recv")
async def bridge_recv(ws: WebSocket, secret: str = Header(None)):
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:
print("Closing websocket %r, invalid secret." % ws)
log.warning("Closing websocket %r, invalid secret.", ws.client.host)
raise _WSException(code=1008, reason="Invalid Secret")
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.")
queue: asyncio.Queue = app.state.bot.bridge_queue
@ -307,7 +309,7 @@ async def bridge_recv(ws: WebSocket, secret: str = Header(None)):
try:
await ws.send_json({"status": "ping"})
except (WebSocketDisconnect, WebSocketException):
print("Websocket %r disconnected." % ws)
log.info("Websocket %r disconnected.", ws)
break
try:
@ -316,10 +318,10 @@ async def bridge_recv(ws: WebSocket, secret: str = Header(None)):
continue
try:
print("Sent data %r to websocket %r." % (data, ws))
await ws.send_json(data)
log.debug("Sent data %r to websocket %r.", data, ws)
except (WebSocketDisconnect, WebSocketException):
print("Websocket %r disconnected." % ws)
log.info("Websocket %r disconnected." % ws)
break
finally:
queue.task_done()