Black reformat

This commit is contained in:
Nexus 2024-04-16 00:46:26 +01:00
parent a634c64cd6
commit d5466a2b09
Signed by: nex
GPG key ID: 0FA334385D0B689F
9 changed files with 313 additions and 462 deletions

View file

@ -41,7 +41,9 @@ class AutoResponder(commands.Cog):
links.append(url.geturl()) links.append(url.geturl())
return links return links
async def _transcode_hevc_to_h264(self, uri: str | pathlib.Path) -> tuple[discord.File, pathlib.Path] | None: async def _transcode_hevc_to_h264(
self, uri: str | pathlib.Path, *, update: discord.Message = None
) -> tuple[discord.File, pathlib.Path] | None:
""" """
Transcodes the given URL or file to H264 from HEVC, trying to preserve quality and file size. Transcodes the given URL or file to H264 from HEVC, trying to preserve quality and file size.
@ -64,7 +66,7 @@ class AutoResponder(commands.Cog):
self.log.warning( self.log.warning(
"Video %r is %.2f seconds long (more than 10 minutes). Refusing to process further.", "Video %r is %.2f seconds long (more than 10 minutes). Refusing to process further.",
uri, uri,
float(info["format"].get("duration", 600.1)) float(info["format"].get("duration", 600.1)),
) )
return return
streams = info.get("streams", []) streams = info.get("streams", [])
@ -105,21 +107,35 @@ class AutoResponder(commands.Cog):
self.log.info("Transcoding %r to %r", uri, tmp_path) self.log.info("Transcoding %r to %r", uri, tmp_path)
args = [ args = [
"-hide_banner", "-hide_banner",
"-hwaccel", "auto", "-hwaccel",
"-i", tmp_dl.name, "auto",
"-c:v", "libx264", "-i",
"-crf", "25", tmp_dl.name,
"-maxrate", "5M", "-c:v",
"-minrate", "100K", "libx264",
"-bufsize", "5M", "-crf",
"-c:a", "libopus", "25",
"-b:a", "64k", "-maxrate",
"-preset", "faster", "5M",
"-vsync", "2", "-minrate",
"-pix_fmt", "yuv420p", "100K",
"-movflags", "faststart", "-bufsize",
"-profile:v", "main", "5M",
"-y" "-c:a",
"libopus",
"-b:a",
"64k",
"-preset",
"faster",
"-vsync",
"2",
"-pix_fmt",
"yuv420p",
"-movflags",
"faststart",
"-profile:v",
"main",
"-y",
] ]
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"ffmpeg", "ffmpeg",
@ -150,7 +166,7 @@ class AutoResponder(commands.Cog):
if message.channel.name == "spam" and message.author.id in { if message.channel.name == "spam" and message.author.id in {
1101439218334576742, 1101439218334576742,
1229496078726860921, 1229496078726860921,
421698654189912064 421698654189912064,
}: }:
# links = self.extract_links(message.content, "static-assets-1.truthsocial.com") # links = self.extract_links(message.content, "static-assets-1.truthsocial.com")
links = self.extract_links(message.content) links = self.extract_links(message.content)
@ -167,7 +183,7 @@ class AutoResponder(commands.Cog):
".ps", ".ps",
".ts", ".ts",
".3gp", ".3gp",
".3g2" ".3g2",
} }
# ^ All containers allowed to contain HEVC # ^ All containers allowed to contain HEVC
# per https://en.wikipedia.org/wiki/Comparison_of_video_container_formats # per https://en.wikipedia.org/wiki/Comparison_of_video_container_formats
@ -175,7 +191,7 @@ class AutoResponder(commands.Cog):
self.log.info("Found link to transcode: %r", link) self.log.info("Found link to transcode: %r", link)
try: try:
async with message.channel.typing(): async with message.channel.typing():
_r = await self._transcode_hevc_to_h264(link) _r = await self._transcode_hevc_to_h264(link, update=message)
if not _r: if not _r:
continue continue
file, _p = _r file, _p = _r
@ -186,7 +202,7 @@ class AutoResponder(commands.Cog):
self.log.warning( self.log.warning(
"Transcoded file too large: %r (%.2f)MB", "Transcoded file too large: %r (%.2f)MB",
_p, _p,
_p.stat().st_size / 1024 / 1024 _p.stat().st_size / 1024 / 1024,
) )
if _p.stat().st_size <= 510 * 1024 * 1024: if _p.stat().st_size <= 510 * 1024 * 1024:
file.fp.seek(0) file.fp.seek(0)
@ -194,17 +210,11 @@ class AutoResponder(commands.Cog):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
response = await client.post( response = await client.post(
"https://0x0.st", "https://0x0.st",
files={ files={"file": (_p.name, file.fp, "video/mp4")},
"file": (_p.name, file.fp, "video/mp4") headers={"User-Agent": "CollegeBot (matrix: @nex:nexy7574.co.uk)"},
},
headers={
"User-Agent": "CollegeBot (matrix: @nex:nexy7574.co.uk)"
}
) )
if response.status_code == 200: if response.status_code == 200:
await message.reply( await message.reply("https://embeds.video/" + response.text.strip())
"https://embeds.video/" + response.text.strip()
)
_p.unlink() _p.unlink()
except Exception as e: except Exception as e:
self.log.error("Failed to transcode %r: %r", link, e) self.log.error("Failed to transcode %r: %r", link, e)

View file

@ -83,18 +83,15 @@ class FFMeta(commands.Cog):
description="The quality of the resulting image from 1%-100%", description="The quality of the resulting image from 1%-100%",
default=50, default=50,
min_value=1, min_value=1,
max_value=100 max_value=100,
) ),
] = 50, ] = 50,
image_format: typing.Annotated[ image_format: typing.Annotated[
str, str,
discord.Option( discord.Option(
str, str, description="The format of the resulting image", choices=["jpeg", "webp"], default="jpeg"
description="The format of the resulting image", ),
choices=["jpeg", "webp"], ] = "jpeg",
default="jpeg"
)
] = "jpeg"
): ):
"""Converts a given URL or attachment to a JPEG""" """Converts a given URL or attachment to a JPEG"""
if url is None: if url is None:
@ -135,17 +132,12 @@ class FFMeta(commands.Cog):
description="The bitrate in kilobits of the resulting audio from 1-512", description="The bitrate in kilobits of the resulting audio from 1-512",
default=96, default=96,
min_value=0, min_value=0,
max_value=512 max_value=512,
) ),
] = 96, ] = 96,
mono: typing.Annotated[ mono: typing.Annotated[
bool, bool, discord.Option(bool, description="Whether to convert the audio to mono", default=False)
discord.Option( ] = False,
bool,
description="Whether to convert the audio to mono",
default=False
)
] = False
): ):
"""Converts a given URL or attachment to an Opus file""" """Converts a given URL or attachment to an Opus file"""
if bitrate == 0: if bitrate == 0:
@ -175,8 +167,10 @@ class FFMeta(commands.Cog):
probe_process = await asyncio.create_subprocess_exec( probe_process = await asyncio.create_subprocess_exec(
"ffprobe", "ffprobe",
"-v", "quiet", "-v",
"-print_format", "json", "quiet",
"-print_format",
"json",
"-show_format", "-show_format",
"-i", "-i",
temp.name, temp.name,
@ -207,12 +201,16 @@ class FFMeta(commands.Cog):
"-stats", "-stats",
"-i", "-i",
temp.name, temp.name,
"-c:a", "libopus", "-c:a",
"-b:a", f"{bitrate}k", "libopus",
"-b:a",
f"{bitrate}k",
"-vn", "-vn",
"-sn", "-sn",
"-ac", str(channels), "-ac",
"-f", "opus", str(channels),
"-f",
"opus",
"-y", "-y",
"pipe:1", "pipe:1",
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,

View file

@ -103,8 +103,7 @@ class NetworkCog(commands.Cog):
file.write(stderr) file.write(stderr)
file.seek(0) file.seek(0)
return await ctx.respond( return await ctx.respond(
"Seemingly all output was filtered. Returning raw command output.", "Seemingly all output was filtered. Returning raw command output.", file=discord.File(file, "whois.txt")
file=discord.File(file, "whois.txt")
) )
for page in paginator.pages: for page in paginator.pages:

View file

@ -166,12 +166,7 @@ class OllamaChatHandler:
async def __aiter__(self): async def __aiter__(self):
async with aiohttp.ClientSession(base_url=self.base_url) as client: async with aiohttp.ClientSession(base_url=self.base_url) as client:
async with client.post( async with client.post(
"/api/chat", "/api/chat", json={"model": self.model, "stream": True, "messages": self.messages}
json={
"model": self.model,
"stream": True,
"messages": self.messages
}
) as response: ) as response:
response.raise_for_status() response.raise_for_status()
async for line in ollama_stream(response.content): async for line in ollama_stream(response.content):
@ -191,10 +186,7 @@ class OllamaClient:
self.base_url = base_url self.base_url = base_url
self.authorisation = authorisation self.authorisation = authorisation
def with_client( def with_client(self, timeout: aiohttp.ClientTimeout | float | int | None = None) -> aiohttp.ClientSession:
self,
timeout: aiohttp.ClientTimeout | float | int | None = None
) -> aiohttp.ClientSession:
""" """
Creates an instance for a request, with properly populated values. Creates an instance for a request, with properly populated values.
:param timeout: :param timeout:
@ -291,9 +283,7 @@ class ChatHistory:
def save_thread(self, thread_id: str): def save_thread(self, thread_id: str):
self.log.info("Saving thread:%s - %r", thread_id, self._internal[thread_id]) self.log.info("Saving thread:%s - %r", thread_id, self._internal[thread_id])
self.redis.set( self.redis.set("threads:" + thread_id, json.dumps(self._internal[thread_id]))
"threads:" + thread_id, json.dumps(self._internal[thread_id])
)
def create_thread(self, member: discord.Member, default: str | None = None) -> str: def create_thread(self, member: discord.Member, default: str | None = None) -> str:
""" """
@ -304,26 +294,15 @@ class ChatHistory:
:return: The thread's ID. :return: The thread's ID.
""" """
key = os.urandom(3).hex() key = os.urandom(3).hex()
self._internal[key] = { self._internal[key] = {"member": member.id, "seed": round(time.time()), "messages": []}
"member": member.id,
"seed": round(time.time()),
"messages": []
}
with open("./assets/ollama-prompt.txt") as file: with open("./assets/ollama-prompt.txt") as file:
system_prompt = default or file.read() system_prompt = default or file.read()
self.add_message( self.add_message(key, "system", system_prompt)
key,
"system",
system_prompt
)
return key return key
@staticmethod @staticmethod
def _construct_message(role: str, content: str, images: typing.Optional[list[str]]) -> dict[str, str]: def _construct_message(role: str, content: str, images: typing.Optional[list[str]]) -> dict[str, str]:
x = { x = {"role": role, "content": content}
"role": role,
"content": content
}
if images: if images:
x["images"] = images x["images"] = images
return x return x
@ -335,10 +314,8 @@ class ChatHistory:
instance = cog.history instance = cog.history
return list( return list(
filter( filter(
lambda v: (ctx.value or v) in v, map( lambda v: (ctx.value or v) in v,
lambda d: list(d.keys()), map(lambda d: list(d.keys()), instance.threads_for(ctx.interaction.user)),
instance.threads_for(ctx.interaction.user)
)
) )
) )
@ -359,7 +336,7 @@ class ChatHistory:
thread: str, thread: str,
role: typing.Literal["user", "assistant", "system"], role: typing.Literal["user", "assistant", "system"],
content: str, content: str,
images: typing.Optional[list[str]] = None images: typing.Optional[list[str]] = None,
) -> None: ) -> None:
""" """
Appends a message to the given thread. Appends a message to the given thread.
@ -503,49 +480,32 @@ class Ollama(commands.Cog):
discord.Option( discord.Option(
str, str,
"The query to feed into ollama. Not the system prompt.", "The query to feed into ollama. Not the system prompt.",
) ),
], ],
model: typing.Annotated[ model: typing.Annotated[
str, str,
discord.Option( discord.Option(
str, str,
"The model to use for ollama. Defaults to 'llama2-uncensored:latest'.", "The model to use for ollama. Defaults to 'llama2-uncensored:latest'.",
default="llama2-uncensored:7b-chat" default="llama2-uncensored:7b-chat",
) ),
], ],
server: typing.Annotated[ server: typing.Annotated[
str, str, discord.Option(str, "The server to use for ollama.", default="next", choices=SERVER_KEYS)
discord.Option(
str,
"The server to use for ollama.",
default="next",
choices=SERVER_KEYS
)
], ],
context: typing.Annotated[ context: typing.Annotated[
str, str, discord.Option(str, "The context key of a previous ollama response to use as context.", default=None)
discord.Option(
str,
"The context key of a previous ollama response to use as context.",
default=None
)
], ],
give_acid: typing.Annotated[ give_acid: typing.Annotated[
bool, bool,
discord.Option( discord.Option(
bool, bool, "Whether to give the AI acid, LSD, and other hallucinogens before responding.", default=False
"Whether to give the AI acid, LSD, and other hallucinogens before responding.", ),
default=False
)
], ],
image: typing.Annotated[ image: typing.Annotated[
discord.Attachment, discord.Attachment,
discord.Option( discord.Option(discord.Attachment, "An image to feed into ollama. Only works with llava.", default=None),
discord.Attachment, ],
"An image to feed into ollama. Only works with llava.",
default=None
)
]
): ):
system_query = None system_query = None
if context is not None: if context is not None:
@ -578,8 +538,7 @@ class Ollama(commands.Cog):
if image: if image:
if fnmatch(model, "llava:*") is False: if fnmatch(model, "llava:*") is False:
await ctx.respond( await ctx.respond(
"You can only use images with llava. Switching model to `llava:latest`.", "You can only use images with llava. Switching model to `llava:latest`.", delete_after=5
delete_after=5
) )
model = "llava:latest" model = "llava:latest"
@ -614,18 +573,13 @@ class Ollama(commands.Cog):
async with aiohttp.ClientSession( async with aiohttp.ClientSession(
base_url=server_config["base_url"], base_url=server_config["base_url"],
timeout=aiohttp.ClientTimeout( timeout=aiohttp.ClientTimeout(connect=30, sock_read=10800, sock_connect=30, total=10830),
connect=30,
sock_read=10800,
sock_connect=30,
total=10830
)
) as session: ) as session:
embed = discord.Embed( embed = discord.Embed(
title="Checking server...", title="Checking server...",
description=f"Checking that specified model and tag ({model}) are available on the server.", description=f"Checking that specified model and tag ({model}) are available on the server.",
color=discord.Color.blurple(), color=discord.Color.blurple(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text="Using server %r" % server, icon_url=server_config.get("icon_url")) embed.set_footer(text="Using server %r" % server, icon_url=server_config.get("icon_url"))
await ctx.respond(embed=embed) await ctx.respond(embed=embed)
@ -636,7 +590,7 @@ class Ollama(commands.Cog):
title="Server was offline. Trying next server.", title="Server was offline. Trying next server.",
description=f"Trying server {server}...", description=f"Trying server {server}...",
color=discord.Color.gold(), color=discord.Color.gold(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text="Using server %r" % server, icon_url=server_config.get("icon_url")) embed.set_footer(text="Using server %r" % server, icon_url=server_config.get("icon_url"))
await ctx.edit(embed=embed) await ctx.edit(embed=embed)
@ -650,7 +604,7 @@ class Ollama(commands.Cog):
title="All servers are offline.", title="All servers are offline.",
description="Please try again later.", description="Please try again later.",
color=discord.Color.red(), color=discord.Color.red(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text="Unable to continue.") embed.set_footer(text="Unable to continue.")
return await ctx.edit(embed=embed) return await ctx.edit(embed=embed)
@ -665,7 +619,7 @@ class Ollama(commands.Cog):
title=f"HTTP {resp.status} {resp.reason!r} while checking for model.", title=f"HTTP {resp.status} {resp.reason!r} while checking for model.",
description=f"```{await resp.text() or 'No response body'}```"[:4096], description=f"```{await resp.text() or 'No response body'}```"[:4096],
color=discord.Color.red(), color=discord.Color.red(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text="Unable to continue.") embed.set_footer(text="Unable to continue.")
return await ctx.edit(embed=embed) return await ctx.edit(embed=embed)
@ -674,7 +628,7 @@ class Ollama(commands.Cog):
title="Connection error while checking for model.", title="Connection error while checking for model.",
description=f"```{e}```"[:4096], description=f"```{e}```"[:4096],
color=discord.Color.red(), color=discord.Color.red(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text="Unable to continue.") embed.set_footer(text="Unable to continue.")
return await ctx.edit(embed=embed) return await ctx.edit(embed=embed)
@ -694,7 +648,7 @@ class Ollama(commands.Cog):
title=f"Downloading {model!r}", title=f"Downloading {model!r}",
description=f"Downloading {model!r} from {server_config['base_url']}", description=f"Downloading {model!r} from {server_config['base_url']}",
color=discord.Color.blurple(), color=discord.Color.blurple(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.add_field(name="Progress", value=progress_bar(0)) embed.add_field(name="Progress", value=progress_bar(0))
await ctx.edit(embed=embed) await ctx.edit(embed=embed)
@ -708,7 +662,7 @@ class Ollama(commands.Cog):
title=f"HTTP {response.status} {response.reason!r} while downloading model.", title=f"HTTP {response.status} {response.reason!r} while downloading model.",
description=f"```{await response.text() or 'No response body'}```"[:4096], description=f"```{await response.text() or 'No response body'}```"[:4096],
color=discord.Color.red(), color=discord.Color.red(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text="Unable to continue.") embed.set_footer(text="Unable to continue.")
return await ctx.edit(embed=embed) return await ctx.edit(embed=embed)
@ -718,7 +672,7 @@ class Ollama(commands.Cog):
embed = discord.Embed( embed = discord.Embed(
title="Download cancelled.", title="Download cancelled.",
colour=discord.Colour.red(), colour=discord.Colour.red(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
return await ctx.edit(embed=embed, view=None) return await ctx.edit(embed=embed, view=None)
if time.time() >= (last_update + 5.1): if time.time() >= (last_update + 5.1):
@ -739,17 +693,15 @@ class Ollama(commands.Cog):
title="Generating response...", title="Generating response...",
description=">>> ", description=">>> ",
color=discord.Color.blurple(), color=discord.Color.blurple(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_author( embed.set_author(
name=model, name=model,
url="https://ollama.ai/library/" + model.split(":")[0], url="https://ollama.ai/library/" + model.split(":")[0],
icon_url="https://ollama.ai/public/ollama.png" icon_url="https://ollama.ai/public/ollama.png",
) )
embed.add_field( embed.add_field(
name="Prompt", name="Prompt", value=">>> " + textwrap.shorten(query, width=1020, placeholder="..."), inline=False
value=">>> " + textwrap.shorten(query, width=1020, placeholder="..."),
inline=False
) )
embed.set_footer(text="Using server %r" % server, icon_url=server_config.get("icon_url")) embed.set_footer(text="Using server %r" % server, icon_url=server_config.get("icon_url"))
if image_data: if image_data:
@ -774,10 +726,7 @@ class Ollama(commands.Cog):
context = list(__thread.keys())[0] context = list(__thread.keys())[0]
messages = self.history.get_history(context) messages = self.history.get_history(context)
user_message = { user_message = {"role": "user", "content": query}
"role": "user",
"content": query
}
if image_data: if image_data:
user_message["images"] = [image_data] user_message["images"] = [image_data]
messages.append(user_message) messages.append(user_message)
@ -789,12 +738,7 @@ class Ollama(commands.Cog):
params["top_p"] = 2 params["top_p"] = 2
params["repeat_penalty"] = 2 params["repeat_penalty"] = 2
payload = { payload = {"model": model, "stream": True, "options": params, "messages": messages}
"model": model,
"stream": True,
"options": params,
"messages": messages
}
async with session.post( async with session.post(
"/api/chat", "/api/chat",
json=payload, json=payload,
@ -805,7 +749,7 @@ class Ollama(commands.Cog):
title=f"HTTP {response.status} {response.reason!r} while generating response.", title=f"HTTP {response.status} {response.reason!r} while generating response.",
description=f"```{await response.text() or 'No response body'}```"[:4096], description=f"```{await response.text() or 'No response body'}```"[:4096],
color=discord.Color.red(), color=discord.Color.red(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_footer(text="Unable to continue.") embed.set_footer(text="Unable to continue.")
return await ctx.edit(embed=embed) return await ctx.edit(embed=embed)
@ -871,7 +815,7 @@ class Ollama(commands.Cog):
description=f"Total: {total_duration}\nLoad: {load_duration}\n" description=f"Total: {total_duration}\nLoad: {load_duration}\n"
f"Prompt Eval: {prompt_eval_duration}\nEval: {eval_duration}", f"Prompt Eval: {prompt_eval_duration}\nEval: {eval_duration}",
color=discord.Color.blurple(), color=discord.Color.blurple(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
return await ctx.respond(embed=embed, ephemeral=True) return await ctx.respond(embed=embed, ephemeral=True)
@ -886,8 +830,8 @@ class Ollama(commands.Cog):
description="Thread/Context ID", description="Thread/Context ID",
type=str, type=str,
autocomplete=ChatHistory.autocomplete, autocomplete=ChatHistory.autocomplete,
) ),
] ],
): ):
"""Shows the history for a thread.""" """Shows the history for a thread."""
# await ctx.defer(ephemeral=True) # await ctx.defer(ephemeral=True)
@ -904,17 +848,11 @@ class Ollama(commands.Cog):
if message["role"] == "system": if message["role"] == "system":
continue continue
max_length = 4000 - len("> **%s**: " % message["role"]) max_length = 4000 - len("> **%s**: " % message["role"])
paginator.add_line( paginator.add_line("> **{}**: {}".format(message["role"], textwrap.shorten(message["content"], max_length)))
"> **{}**: {}".format(message["role"], textwrap.shorten(message["content"], max_length))
)
embeds = [] embeds = []
for page in paginator.pages: for page in paginator.pages:
embeds.append( embeds.append(discord.Embed(description=page))
discord.Embed(
description=page
)
)
ephemeral = len(embeds) > 1 ephemeral = len(embeds) > 1
for chunk in discord.utils.as_chunks(iter(embeds or [discord.Embed(title="No Content.")]), 10): for chunk in discord.utils.as_chunks(iter(embeds or [discord.Embed(title="No Content.")]), 10):
await ctx.respond(embeds=chunk, ephemeral=ephemeral) await ctx.respond(embeds=chunk, ephemeral=ephemeral)
@ -929,10 +867,7 @@ class Ollama(commands.Cog):
if not content: if not content:
return await ctx.respond("No content to send to AI.", ephemeral=True) return await ctx.respond("No content to send to AI.", ephemeral=True)
await ctx.defer() await ctx.defer()
user_message = { user_message = {"role": "user", "content": message.content}
"role": "user",
"content": message.content
}
self.history.add_message(thread, "user", user_message["content"]) self.history.add_message(thread, "user", user_message["content"])
for _ in range(10): for _ in range(10):
@ -947,10 +882,7 @@ class Ollama(commands.Cog):
with client.download_model("orca-mini", "3b") as handler: with client.download_model("orca-mini", "3b") as handler:
async for _ in handler: async for _ in handler:
self.log.info( self.log.info(
"Downloading orca-mini:3b on server %r - %s (%.2f%%)", "Downloading orca-mini:3b on server %r - %s (%.2f%%)", server, handler.status, handler.percent
server,
handler.status,
handler.percent
) )
messages = self.history.get_history(thread) messages = self.history.get_history(thread)

View file

@ -28,11 +28,7 @@ class QuoteQuota(commands.Cog):
return c return c
@staticmethod @staticmethod
def generate_pie_chart( def generate_pie_chart(usernames: list[str], counts: list[int], no_other: bool = False) -> discord.File:
usernames: list[str],
counts: list[int],
no_other: bool = False
) -> discord.File:
""" """
Converts the given username and count tuples into a nice pretty pie chart. Converts the given username and count tuples into a nice pretty pie chart.
@ -81,7 +77,7 @@ class QuoteQuota(commands.Cog):
) )
fig.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, wspace=0.3, hspace=0.4) fig.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, wspace=0.3, hspace=0.4)
fio = io.BytesIO() fio = io.BytesIO()
fig.savefig(fio, format='png') fig.savefig(fio, format="png")
fio.seek(0) fio.seek(0)
return discord.File(fio, filename="pie.png") return discord.File(fio, filename="pie.png")
@ -97,8 +93,8 @@ class QuoteQuota(commands.Cog):
description="How many days to look back on. Defaults to 7.", description="How many days to look back on. Defaults to 7.",
default=7, default=7,
min_value=1, min_value=1,
max_value=365 max_value=365,
) ),
], ],
merge_other: Annotated[ merge_other: Annotated[
bool, bool,
@ -106,9 +102,9 @@ class QuoteQuota(commands.Cog):
bool, bool,
name="merge_other", name="merge_other",
description="Whether to merge authors with less than 5% of the total count into 'Other'.", description="Whether to merge authors with less than 5% of the total count into 'Other'.",
default=True default=True,
) ),
] ],
): ):
"""Checks the quote quota for the quotes channel.""" """Checks the quote quota for the quotes channel."""
now = discord.utils.utcnow() now = discord.utils.utcnow()
@ -123,11 +119,7 @@ class QuoteQuota(commands.Cog):
authors = {} authors = {}
filtered_messages = 0 filtered_messages = 0
total = 0 total = 0
async for message in channel.history( async for message in channel.history(limit=None, after=oldest, oldest_first=False):
limit=None,
after=oldest,
oldest_first=False
):
total += 1 total += 1
if not message.content: if not message.content:
filtered_messages += 1 filtered_messages += 1
@ -164,33 +156,23 @@ class QuoteQuota(commands.Cog):
return await ctx.edit( return await ctx.edit(
content="No valid messages found in the last {!s} days. " content="No valid messages found in the last {!s} days. "
"Make sure quotes are formatted properly ending with ` - AuthorName`" "Make sure quotes are formatted properly ending with ` - AuthorName`"
" (e.g. `\"This is my quote\" - Jimmy`)".format(days) ' (e.g. `"This is my quote" - Jimmy`)'.format(days)
) )
else: else:
return await ctx.edit( return await ctx.edit(content="No messages found in the last {!s} days.".format(days))
content="No messages found in the last {!s} days.".format(days)
)
file = await asyncio.to_thread( file = await asyncio.to_thread(
self.generate_pie_chart, self.generate_pie_chart, list(authors.keys()), list(authors.values()), merge_other
list(authors.keys()),
list(authors.values()),
merge_other
) )
return await ctx.edit( return await ctx.edit(
content="{:,} messages (out of {:,}) were filtered (didn't follow format?)".format( content="{:,} messages (out of {:,}) were filtered (didn't follow format?)".format(
filtered_messages, filtered_messages, total
total
), ),
file=file file=file,
) )
def _metacounter( def _metacounter(
self, self, messages: list[discord.Message], filter_func: Callable[[discord.Message], bool], *, now: datetime = None
messages: list[discord.Message],
filter_func: Callable[[discord.Message], bool],
*,
now: datetime = None
) -> dict[str, float | int]: ) -> dict[str, float | int]:
now = now or discord.utils.utcnow().replace(minute=0, second=0, microsecond=0) now = now or discord.utils.utcnow().replace(minute=0, second=0, microsecond=0)
counts = { counts = {
@ -227,6 +209,7 @@ class QuoteQuota(commands.Cog):
:param messages: The messages to process :param messages: The messages to process
:returns: The stats :returns: The stats
""" """
def is_truth(msg: discord.Message) -> bool: def is_truth(msg: discord.Message) -> bool:
if msg.author.id == 1101439218334576742: if msg.author.id == 1101439218334576742:
# if msg.created_at.timestamp() <= 1713202855.80234: # if msg.created_at.timestamp() <= 1713202855.80234:
@ -250,6 +233,7 @@ class QuoteQuota(commands.Cog):
:param messages: The messages to process :param messages: The messages to process
:returns: The stats :returns: The stats
""" """
def is_truth(msg: discord.Message) -> bool: def is_truth(msg: discord.Message) -> bool:
if msg.author.id == 1229496078726860921: if msg.author.id == 1229496078726860921:
# All the tate truths are already tagged. # All the tate truths are already tagged.
@ -268,14 +252,9 @@ class QuoteQuota(commands.Cog):
:param channel: The channel to process :param channel: The channel to process
:returns: The stats :returns: The stats
""" """
embed = discord.Embed( embed = discord.Embed(title="Truth Counts", color=discord.Color.blurple(), timestamp=discord.utils.utcnow())
title="Truth Counts",
color=discord.Color.blurple(),
timestamp=discord.utils.utcnow()
)
messages: list[discord.Message] = await channel.history( messages: list[discord.Message] = await channel.history(
limit=None, limit=None, after=discord.Object(1229487065117233203)
after=discord.Object(1229487065117233203)
).flatten() ).flatten()
trump_stats = await self._process_trump_truths(messages) trump_stats = await self._process_trump_truths(messages)
tate_stats = await self._process_tate_truths(messages) tate_stats = await self._process_tate_truths(messages)
@ -296,7 +275,7 @@ class QuoteQuota(commands.Cog):
f"**Last Week:** {tate_stats['week']:,} ({tate_stats['per_day']:.1f}/day)\n" f"**Last Week:** {tate_stats['week']:,} ({tate_stats['per_day']:.1f}/day)\n"
f"**Last Day:** {tate_stats['day']:,} ({tate_stats['per_hour']:.1f}/hour)\n" f"**Last Day:** {tate_stats['day']:,} ({tate_stats['per_hour']:.1f}/hour)\n"
f"**Last Hour:** {tate_stats['hour']:,} ({tate_stats['per_minute']:.1f}/min)" f"**Last Hour:** {tate_stats['hour']:,} ({tate_stats['per_minute']:.1f}/min)"
) ),
) )
return embed return embed
@ -314,7 +293,7 @@ class QuoteQuota(commands.Cog):
title="Counting truths, please wait.", title="Counting truths, please wait.",
description="This may take a minute depending on how insane Trump and Tate are feeling.", description="This may take a minute depending on how insane Trump and Tate are feeling.",
color=discord.Color.blurple(), color=discord.Color.blurple(),
timestamp=now timestamp=now,
) )
await ctx.respond(embed=embed) await ctx.respond(embed=embed)
embed = await self._process_all_messages(channel) embed = await self._process_all_messages(channel)

View file

@ -29,12 +29,12 @@ RESOLUTIONS = {
"480p": "854x480", "480p": "854x480",
"360p": "640x360", "360p": "640x360",
"240p": "426x240", "240p": "426x240",
"144p": "256x144" "144p": "256x144",
} }
_RES_OPTION = discord.Option( _RES_OPTION = discord.Option(
name="resolution", name="resolution",
description="The resolution of the browser, can be WxH, or a preset (e.g. 1080p).", description="The resolution of the browser, can be WxH, or a preset (e.g. 1080p).",
autocomplete=discord.utils.basic_autocomplete(tuple(RESOLUTIONS.keys())) autocomplete=discord.utils.basic_autocomplete(tuple(RESOLUTIONS.keys())),
) )
@ -64,9 +64,7 @@ class ScreenshotCog(commands.Cog):
"plugins.always_open_pdf_externally": False, "plugins.always_open_pdf_externally": False,
"download_restrictions": 3, "download_restrictions": 3,
} }
_chrome_options.add_experimental_option( _chrome_options.add_experimental_option("prefs", prefs)
"prefs", prefs
)
return _chrome_options return _chrome_options
def compress_png(self, input_file: io.BytesIO) -> io.BytesIO: def compress_png(self, input_file: io.BytesIO) -> io.BytesIO:
@ -100,11 +98,8 @@ class ScreenshotCog(commands.Cog):
load_timeout: int = 10, load_timeout: int = 10,
render_timeout: int = None, render_timeout: int = None,
eager: bool = None, eager: bool = None,
resolution: typing.Annotated[ resolution: typing.Annotated[str, _RES_OPTION] = "1440p",
str, use_proxy: bool = False,
_RES_OPTION
] = "1440p",
use_proxy: bool = False
): ):
"""Screenshots a webpage.""" """Screenshots a webpage."""
await ctx.defer() await ctx.defer()
@ -125,7 +120,7 @@ class ScreenshotCog(commands.Cog):
load_timeout, load_timeout,
render_timeout, render_timeout,
"eager" if eager else "lazy", "eager" if eager else "lazy",
resolution resolution,
) )
parsed = urlparse(url) parsed = urlparse(url)
await ctx.respond("Initialising...") await ctx.respond("Initialising...")
@ -139,11 +134,7 @@ class ScreenshotCog(commands.Cog):
else: else:
use_proxy = False use_proxy = False
service = await asyncio.to_thread(ChromeService) service = await asyncio.to_thread(ChromeService)
driver: webdriver.Chrome = await asyncio.to_thread( driver: webdriver.Chrome = await asyncio.to_thread(webdriver.Chrome, service=service, options=options)
webdriver.Chrome,
service=service,
options=options
)
driver.set_page_load_timeout(load_timeout) driver.set_page_load_timeout(load_timeout)
if resolution: if resolution:
resolution = RESOLUTIONS.get(resolution.lower(), resolution) resolution = RESOLUTIONS.get(resolution.lower(), resolution)
@ -228,7 +219,7 @@ class ScreenshotCog(commands.Cog):
f"Resolution: {resolution}" f"Resolution: {resolution}"
f"Used proxy: {use_proxy}", f"Used proxy: {use_proxy}",
colour=discord.Colour.dark_theme(), colour=discord.Colour.dark_theme(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
embed.set_image(url="attachment://" + fn) embed.set_image(url="attachment://" + fn)
return await ctx.edit(content=None, embed=embed, file=discord.File(file, filename=fn)) return await ctx.edit(content=None, embed=embed, file=discord.File(file, filename=fn))

View file

@ -59,11 +59,11 @@ class YTDLCog(commands.Cog):
# "max_filesize": (25 * 1024 * 1024) - 256 # "max_filesize": (25 * 1024 * 1024) - 256
} }
self.colours = { self.colours = {
"youtube.com": 0xff0000, "youtube.com": 0xFF0000,
"youtu.be": 0xff0000, "youtu.be": 0xFF0000,
"tiktok.com": 0x25F5EF, "tiktok.com": 0x25F5EF,
"instagram.com": 0xe1306c, "instagram.com": 0xE1306C,
"shronk.net": 0xFFF952 "shronk.net": 0xFFF952,
} }
async def _init_db(self): async def _init_db(self):
@ -90,7 +90,7 @@ class YTDLCog(commands.Cog):
format_id: str, format_id: str,
attachment_index: int = 0, attachment_index: int = 0,
*, *,
snip: typing.Optional[str] = None snip: typing.Optional[str] = None,
): ):
""" """
Saves a link to discord to prevent having to re-download it. Saves a link to discord to prevent having to re-download it.
@ -101,7 +101,7 @@ class YTDLCog(commands.Cog):
:param snip: The start and end time to snip the video. e.g. 00:00:00-00:10:00 :param snip: The start and end time to snip the video. e.g. 00:00:00-00:10:00
:return: The created hash key :return: The created hash key
""" """
snip = snip or '*' snip = snip or "*"
await self._init_db() await self._init_db()
async with aiosqlite.connect("./data/ytdl.db") as db: async with aiosqlite.connect("./data/ytdl.db") as db:
_hash = hashlib.md5(f"{webpage_url}:{format_id}:{snip}".encode()).hexdigest() _hash = hashlib.md5(f"{webpage_url}:{format_id}:{snip}".encode()).hexdigest()
@ -113,7 +113,7 @@ class YTDLCog(commands.Cog):
snip, snip,
message.channel.id, message.channel.id,
message.id, message.id,
attachment_index attachment_index,
) )
await db.execute( await db.execute(
""" """
@ -124,17 +124,12 @@ class YTDLCog(commands.Cog):
channel_id=excluded.channel_id, channel_id=excluded.channel_id,
attachment_index=excluded.attachment_index attachment_index=excluded.attachment_index
""", """,
(_hash, message.id, message.channel.id, webpage_url, format_id, attachment_index) (_hash, message.id, message.channel.id, webpage_url, format_id, attachment_index),
) )
await db.commit() await db.commit()
return _hash return _hash
async def get_saved( async def get_saved(self, webpage_url: str, format_id: str, snip: str) -> typing.Optional[str]:
self,
webpage_url: str,
format_id: str,
snip: str
) -> typing.Optional[str]:
""" """
Attempts to retrieve the attachment URL of a previously saved download. Attempts to retrieve the attachment URL of a previously saved download.
:param webpage_url: The webpage url :param webpage_url: The webpage url
@ -146,15 +141,10 @@ class YTDLCog(commands.Cog):
async with aiosqlite.connect("./data/ytdl.db") as db: async with aiosqlite.connect("./data/ytdl.db") as db:
_hash = hashlib.md5(f"{webpage_url}:{format_id}:{snip}".encode()).hexdigest() _hash = hashlib.md5(f"{webpage_url}:{format_id}:{snip}".encode()).hexdigest()
self.log.debug( self.log.debug(
"Attempting to find a saved download for '%s:%s:%s' (%r).", "Attempting to find a saved download for '%s:%s:%s' (%r).", webpage_url, format_id, snip, _hash
webpage_url,
format_id,
snip,
_hash
) )
cursor = await db.execute( cursor = await db.execute(
"SELECT message_id, channel_id, attachment_index FROM downloads WHERE key=?", "SELECT message_id, channel_id, attachment_index FROM downloads WHERE key=?", (_hash,)
(_hash,)
) )
entry = await cursor.fetchone() entry = await cursor.fetchone()
if not entry: if not entry:
@ -199,14 +189,10 @@ class YTDLCog(commands.Cog):
"-movflags", "-movflags",
"faststart", "faststart",
"-y", "-y",
str(new_file) str(new_file),
] ]
self.log.debug("Running command: ffmpeg %s", " ".join(args)) self.log.debug("Running command: ffmpeg %s", " ".join(args))
process = subprocess.run( process = subprocess.run(["ffmpeg", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
["ffmpeg", *args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
if process.returncode != 0: if process.returncode != 0:
raise RuntimeError(process.stderr.decode()) raise RuntimeError(process.stderr.decode())
return new_file return new_file
@ -217,14 +203,7 @@ class YTDLCog(commands.Cog):
async def yt_dl_command( async def yt_dl_command(
self, self,
ctx: discord.ApplicationContext, ctx: discord.ApplicationContext,
url: typing.Annotated[ url: typing.Annotated[str, discord.Option(str, description="The URL to download from.", required=True)],
str,
discord.Option(
str,
description="The URL to download from.",
required=True
)
],
user_format: typing.Annotated[ user_format: typing.Annotated[
typing.Optional[str], typing.Optional[str],
discord.Option( discord.Option(
@ -232,8 +211,8 @@ class YTDLCog(commands.Cog):
name="format", name="format",
description="The name of the format to download. Can also specify resolutions for youtube.", description="The name of the format to download. Can also specify resolutions for youtube.",
required=False, required=False,
default=None default=None,
) ),
], ],
audio_only: typing.Annotated[ audio_only: typing.Annotated[
bool, bool,
@ -243,15 +222,12 @@ class YTDLCog(commands.Cog):
description="Whether to convert result into an m4a file. Overwrites `format` if True.", description="Whether to convert result into an m4a file. Overwrites `format` if True.",
required=False, required=False,
default=False, default=False,
) ),
], ],
snip: typing.Annotated[ snip: typing.Annotated[
typing.Optional[str], typing.Optional[str],
discord.Option( discord.Option(description="A start and end position to trim. e.g. 00:00:00-00:10:00.", required=False),
description="A start and end position to trim. e.g. 00:00:00-00:10:00.", ],
required=False
)
]
): ):
"""Runs yt-dlp and outputs into discord.""" """Runs yt-dlp and outputs into discord."""
await ctx.defer() await ctx.defer()
@ -279,10 +255,7 @@ class YTDLCog(commands.Cog):
# Overwrite format here to be best audio under 25 megabytes. # Overwrite format here to be best audio under 25 megabytes.
chosen_format = "ba[filesize<20M]" chosen_format = "ba[filesize<20M]"
# Also force sorting by the best audio bitrate first. # Also force sorting by the best audio bitrate first.
options["format_sort"] = [ options["format_sort"] = ["abr", "br"]
"abr",
"br"
]
options["postprocessors"] = [ options["postprocessors"] = [
{"key": "FFmpegExtractAudio", "preferredquality": "96", "preferredcodec": "best"} {"key": "FFmpegExtractAudio", "preferredquality": "96", "preferredcodec": "best"}
] ]
@ -290,9 +263,7 @@ class YTDLCog(commands.Cog):
options["paths"] = paths options["paths"] = paths
with yt_dlp.YoutubeDL(options) as downloader: with yt_dlp.YoutubeDL(options) as downloader:
await ctx.respond( await ctx.respond(embed=discord.Embed().set_footer(text="Downloading (step 1/10)"))
embed=discord.Embed().set_footer(text="Downloading (step 1/10)")
)
try: try:
# noinspection PyTypeChecker # noinspection PyTypeChecker
extracted_info = await asyncio.to_thread(downloader.extract_info, url, download=False) extracted_info = await asyncio.to_thread(downloader.extract_info, url, download=False)
@ -309,7 +280,7 @@ class YTDLCog(commands.Cog):
"fps": "1", "fps": "1",
"vcodec": "error", "vcodec": "error",
"acodec": "error", "acodec": "error",
"filesize": 0 "filesize": 0,
} }
title = "error" title = "error"
description = str(e) description = str(e)
@ -362,10 +333,12 @@ class YTDLCog(commands.Cog):
title=title, title=title,
description=description, description=description,
url=webpage_url, url=webpage_url,
colour=self.colours.get(domain, discord.Colour.og_blurple()) colour=self.colours.get(domain, discord.Colour.og_blurple()),
).set_footer(text="Downloading (step 2/10)").set_thumbnail(url=thumbnail_url)
) )
previous = await self.get_saved(webpage_url, extracted_info["format_id"], snip or '*') .set_footer(text="Downloading (step 2/10)")
.set_thumbnail(url=thumbnail_url)
)
previous = await self.get_saved(webpage_url, extracted_info["format_id"], snip or "*")
if previous: if previous:
await ctx.edit( await ctx.edit(
content=previous, content=previous,
@ -375,10 +348,8 @@ class YTDLCog(commands.Cog):
colour=discord.Colour.green(), colour=discord.Colour.green(),
timestamp=discord.utils.utcnow(), timestamp=discord.utils.utcnow(),
url=previous, url=previous,
fields=[ fields=[discord.EmbedField(name="URL", value=previous, inline=False)],
discord.EmbedField(name="URL", value=previous, inline=False) ).set_image(url=previous),
]
).set_image(url=previous)
) )
return return
try: try:
@ -410,7 +381,7 @@ class YTDLCog(commands.Cog):
"Failed to locate downloaded file. Was supposed to be looking for a file extension of " "Failed to locate downloaded file. Was supposed to be looking for a file extension of "
"%r amongst files %r, however none were found.", "%r amongst files %r, however none were found.",
extracted_info["ext"], extracted_info["ext"],
list(map(str, temp_dir.iterdir())) list(map(str, temp_dir.iterdir())),
) )
return await ctx.edit( return await ctx.edit(
embed=discord.Embed( embed=discord.Embed(
@ -418,7 +389,7 @@ class YTDLCog(commands.Cog):
description="Failed to locate downloaded video file.\n" description="Failed to locate downloaded video file.\n"
f"Files: {', '.join(list(map(str, temp_dir.iterdir())))}", f"Files: {', '.join(list(map(str, temp_dir.iterdir())))}",
colour=discord.Colour.red(), colour=discord.Colour.red(),
url=webpage_url url=webpage_url,
) )
) )
@ -454,7 +425,7 @@ class YTDLCog(commands.Cog):
"-y", "-y",
"-strict", "-strict",
"2", "2",
str(new_file) str(new_file),
] ]
async with ctx.channel.typing(): async with ctx.channel.typing():
await ctx.edit( await ctx.edit(
@ -462,15 +433,12 @@ class YTDLCog(commands.Cog):
title=f"Trimming from {trim_start} to {trim_end}.", title=f"Trimming from {trim_start} to {trim_end}.",
description="Please wait, this may take a couple of minutes.", description="Please wait, this may take a couple of minutes.",
colour=discord.Colour.og_blurple(), colour=discord.Colour.og_blurple(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
) )
self.log.debug("Running command: 'ffmpeg %s'", " ".join(args)) self.log.debug("Running command: 'ffmpeg %s'", " ".join(args))
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
"ffmpeg", "ffmpeg", *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
) )
stdout, stderr = await process.communicate() stdout, stderr = await process.communicate()
self.log.debug("STDOUT:\n%r", stdout.decode()) self.log.debug("STDOUT:\n%r", stdout.decode())
@ -481,7 +449,7 @@ class YTDLCog(commands.Cog):
title="Error", title="Error",
description=f"Trimming failed:\n```\n{stderr.decode()}\n```", description=f"Trimming failed:\n```\n{stderr.decode()}\n```",
colour=discord.Colour.red(), colour=discord.Colour.red(),
url=webpage_url url=webpage_url,
) )
) )
file = new_file file = new_file
@ -498,7 +466,7 @@ class YTDLCog(commands.Cog):
title="Error", title="Error",
description=f"File is too large to upload ({round(size_bytes / 1024 / 1024)}MB).", description=f"File is too large to upload ({round(size_bytes / 1024 / 1024)}MB).",
colour=discord.Colour.red(), colour=discord.Colour.red(),
url=webpage_url url=webpage_url,
) )
) )
size_megabits = (size_bytes * 8) / 1024 / 1024 size_megabits = (size_bytes * 8) / 1024 / 1024
@ -509,7 +477,7 @@ class YTDLCog(commands.Cog):
title="Uploading...", title="Uploading...",
description=f"ETA <t:{int(eta_seconds + discord.utils.utcnow().timestamp()) + 2}:R>", description=f"ETA <t:{int(eta_seconds + discord.utils.utcnow().timestamp()) + 2}:R>",
colour=discord.Colour.og_blurple(), colour=discord.Colour.og_blurple(),
timestamp=discord.utils.utcnow() timestamp=discord.utils.utcnow(),
) )
) )
try: try:
@ -520,10 +488,10 @@ class YTDLCog(commands.Cog):
description="Views: {:,} | Likes: {:,}".format(views or 0, likes or 0), description="Views: {:,} | Likes: {:,}".format(views or 0, likes or 0),
colour=discord.Colour.green(), colour=discord.Colour.green(),
timestamp=discord.utils.utcnow(), timestamp=discord.utils.utcnow(),
url=webpage_url url=webpage_url,
),
) )
) await self.save_link(msg, webpage_url, chosen_format_id, snip=snip or "*")
await self.save_link(msg, webpage_url, chosen_format_id, snip=snip or '*')
except discord.HTTPException as e: except discord.HTTPException as e:
self.log.error(e, exc_info=True) self.log.error(e, exc_info=True)
return await ctx.edit( return await ctx.edit(
@ -531,7 +499,7 @@ class YTDLCog(commands.Cog):
title="Error", title="Error",
description=f"Upload failed:\n```\n{e}\n```", description=f"Upload failed:\n```\n{e}\n```",
colour=discord.Colour.red(), colour=discord.Colour.red(),
url=webpage_url url=webpage_url,
) )
) )

View file

@ -13,7 +13,7 @@ if (Path.cwd() / ".git").exists():
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
text=True, text=True,
check=True check=True,
).stdout.strip() ).stdout.strip()
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
log.debug("Unable to auto-detect running version using git.", exc_info=True) log.debug("Unable to auto-detect running version using git.", exc_info=True)
@ -23,29 +23,15 @@ else:
VERSION = "unknown" VERSION = "unknown"
try: try:
CONFIG = toml.load('config.toml') CONFIG = toml.load("config.toml")
CONFIG.setdefault("logging", {}) CONFIG.setdefault("logging", {})
CONFIG.setdefault("jimmy", {}) CONFIG.setdefault("jimmy", {})
CONFIG.setdefault("ollama", {}) CONFIG.setdefault("ollama", {})
CONFIG.setdefault("rss", {"meta": {"channel": None}}) CONFIG.setdefault("rss", {"meta": {"channel": None}})
CONFIG.setdefault("screenshot", {}) CONFIG.setdefault("screenshot", {})
CONFIG.setdefault("quote_a", {"channel": None}) CONFIG.setdefault("quote_a", {"channel": None})
CONFIG.setdefault( CONFIG.setdefault("server", {"host": "0.0.0.0", "port": 8080, "channel": 1032974266527907901})
"server", CONFIG.setdefault("redis", {"host": "redis", "port": 6379, "decode_responses": True})
{
"host": "0.0.0.0",
"port": 8080,
"channel": 1032974266527907901
}
)
CONFIG.setdefault(
"redis",
{
"host": "redis",
"port": 6379,
"decode_responses": True
}
)
except FileNotFoundError: except FileNotFoundError:
cwd = Path.cwd() cwd = Path.cwd()
log.critical("Unable to locate config.toml in %s.", cwd, exc_info=True) log.critical("Unable to locate config.toml in %s.", cwd, exc_info=True)

View file

@ -39,7 +39,7 @@ class KumaThread(KillableThread):
def calculate_backoff(self) -> float: def calculate_backoff(self) -> float:
rnd = random.uniform(0, 1) rnd = random.uniform(0, 1)
retries = min(self.retries, 1000) retries = min(self.retries, 1000)
t = (2 * 2 ** retries) + rnd t = (2 * 2**retries) + rnd
self.log.debug("Backoff: 2 * (2 ** %d) + %f = %f", retries, rnd, t) self.log.debug("Backoff: 2 * (2 ** %d) + %f = %f", retries, rnd, t)
# T can never exceed self.interval # T can never exceed self.interval
return max(0, min(self.interval, t)) return max(0, min(self.interval, t))
@ -79,18 +79,10 @@ logging.basicConfig(
show_time=False, show_time=False,
show_path=False, show_path=False,
markup=True, markup=True,
console=Console( console=Console(width=cols, height=lns),
width=cols,
height=lns
)
), ),
FileHandler( FileHandler(filename=CONFIG["logging"].get("file", "jimmy.log"), mode="a", encoding="utf-8", errors="replace"),
filename=CONFIG["logging"].get("file", "jimmy.log"), ],
mode="a",
encoding="utf-8",
errors="replace"
)
]
) )
for logger in CONFIG["logging"].get("suppress", []): for logger in CONFIG["logging"].get("suppress", []):
logging.getLogger(logger).setLevel(logging.WARNING) logging.getLogger(logger).setLevel(logging.WARNING)
@ -106,8 +98,7 @@ class Client(commands.Bot):
async def start(self, token: str, *, reconnect: bool = True) -> None: async def start(self, token: str, *, reconnect: bool = True) -> None:
if CONFIG["jimmy"].get("uptime_kuma_url"): if CONFIG["jimmy"].get("uptime_kuma_url"):
self.uptime_thread = KumaThread( self.uptime_thread = KumaThread(
CONFIG["jimmy"]["uptime_kuma_url"], CONFIG["jimmy"]["uptime_kuma_url"], CONFIG["jimmy"].get("uptime_kuma_interval", 60.0)
CONFIG["jimmy"].get("uptime_kuma_interval", 60.0)
) )
self.uptime_thread.start() self.uptime_thread.start()
await super().start(token, reconnect=reconnect) await super().start(token, reconnect=reconnect)
@ -124,7 +115,7 @@ bot = Client(
case_insensitive=True, case_insensitive=True,
strip_after_prefix=True, strip_after_prefix=True,
debug_guilds=CONFIG["jimmy"].get("debug_guilds"), debug_guilds=CONFIG["jimmy"].get("debug_guilds"),
intents=discord.Intents.all() intents=discord.Intents.all(),
) )
for ext in ("ytdl", "net", "screenshot", "ollama", "ffmeta", "quote_quota", "auto_responder"): for ext in ("ytdl", "net", "screenshot", "ollama", "ffmeta", "quote_quota", "auto_responder"):
@ -163,8 +154,7 @@ async def on_application_command_error(ctx: discord.ApplicationContext, exc: Exc
for page in paginator.pages: for page in paginator.pages:
await ctx.respond(page) await ctx.respond(page)
else: else:
await ctx.respond(f"An error occurred while processing your command. Please try again later.\n" await ctx.respond(f"An error occurred while processing your command. Please try again later.\n" f"{exc}")
f"{exc}")
@bot.listen() @bot.listen()
@ -183,9 +173,7 @@ async def delete_message(ctx: discord.ApplicationContext, message: discord.Messa
if message.author != bot.user: if message.author != bot.user:
return await ctx.respond("I don't have permission to delete messages in this channel.", delete_after=30) return await ctx.respond("I don't have permission to delete messages in this channel.", delete_after=30)
log.info( log.info("%s deleted message %s>%s: %r", ctx.author, ctx.channel.name, message.id, message.content)
"%s deleted message %s>%s: %r", ctx.author, ctx.channel.name, message.id, message.content
)
await message.delete(delay=3) await message.delete(delay=3)
await ctx.respond(f"\N{white heavy check mark} Deleted message by {message.author.display_name}.") await ctx.respond(f"\N{white heavy check mark} Deleted message by {message.author.display_name}.")
await ctx.delete(delay=15) await ctx.delete(delay=15)