mirror of
https://github.com/nexy7574/LCC-bot.git
synced 2024-09-19 18:16:34 +01:00
Merge branch 'master' of github.com:nexy7574/lcc-bot
This commit is contained in:
commit
3f42a9ca6f
16 changed files with 552 additions and 593 deletions
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
23
cogs/info.py
23
cogs/info.py
|
@ -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
207
cogs/mcdonalds.py
Normal 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))
|
373
cogs/other.py
373
cogs/other.py
|
@ -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"))
|
||||||
|
|
||||||
|
@ -1827,12 +1747,13 @@ class OtherCog(commands.Cog):
|
||||||
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,8 +2016,9 @@ 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
|
||||||
|
@ -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 = (
|
||||||
|
"* Total: {}\n"
|
||||||
"* Model load: {}\n"
|
"* Model load: {}\n"
|
||||||
"* Sample generation: {}\n"
|
"* Sample generation: {}\n"
|
||||||
"* Prompt eval: {}\n"
|
"* Prompt eval: {}\n"
|
||||||
"* Response generation: {}\n").format(
|
"* 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)
|
||||||
|
@ -2300,41 +2166,28 @@ class OtherCog(commands.Cog):
|
||||||
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:
|
||||||
|
@ -2398,9 +2239,7 @@ class OtherCog(commands.Cog):
|
||||||
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}`)."
|
||||||
|
@ -2434,7 +2271,7 @@ class OtherCog(commands.Cog):
|
||||||
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,6 +2279,7 @@ 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()
|
||||||
|
async with self.transcribe_lock:
|
||||||
if not message.attachments:
|
if not message.attachments:
|
||||||
return await ctx.respond("No attachments found.")
|
return await ctx.respond("No attachments found.")
|
||||||
|
|
||||||
|
@ -2468,15 +2306,11 @@ class OtherCog(commands.Cog):
|
||||||
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)
|
f.seek(0)
|
||||||
|
|
||||||
transcript = await asyncio.to_thread(
|
transcript = await asyncio.to_thread(
|
||||||
client.audio.transcriptions.create,
|
client.audio.transcriptions.create, file=pathlib.Path(f.name), model="whisper-1"
|
||||||
file=pathlib.Path(f.name),
|
|
||||||
model="whisper-1"
|
|
||||||
)
|
)
|
||||||
text = transcript.text
|
text = transcript.text
|
||||||
cache.write_text(text)
|
cache.write_text(text)
|
||||||
|
@ -2492,12 +2326,51 @@ class OtherCog(commands.Cog):
|
||||||
|
|
||||||
if await self.bot.is_owner(ctx.user):
|
if await self.bot.is_owner(ctx.user):
|
||||||
await ctx.respond(
|
await ctx.respond(
|
||||||
("Cached response ({})" if cached else "Uncached response ({})").format(
|
("Cached response ({})" if cached else "Uncached response ({})").format(file_hash), ephemeral=True
|
||||||
file_hash
|
|
||||||
),
|
|
||||||
ephemeral=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@commands.slash_command()
|
||||||
|
async def whois(self, ctx: discord.ApplicationContext, domain: str):
|
||||||
|
"""Runs a WHOIS."""
|
||||||
|
await ctx.defer()
|
||||||
|
url = urlparse("http://" + domain)
|
||||||
|
if not url.hostname:
|
||||||
|
return await ctx.respond("Invalid domain.")
|
||||||
|
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
"whois", "-H", url.hostname, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
stdout, stderr = await process.communicate()
|
||||||
|
await process.wait()
|
||||||
|
stdout = stdout.decode("utf-8", "replace")
|
||||||
|
stderr = stderr.decode("utf-8", "replace")
|
||||||
|
if process.returncode != 0:
|
||||||
|
return await ctx.respond(f"Error:\n```{stderr[:3900]}```")
|
||||||
|
|
||||||
|
paginator = commands.Paginator()
|
||||||
|
redacted = io.BytesIO()
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
if line.startswith(">>> Last update"):
|
||||||
|
break
|
||||||
|
if "REDACTED" in line or "Please query the WHOIS server of the owning registrar" in line:
|
||||||
|
redacted.write(line.encode() + b"\n")
|
||||||
|
else:
|
||||||
|
paginator.add_line(line)
|
||||||
|
|
||||||
|
if redacted.tell() > 0:
|
||||||
|
redacted.seek(0)
|
||||||
|
file = discord.File(redacted, "redacted-fields.txt", description="Any discovered redacted fields.")
|
||||||
|
else:
|
||||||
|
file = None
|
||||||
|
if len(paginator.pages) > 1:
|
||||||
|
for page in paginator.pages:
|
||||||
|
await ctx.respond(page)
|
||||||
|
if file:
|
||||||
|
await ctx.respond(file=file)
|
||||||
|
else:
|
||||||
|
kwargs = {"file": file} if file else {}
|
||||||
|
await ctx.respond(paginator.pages[0], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def setup(bot):
|
def setup(bot):
|
||||||
bot.add_cog(OtherCog(bot))
|
bot.add_cog(OtherCog(bot))
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
62
main.py
|
@ -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)
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
|
|
@ -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})...")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue