Merge remote-tracking branch 'origin/master'
Some checks failed
Build and Publish / build_and_publish (push) Has been cancelled

This commit is contained in:
Nexus 2024-07-04 01:53:16 +01:00
commit ddc101b486
Signed by: nex
GPG key ID: 0FA334385D0B689F
8 changed files with 212 additions and 42 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.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.url="https://git.i-am.nexus/nex/college-bot-v2"
LABEL org.opencontainers.image.licenses AGPL-3.0 LABEL org.opencontainers.image.licenses="AGPL-3.0"
LABEL org.opencontainers.image.title "College Bot v2" LABEL org.opencontainers.image.title="College Bot v2"
LABEL org.opencontainers.image.description "Version 2 of jimmy." LABEL org.opencontainers.image.description="Version 2 of jimmy."
WORKDIR /app WORKDIR /app
RUN DEBIAN_FRONTEND=noninteractive apt-get update
# Install chrome dependencies # 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 \ webext-ublock-origin-chromium \
gconf-service \ gconf-service \
libasound2 \ libasound2 \
@ -49,15 +47,10 @@ fonts-liberation \
libappindicator1 \ libappindicator1 \
libnss3 \ libnss3 \
lsb-release \ lsb-release \
xdg-utils xdg-utils \
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
chromium-common \ chromium-common \
chromium-driver \ chromium-driver \
chromium-sandbox chromium-sandbox \
# Install general utilities
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
traceroute \ traceroute \
iputils-ping \ iputils-ping \
dnsutils \ dnsutils \
@ -65,19 +58,13 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
git \ git \
whois \ whois \
curl \ curl \
libmagic-dev libmagic-dev \
nmap \
# Install image dependencies ffmpeg
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
COPY requirements.txt /tmp/requirements.txt 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/ COPY ./src/ /app/
CMD ["/app/venv/bin/python", "main.py"] CMD ["python", "main.py"]

View file

@ -9,6 +9,11 @@ services:
- ./jimmy.log:/app/jimmy.log - ./jimmy.log:/app/jimmy.log
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
- jimmy-data:/app/data - 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: extra_hosts:
- host.docker.internal:host-gateway - host.docker.internal:host-gateway
depends_on: depends_on:

View file

@ -20,3 +20,4 @@ python-magic~=0.4
aiofiles~=23.2 aiofiles~=23.2
fuzzywuzzy[speedup]~=0.18 fuzzywuzzy[speedup]~=0.18
tortoise-orm[asyncpg]~=0.21 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 asyncio
import io import io
import shutil
import typing import typing
from collections.abc import Iterable from collections.abc import Iterable
import logging import logging
@ -105,6 +111,12 @@ class AutoResponder(commands.Cog):
:param uri: The URI to transcode :param uri: The URI to transcode
:return: A transcoded file :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 last_reaction: str | None = None
def update_reaction(new: str | None = None) -> None: def update_reaction(new: str | None = None) -> None:
@ -118,9 +130,7 @@ class AutoResponder(commands.Cog):
last_reaction = new last_reaction = new
self.log.info("Waiting for transcode lock to release") self.log.info("Waiting for transcode lock to release")
async with self.transcode_lock: async with self.transcode_lock:
cog: FFMeta = self.bot.get_cog("FFMeta") cog = FFMeta(self.bot)
if not cog:
raise RuntimeError("FFMeta cog not loaded")
if not isinstance(uri, str): if not isinstance(uri, str):
uri = str(uri) uri = str(uri)
@ -240,6 +250,12 @@ class AutoResponder(commands.Cog):
*domains: str, *domains: str,
additional: Iterable[str] = None additional: Iterable[str] = None
) -> 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: if self.allow_hevc_to_h264 is False:
self.log.debug("hevc_to_h264 is disabled, not engaging.") self.log.debug("hevc_to_h264 is disabled, not engaging.")
return return

View file

@ -2,7 +2,6 @@
This module is only meant to be loaded during election times. This module is only meant to be loaded during election times.
""" """
import asyncio import asyncio
import datetime
import logging import logging
import random import random
import re import re

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 asyncio
import io import io
import json import json
import logging import logging
import pathlib import pathlib
import shutil
import sys import sys
import tempfile import tempfile
import typing import typing
@ -56,6 +62,8 @@ class FFMeta(commands.Cog):
@commands.slash_command() @commands.slash_command()
async def ffprobe(self, ctx: discord.ApplicationContext, url: str = None, attachment: discord.Attachment = None): async def ffprobe(self, ctx: discord.ApplicationContext, url: str = None, attachment: discord.Attachment = None):
"""Runs ffprobe on a given URL or attachment""" """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 url is None:
if attachment is None: if attachment is None:
return await ctx.respond("No URL or attachment provided") return await ctx.respond("No URL or attachment provided")
@ -142,6 +150,10 @@ class FFMeta(commands.Cog):
] = False, ] = False,
): ):
"""Converts a given URL or attachment to an Opus file""" """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: if bitrate == 0:
bitrate = 0.5 bitrate = 0.5
if mono: if mono:
@ -244,6 +256,8 @@ class FFMeta(commands.Cog):
rbh = Path("assets/right-behind-you.ogg").resolve() rbh = Path("assets/right-behind-you.ogg").resolve()
if not rbh.exists(): if not rbh.exists():
return await ctx.respond("The file `right-behind-you.ogg` is missing.") 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"): if not image.content_type.startswith("image"):
return await ctx.respond("That's not an image!") return await ctx.respond("That's not an image!")
with tempfile.NamedTemporaryFile(suffix=Path(image.filename).suffix) as temp: with tempfile.NamedTemporaryFile(suffix=Path(image.filename).suffix) as temp:

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 asyncio
import io import io
import json import json
import os import os
import re import re
import shlex
import shutil
import tempfile
import time import time
import typing import typing
import warnings
from pathlib import Path from pathlib import Path
import discord import discord
@ -14,6 +24,8 @@ from dns import asyncresolver
from rich.console import Console from rich.console import Console
from rich.tree import Tree from rich.tree import Tree
from conf import CONFIG from conf import CONFIG
from superpaste import HstSHBackend
from superpaste.backends import GenericFile
class GetFilteredTextView(discord.ui.View): class GetFilteredTextView(discord.ui.View):
@ -43,6 +55,8 @@ class NetworkCog(commands.Cog):
@commands.slash_command() @commands.slash_command()
async def ping(self, ctx: discord.ApplicationContext, target: str = None): async def ping(self, ctx: discord.ApplicationContext, target: str = None):
"""Get the bot's latency, or the network latency to a target.""" """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: if target is None:
return await ctx.respond(f"Pong! {round(self.bot.latency * 1000)}ms") return await ctx.respond(f"Pong! {round(self.bot.latency * 1000)}ms")
else: else:
@ -69,6 +83,8 @@ class NetworkCog(commands.Cog):
@commands.slash_command() @commands.slash_command()
async def whois(self, ctx: discord.ApplicationContext, target: str): async def whois(self, ctx: discord.ApplicationContext, target: str):
"""Get information about a user.""" """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): async def run_command(with_disclaimer: bool = False):
args = [] if with_disclaimer else ["-H"] args = [] if with_disclaimer else ["-H"]
@ -215,6 +231,8 @@ class NetworkCog(commands.Cog):
await ctx.defer() await ctx.defer()
if re.search(r"\s+", url): if re.search(r"\s+", url):
return await ctx.respond("URL cannot contain spaces.") 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"] args = ["sudo", "-E", "-n", "traceroute"]
flags = { flags = {
@ -346,6 +364,132 @@ class NetworkCog(commands.Cog):
await ctx.respond(embed=embed) 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): def setup(bot):
bot.add_cog(NetworkCog(bot)) bot.add_cog(NetworkCog(bot))

View file

@ -3,6 +3,7 @@ import functools
import hashlib import hashlib
import logging import logging
import math import math
import shutil
import time import time
import datetime import datetime
@ -190,6 +191,8 @@ class YTDLCog(commands.Cog):
:param file: The file to convert :param file: The file to convert
:return: The converted file :return: The converted file
""" """
if not shutil.which("ffmpeg"):
raise RuntimeError("ffmpeg is not installed.")
new_file = file.with_suffix(".m4a") new_file = file.with_suffix(".m4a")
args = [ args = [
"-vn", "-vn",
@ -319,8 +322,9 @@ class YTDLCog(commands.Cog):
else: else:
chosen_format = user_format chosen_format = user_format
ffmpeg_installed = bool(shutil.which("ffmpeg"))
options.setdefault("postprocessors", []) options.setdefault("postprocessors", [])
if audio_only: if audio_only and ffmpeg_installed:
# 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.
@ -332,7 +336,7 @@ class YTDLCog(commands.Cog):
options["format"] = chosen_format options["format"] = chosen_format
options["paths"] = paths options["paths"] = paths
if subtitles: if subtitles and ffmpeg_installed:
subtitles, burn = subtitles.split("+", 1) if "+" in subtitles else (subtitles, "0") subtitles, burn = subtitles.split("+", 1) if "+" in subtitles else (subtitles, "0")
burn = burn[0].lower() in ("y", "1", "t") burn = burn[0].lower() in ("y", "1", "t")
if subtitles.lower() == "auto": if subtitles.lower() == "auto":