Merge branch 'master' of i-am.nexus:nex/college-bot-v2
All checks were successful
Build and Publish / build_and_publish (push) Successful in 1m25s

This commit is contained in:
Nexus 2024-07-05 00:11:58 +01:00
commit da8423bb97
11 changed files with 352 additions and 50 deletions

View file

@ -1,17 +1,15 @@
FROM python:3.11-slim
FROM python:3.12-slim
LABEL org.opencontainers.image.source https://git.i-am.nexus/nex/college-bot-v2
LABEL org.opencontainers.image.url https://git.i-am.nexus/nex/college-bot-v2
LABEL org.opencontainers.image.licenses AGPL-3.0
LABEL org.opencontainers.image.title "College Bot v2"
LABEL org.opencontainers.image.description "Version 2 of jimmy."
LABEL org.opencontainers.image.source="https://git.i-am.nexus/nex/college-bot-v2"
LABEL org.opencontainers.image.url="https://git.i-am.nexus/nex/college-bot-v2"
LABEL org.opencontainers.image.licenses="AGPL-3.0"
LABEL org.opencontainers.image.title="College Bot v2"
LABEL org.opencontainers.image.description="Version 2 of jimmy."
WORKDIR /app
RUN DEBIAN_FRONTEND=noninteractive apt-get update
# Install chrome dependencies
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends \
webext-ublock-origin-chromium \
gconf-service \
libasound2 \
@ -49,35 +47,24 @@ fonts-liberation \
libappindicator1 \
libnss3 \
lsb-release \
xdg-utils
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
chromium-common \
chromium-driver \
chromium-sandbox
# Install general utilities
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
traceroute \
iputils-ping \
dnsutils \
net-tools \
git \
whois \
curl \
libmagic-dev
# Install image dependencies
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ffmpeg \
imagemagick
RUN python3 -m venv /app/venv
RUN /app/venv/bin/pip install --upgrade --no-input pip wheel setuptools
xdg-utils \
chromium-common \
chromium-driver \
chromium-sandbox \
traceroute \
iputils-ping \
dnsutils \
net-tools \
git \
whois \
curl \
libmagic-dev \
nmap \
ffmpeg
COPY requirements.txt /tmp/requirements.txt
RUN /app/venv/bin/pip install -Ur /tmp/requirements.txt --no-input
RUN pip install -Ur /tmp/requirements.txt --no-input
COPY ./src/ /app/
CMD ["/app/venv/bin/python", "main.py"]
CMD ["python", "main.py"]

View file

@ -34,6 +34,7 @@ allowed_models = [
# Note that every model has a tag called "latest" which is the most recent version.
]
base_url = "http://ollama:11434/api" # this is the default if you're running via docker compose
is_gpu = false
[ollama.external]
owner = 421698654189912064

View file

@ -9,6 +9,11 @@ services:
- ./jimmy.log:/app/jimmy.log
- /dev/dri:/dev/dri
- jimmy-data:/app/data
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- /usr/bin/nmap:/usr/bin/nmap:ro
- /usr/bin/ffmpeg:/usr/bin/ffmpeg:ro
- /usr/bin/ffprobe:/usr/bin/ffprobe:ro
extra_hosts:
- host.docker.internal:host-gateway
depends_on:

View file

@ -20,3 +20,4 @@ python-magic~=0.4
aiofiles~=23.2
fuzzywuzzy[speedup]~=0.18
tortoise-orm[asyncpg]~=0.21
superpaste @ git+https://github.com/nexy7574/superpaste.git@e31eca6

View file

@ -1,5 +1,11 @@
# In order to use commands in this system, the following things must be installed (ideally in /usr/local/bin):
# - `ffmpeg`, usually `ffmpeg`, sometimes `ffmpeg-full`.
# - /dev/dri should be passed through to the container for hardware acceleration.
# - `ffprobe`, usually `ffmpeg`, sometimes `ffmpeg-full`.
# In docker, you can just bind these as volumes, and read-only too.
import asyncio
import io
import shutil
import typing
from collections.abc import Iterable
import logging
@ -105,6 +111,12 @@ class AutoResponder(commands.Cog):
:param uri: The URI to transcode
:return: A transcoded file
"""
if not shutil.which("ffmpeg"):
self.log.error("ffmpeg not installed")
return
if not shutil.which("ffprobe"):
self.log.error("ffprobe not installed")
return
last_reaction: str | None = None
def update_reaction(new: str | None = None) -> None:
@ -118,9 +130,7 @@ class AutoResponder(commands.Cog):
last_reaction = new
self.log.info("Waiting for transcode lock to release")
async with self.transcode_lock:
cog: FFMeta = self.bot.get_cog("FFMeta")
if not cog:
raise RuntimeError("FFMeta cog not loaded")
cog = FFMeta(self.bot)
if not isinstance(uri, str):
uri = str(uri)
@ -240,6 +250,12 @@ class AutoResponder(commands.Cog):
*domains: str,
additional: Iterable[str] = None
) -> None:
if not shutil.which("ffmpeg"):
self.log.error("ffmpeg not installed")
return
if not shutil.which("ffprobe"):
self.log.error("ffprobe not installed")
return
if self.allow_hevc_to_h264 is False:
self.log.debug("hevc_to_h264 is disabled, not engaging.")
return

View file

@ -2,14 +2,15 @@
This module is only meant to be loaded during election times.
"""
import asyncio
import datetime
import logging
import random
import datetime
import re
import discord
import httpx
from bs4 import BeautifulSoup
from discord.ext import commands
from discord.ext import commands, tasks
SPAN_REGEX = re.compile(
@ -39,10 +40,66 @@ class ElectionCog(commands.Cog):
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 "
"Safari/537.36",
}
ETA = datetime.datetime(
2024,
7,
4,
23,
30,
tzinfo=datetime.datetime.now().astimezone().tzinfo
)
def __init__(self, bot):
self.bot = bot
self.bot: commands.Bot = bot
self.log = logging.getLogger("jimmy.cogs.election")
self.countdown_message = None
self.check_election.start()
def cog_unload(self) -> None:
self.check_election.cancel()
@tasks.loop(minutes=1)
async def check_election(self):
if not self.bot.is_ready():
await self.bot.wait_until_ready()
guild = self.bot.get_guild(994710566612500550)
if not guild:
return self.log.error("Nonsense guild not found. Can't do countdown.")
channel = discord.utils.get(guild.text_channels, name="countdown")
if not channel:
return self.log.error("Countdown channel not found.")
await asyncio.sleep(random.randint(0, 10))
now = discord.utils.utcnow()
diff = (self.ETA - now).total_seconds()
if diff < -86400:
return self.log.debug("Countdown long expired.")
if diff > 3600:
hours, remainder = map(round, divmod(diff, 3600))
minutes, seconds = map(round, divmod(remainder, 60))
message = f"+ {hours} hours, {minutes} minutes, and {seconds} seconds."
elif diff > 60:
minutes, seconds = map(round, divmod(diff, 60))
message = f"+ {minutes} minutes and {seconds} seconds."
elif diff >= 0:
message = f"+ {round(diff)} seconds."
else:
message = "Results time!"
if self.countdown_message:
try:
return await self.countdown_message.edit(
content=f"```diff\n{message}```"
)
except discord.HTTPException:
self.log.exception("Failed to edit countdown message.")
self.countdown_message = await channel.send(
f"```diff\n{message}```"
)
self.log.debug("Sent countdown message")
def process_soup(self, soup: BeautifulSoup) -> dict[str, list[int]] | None:
good_soups = list(soup.find_all(attrs={"data-testid": "election-banner-results-bar"}))

View file

@ -1,8 +1,14 @@
# In order to use commands in this system, the following things must be installed (ideally in /usr/local/bin):
# - `ffmpeg`, usually `ffmpeg`, sometimes `ffmpeg-full`.
# - /dev/dri should be passed through to the container for hardware acceleration.
# - `ffprobe`, usually `ffmpeg`, sometimes `ffmpeg-full`.
# In docker, you can just bind these as volumes, and read-only too.
import asyncio
import io
import json
import logging
import pathlib
import shutil
import sys
import tempfile
import typing
@ -56,6 +62,8 @@ class FFMeta(commands.Cog):
@commands.slash_command()
async def ffprobe(self, ctx: discord.ApplicationContext, url: str = None, attachment: discord.Attachment = None):
"""Runs ffprobe on a given URL or attachment"""
if not shutil.which("ffprobe"):
return await ctx.respond("ffprobe is not installed on this system.")
if url is None:
if attachment is None:
return await ctx.respond("No URL or attachment provided")
@ -142,6 +150,10 @@ class FFMeta(commands.Cog):
] = False,
):
"""Converts a given URL or attachment to an Opus file"""
if not shutil.which("ffmpeg"):
return await ctx.respond("ffmpeg is not installed on this system.")
if not shutil.which("ffprobe"):
return await ctx.respond("ffprobe is not installed on this system.")
if bitrate == 0:
bitrate = 0.5
if mono:
@ -244,6 +256,8 @@ class FFMeta(commands.Cog):
rbh = Path("assets/right-behind-you.ogg").resolve()
if not rbh.exists():
return await ctx.respond("The file `right-behind-you.ogg` is missing.")
if not shutil.which("ffmpeg"):
return await ctx.respond("ffmpeg is not installed on this system.")
if not image.content_type.startswith("image"):
return await ctx.respond("That's not an image!")
with tempfile.NamedTemporaryFile(suffix=Path(image.filename).suffix) as temp:

View file

@ -13,6 +13,7 @@ class MeterCog(commands.Cog):
self.cache = {}
@commands.slash_command(name="gay-meter")
@discord.guild_only()
async def gay_meter(self, ctx: discord.ApplicationContext, user: discord.User = None):
"""Checks how gay someone is"""
user = user or ctx.user
@ -21,23 +22,34 @@ class MeterCog(commands.Cog):
for i in range(0, 125, 25):
await ctx.edit(content="Calculating... %d%%" % i)
await asyncio.sleep(random.randint(1, 30) / 10)
pct = user.id % 100
if random.randint(0, 1):
pct = sum((user.id, ctx.user.id, ctx.channel.id, ctx.guild.id)) % 400
else:
pct = user.id % 100
await ctx.edit(content=f"{user.mention} is {pct}% gay.")
@commands.slash_command(name="penis-length")
async def penis_meter(self, ctx: discord.ApplicationContext, user: discord.User = None):
"""Checks the length of someone's penis."""
user = user or ctx.user
n = self.cache.get(user) or random.randint(0, 100)
if random.randint(0, 1):
pct = sum((user.id, ctx.user.id)) % 200
else:
pct = user.id % 125
pct = pct
chunks = ["8"]
chunks += ["="] * n
chunks += ["="] * pct
chunks.append("B")
self.cache[user] = n
inch = pct * 0.3937008
im = "inch"
if pct > 30.48:
im = "ft"
inch = pct * 0.0328084
return await ctx.respond(
embed=discord.Embed(
title=f"{user.display_name}'s penis length:",
description="%d cm\n%s" % (n, "".join(chunks))
description="%d cm (%.2f%s)\n%s" % (pct, inch, im, "".join(chunks))
)
)

View file

@ -1,10 +1,20 @@
# In order to use commands in this system, the following things must be installed (ideally in /usr/local/bin):
# - `ping`, usually iputils-ping
# - `whois`, usually whois
# - `traceroute`, usually traceroute
# - `nmap`, usually nmap
# In docker, you can just bind these as volumes, and read-only too.
import asyncio
import io
import json
import os
import re
import shlex
import shutil
import tempfile
import time
import typing
import warnings
from pathlib import Path
import discord
@ -14,6 +24,8 @@ from dns import asyncresolver
from rich.console import Console
from rich.tree import Tree
from conf import CONFIG
from superpaste import HstSHBackend
from superpaste.backends import GenericFile
class GetFilteredTextView(discord.ui.View):
@ -43,6 +55,8 @@ class NetworkCog(commands.Cog):
@commands.slash_command()
async def ping(self, ctx: discord.ApplicationContext, target: str = None):
"""Get the bot's latency, or the network latency to a target."""
if not shutil.which("ping"):
return await ctx.respond("Ping is not installed on this system.")
if target is None:
return await ctx.respond(f"Pong! {round(self.bot.latency * 1000)}ms")
else:
@ -69,6 +83,8 @@ class NetworkCog(commands.Cog):
@commands.slash_command()
async def whois(self, ctx: discord.ApplicationContext, target: str):
"""Get information about a user."""
if not shutil.which("whois"):
return await ctx.respond("Whois is not installed on this system.")
async def run_command(with_disclaimer: bool = False):
args = [] if with_disclaimer else ["-H"]
@ -215,6 +231,8 @@ class NetworkCog(commands.Cog):
await ctx.defer()
if re.search(r"\s+", url):
return await ctx.respond("URL cannot contain spaces.")
if not shutil.which("traceroute"):
return await ctx.respond("Traceroute is not installed on this system.")
args = ["sudo", "-E", "-n", "traceroute"]
flags = {
@ -346,6 +364,132 @@ class NetworkCog(commands.Cog):
await ctx.respond(embed=embed)
@commands.slash_command()
@commands.max_concurrency(1, commands.BucketType.user)
async def nmap(
self,
ctx: discord.ApplicationContext,
target: str,
technique: typing.Annotated[
str,
discord.Option(
str,
choices=[
discord.OptionChoice(name="TCP SYN", value="S"),
discord.OptionChoice(name="TCP Connect", value="T"),
discord.OptionChoice(name="TCP ACK", value="A"),
discord.OptionChoice(name="TCP Window", value="W"),
discord.OptionChoice(name="TCP Maimon", value="M"),
discord.OptionChoice(name="UDP", value="U"),
discord.OptionChoice(name="TCP Null", value="N"),
discord.OptionChoice(name="TCP FIN", value="F"),
discord.OptionChoice(name="TCP XMAS", value="X"),
],
default="T"
)
] = "T",
treat_all_hosts_online: bool = False,
service_scan: bool = False,
fast_mode: bool = False,
enable_os_detection: bool = False,
timing: typing.Annotated[
int,
discord.Option(
int,
description="Timing template to use 0 is slowest, 5 is fastest.",
choices=[0, 1, 2, 3, 4, 5],
default=3
)
] = 3,
ports: str = None
):
"""Runs nmap on a target. You cannot specify multiple targets."""
await ctx.defer()
if len(shlex.split(target)) > 1:
return await ctx.respond("You cannot specify multiple targets.")
if not shutil.which("nmap"):
warnings.warn(
"NMAP is not installed on this system, so the /nmap command is not enabled. "
"If you would like to enable it, install nmap, or mount the binary as a volume in docker."
)
return await ctx.respond("Nmap is not installed on this system.")
is_superuser = os.getuid() == 0
if technique != "T" and not is_superuser:
return await ctx.respond("Only `TCP Connect` can be used on this system.")
if enable_os_detection and not is_superuser:
return await ctx.respond("OS detection is not available on this system.")
with tempfile.TemporaryDirectory(prefix=f"nmap-{ctx.user.id}-{discord.utils.utcnow().timestamp():.0f}") as tmp:
tmp_dir = Path(tmp)
args = [
"nmap",
"-oA",
str(tmp_dir.resolve() / target),
"-T",
str(timing),
"-s" + technique,
"--reason",
"--noninteractive"
]
if treat_all_hosts_online:
args.append("-Pn")
if service_scan:
args.append("-sV")
if fast_mode:
args.append("-F")
if ports:
args.extend(("-p", ports))
if enable_os_detection:
args.append("-O")
args.append(target)
await ctx.respond(
embed=discord.Embed(
title="Running nmap...",
description="Command:\n"
"```{}```".format(shlex.join(args)),
)
)
process = await asyncio.create_subprocess_exec(
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
_, stderr = await process.communicate()
files = [discord.File(x, filename=x.name + ".txt") for x in tmp_dir.iterdir()]
if not files:
if len(stderr) <= 4089:
return await ctx.edit(
embed=discord.Embed(
title="Nmap failed.",
description="```\n" + stderr.decode() + "```",
color=discord.Color.red()
)
)
result = await HstSHBackend().async_create_paste(
GenericFile(stderr.decode())
)
return await ctx.edit(
embed=discord.Embed(
title="Nmap failed.",
description=f"Output was too long. [View full output]({result.url})",
color=discord.Color.red()
)
)
await ctx.edit(
embed=discord.Embed(
title="Nmap finished!",
description="Result files are attached.\n"
"* `gnmap` is 'greppable'\n"
"* `xml` is XML output\n"
"* `nmap` is normal output",
color=discord.Color.green()
),
files=files
)
def setup(bot):
bot.add_cog(NetworkCog(bot))

View file

@ -461,6 +461,28 @@ class PromptSelector(discord.ui.View):
@discord.ui.button(label="Done", style=discord.ButtonStyle.success, custom_id="done")
async def done(self, btn: discord.ui.Button, interaction: Interaction):
self.ctx.interaction = interaction
self.stop()
class ConfirmCPURun(discord.ui.View):
def __init__(self, ctx: discord.ApplicationContext):
super().__init__(timeout=600, disable_on_timeout=True)
self.ctx = ctx
self.proceed = False
async def interaction_check(self, interaction: Interaction) -> bool:
return interaction.user == self.ctx.user
@discord.ui.button(label="Run on CPU", style=discord.ButtonStyle.primary, custom_id="cpu")
async def run_on_cpu(self, btn: discord.ui.Button, interaction: Interaction):
await interaction.response.defer(invisible=True)
self.proceed = True
self.stop()
@discord.ui.button(label="Abort", style=discord.ButtonStyle.primary, custom_id="gpu")
async def run_on_gpu(self, btn: discord.ui.Button, interaction: Interaction):
await interaction.response.defer(invisible=True)
self.stop()
@ -637,6 +659,24 @@ class Ollama(commands.Cog):
for i in range(10):
try:
server = self.next_server(tried)
if server and CONFIG["ollama"]["server"].get("is_gpu", False) is not True:
cf = ConfirmCPURun(ctx)
await ctx.edit(
embed=discord.Embed(
title=f"Server {server} is available, but...",
description="It is CPU only, which means it is very slow and will likely crash.\n"
"If you really want, you can continue your generation, using this "
"server. Be aware though, once in motion, it cannot be stopped.\n\n"
""
"Continue?",
color=discord.Color.red(),
),
view=cf
)
await cf.wait()
await ctx.edit(view=None)
if cf.proceed:
break
except RuntimeError:
tried.add(server)
continue
@ -752,6 +792,27 @@ class Ollama(commands.Cog):
key = os.urandom(6).hex()
if server_config.get("is_gpu", False) is False:
cf2 = ConfirmCPURun(ctx)
await ctx.edit(
content=None,
embed=discord.Embed(
title="Before you continue",
description="You've selected a CPU-only server. This will be really slow. This will also likely"
" bring the host to a halt. Consider the pain the CPU is about to endure. "
"Are you super"
" sure you want to continue? You can run `h!ollama-status` to see what servers are"
" available.",
color=discord.Color.red(),
),
view=cf2
)
await cf2.wait()
if cf2.proceed is False:
return await ctx.edit("Cancelled.", embed=None, view=None)
else:
await ctx.edit(view=None)
embed = discord.Embed(
title="Generating response...",
description=">>> ",

View file

@ -3,6 +3,7 @@ import functools
import hashlib
import logging
import math
import shutil
import time
import datetime
@ -190,6 +191,8 @@ class YTDLCog(commands.Cog):
:param file: The file to convert
:return: The converted file
"""
if not shutil.which("ffmpeg"):
raise RuntimeError("ffmpeg is not installed.")
new_file = file.with_suffix(".m4a")
args = [
"-vn",
@ -319,8 +322,9 @@ class YTDLCog(commands.Cog):
else:
chosen_format = user_format
ffmpeg_installed = bool(shutil.which("ffmpeg"))
options.setdefault("postprocessors", [])
if audio_only:
if audio_only and ffmpeg_installed:
# Overwrite format here to be best audio under 25 megabytes.
chosen_format = "ba[filesize<20M]"
# Also force sorting by the best audio bitrate first.
@ -332,7 +336,7 @@ class YTDLCog(commands.Cog):
options["format"] = chosen_format
options["paths"] = paths
if subtitles:
if subtitles and ffmpeg_installed:
subtitles, burn = subtitles.split("+", 1) if "+" in subtitles else (subtitles, "0")
burn = burn[0].lower() in ("y", "1", "t")
if subtitles.lower() == "auto":