Compare commits

..
2 Commits
Author SHA1 Message Date
marc 780cd76090 storage urls that work
Docker / build (push) Canceled after 0s
Docker / test-python-versions (macos-latest, 3.10) (push) Canceled after 0s
Docker / test-python-versions (macos-latest, 3.8) (push) Canceled after 0s
Docker / test-python-versions (macos-latest, 3.9) (push) Canceled after 0s
Docker / test-python-versions (ubuntu-latest, 3.10) (push) Canceled after 0s
Docker / test-python-versions (ubuntu-latest, 3.8) (push) Canceled after 0s
Docker / test-python-versions (ubuntu-latest, 3.9) (push) Canceled after 0s
Docker / test-python-versions (windows-latest, 3.10) (push) Canceled after 0s
Docker / test-python-versions (windows-latest, 3.8) (push) Canceled after 0s
Docker / test-python-versions (windows-latest, 3.9) (push) Canceled after 0s
2024-12-28 01:10:58 +01:00
marc 50fbdc698f show channel id 2024-12-28 01:09:25 +01:00
61 changed files with 1799 additions and 1583 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
FROM python:3.13-alpine AS alpine
FROM python:3.8.13-alpine AS alpine
LABEL maintainer="marc.partensky@proton.me"
LABEL image="https://hub.docker.com/r/marcpartensky/discord-bot"
LABEL source="https://github.com/marcpartensky/discord-bot"
WORKDIR /app
COPY README.md LICENSE pyproject.toml uv.lock build.sh requirements.txt ./
COPY README.md LICENSE pyproject.toml uv.lock build.sh ./
COPY ./discord_bot discord_bot
RUN ./build.sh
WORKDIR /app/discord_bot
-28
View File
@@ -1,28 +0,0 @@
set dotenv-load := true
image := "marcpartensky/discord-bot"
# lance le bot en local
run:
uv run --directory discord_bot python .
# installe les deps
sync:
uv sync
# build l'image
build:
podman build -t {{image}}:local .
# lance l'image
docker-run:
podman run --rm -it --env-file .env {{image}}:local
# logs du service
logs:
journalctl -fu podman-discord-bot
# relance le service
restart:
sudo systemctl reset-failed podman-discord-bot
sudo systemctl restart podman-discord-bot
+2 -2
View File
@@ -9,8 +9,8 @@ wait
export PATH="$PATH:$HOME/.cargo/bin"
# uv sync -v &
# uv pip compile pyproject.toml -o requirements.txt
# uv export --format requirements.txt > requirements.txt
uv pip compile pyproject.toml -o requirements.txt
# uv export > requirements.txt
# sed -i '1,2d' requirements.txt
pip install -r requirements.txt &
apk add ffmpeg postgresql p7zip &
+6 -28
View File
@@ -51,15 +51,11 @@ class Main(commands.Bot):
self.good_invited = "bon invité"
self.bad_invited = "mauvais invité"
self.help_every = 5
self._status = itertools.cycle([]) # Will be populated in load_status()
async def setup_hook(self) -> None:
"""Automatically run by discord.py."""
print("DEBUG: setup_hook called")
await self.load_cogs()
print("DEBUG: load_cogs finished")
self.load_status()
print("DEBUG: load_status finished")
await self.load_cogs()
def load_status(self):
"""Charge les statuts."""
@@ -83,7 +79,7 @@ class Main(commands.Bot):
bar.update(i)
if filename.endswith(".py"):
print(filename)
self.load_extension(f"cogs.{filename[:-3]}")
await self.load_extension(f"cogs.{filename[:-3]}")
@commands.command()
async def environment(self, ctx: commands.Context):
@@ -163,16 +159,11 @@ class Main(commands.Bot):
- discord.ActivityType.watching,
- discord.ActivityType.competing
"""
try:
status_name = next(self._status)
await self.change_presence(
activity=discord.Activity(
type=discord.ActivityType.watching, name=status_name
)
await self.change_presence(
activity=discord.Activity(
type=discord.ActivityType.watching, name=next(self._status)
)
except StopIteration:
# Pas de statuts disponibles, on ne fait rien
pass
)
@commands.Cog.listener()
async def on_ready(self):
@@ -180,18 +171,6 @@ class Main(commands.Bot):
self.change_status.start()
print(f"{self.user} is connected to Discord!")
# Démarrer le serveur HTTP API
print(f"DEBUG: on_ready - Looking for API cog...")
api_cog = self.get_cog("API")
print(f"DEBUG: API cog = {api_cog}")
if api_cog and hasattr(api_cog, '_start_server'):
print(f"DEBUG: Starting HTTP server from on_ready...")
if not hasattr(api_cog, '_server_started') or not api_cog._server_started:
await api_cog._start_server()
api_cog._server_started = True
else:
print("DEBUG: API cog not found or no _start_server method")
@commands.Cog.listener()
async def on_member_join(self, member: discord.Member):
"""Venue un nouveau membre."""
@@ -309,7 +288,6 @@ async def main():
bot = Main(intents=intents, command_prefix=prefix, case_insensitive=False, client_id=client_id)
async with bot:
await bot.setup_hook()
await bot.start(token)
+2 -2
View File
@@ -363,5 +363,5 @@ class Admin(commands.Cog):
await ctx.send(msg)
def setup(bot):
bot.add_cog(Admin(bot))
async def setup(bot):
await bot.add_cog(Admin(bot))
+2 -2
View File
@@ -70,5 +70,5 @@ class Advertisement(commands.Cog):
await ctx.send(f"Le prix de {name} est maintenant à {price}.")
def setup(bot):
bot.add_cog(Advertisement(bot))
async def setup(bot):
await bot.add_cog(Advertisement(bot))
+107 -111
View File
@@ -2,145 +2,141 @@
"""HTTP web service to interact with the bot."""
__author__ = "Marc Partensky"
import asyncio
import functools
import os
import traceback
# from config.config import cluster, access, check
# from utils.check import check
# from utils import tools
import os, time
import typing
import uuid
import traceback
import discord
from aiohttp import web
import aiofiles
from discord.ext import commands
import server
from aiohttp import web
def json_errors(handler):
"""Mappe les exceptions Discord vers des réponses JSON."""
@functools.wraps(handler)
async def wrapper(self, request: web.Request):
try:
return await handler(self, request)
except KeyError as e:
return web.json_response({"error": f"Missing required field: {e}"}, status=400)
except discord.NotFound:
return web.json_response({"error": "Not found"}, status=404)
except discord.Forbidden:
return web.json_response({"error": "Forbidden"}, status=403)
except Exception as e:
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
return wrapper
MAX_CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB
class API(commands.Cog):
"""HTTP web service to interact with the bot."""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.host = os.environ.get("DISCORD_BOT_HOST", "0.0.0.0")
self.port = int(os.environ.get("DISCORD_BOT_PORT", "8050"))
self.token = os.environ.get("DISCORD_BOT_API_TOKEN")
self.server = server.HTTPServer(
bot=self.bot,
host=os.environ.get("DISCORD_BOT_HOST") or "localhost",
port=int(os.environ.get("DISCORD_BOT_PORT") or "8000"),
)
self.guild_id = int(os.environ.get(
"DISCORD_BOT_GUILD_ID"
) or "550332212340326428")
self.contexts: typing.Dict[str, commands.Context] = {}
self.runner: typing.Optional[web.AppRunner] = None
self.site: typing.Optional[web.TCPSite] = None
self._lock = asyncio.Lock()
self.channel_id = int(os.environ["CHANNEL_ID_NOTIF"])
self.bot.loop.create_task(self._start_server())
self.app = web.Application(middlewares=[self._auth])
self.app.router.add_post("/send/user", self.send_user)
self.app.router.add_post("/send/channel", self.send_channel)
self.app.router.add_post("/command/channel", self.command_channel)
self.app.router.add_get("/debug/bot", self.debug_bot)
self.app.router.add_get("/live", self.live)
async def _start_server(self):
"""Task to run the HTTP server."""
await self.bot.wait_until_ready()
await self.server.start()
@web.middleware
async def _auth(self, request: web.Request, handler):
if self.token and request.path != "/live":
if request.headers.get("Authorization") != f"Bearer {self.token}":
return web.json_response({"error": "Unauthorized"}, status=401)
return await handler(request)
@commands.Cog.listener()
async def on_ready(self):
"""on_ready peut se redéclencher à chaque reconnexion: idempotent."""
async with self._lock:
if self.site is not None:
return
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.host, self.port)
try:
await self.site.start()
except OSError:
self.site = None
await self.runner.cleanup()
self.runner = None
raise
print(f"HTTP server listening on {self.host}:{self.port}")
def cog_unload(self):
if self.runner:
asyncio.create_task(self.runner.cleanup())
async def _send(self, request: web.Request, fetch):
body = await request.json()
target = await fetch(int(body["id"]))
await target.send(body["message"])
return web.json_response({"text": "Successfully sent message."})
@json_errors
@server.add_route(path="/send/user", method="POST", cog="API")
async def send_user(self, request: web.Request):
"""Send a DM to a Discord user."""
return await self._send(request, self.bot.fetch_user)
@json_errors
async def send_channel(self, request: web.Request):
"""Send a message to a Discord channel."""
return await self._send(request, self.bot.fetch_channel)
@json_errors
async def command_channel(self, request: web.Request):
"""Invoke a bot command on a saved context."""
"""API home path."""
body = await request.json()
command = self.bot.get_command(body["cmd"])
if command is None:
return web.json_response({
"error": "No match found.",
"commands": [
{"name": c.name, "doc": c.short_doc} for c in self.bot.commands
],
}, status=404)
user: discord.User = await self.bot.fetch_user(body["id"])
await user.send(body["message"])
return web.json_response(
data={"text": "Successfully sent message."}, status=200
)
ctx = self.contexts.get(body["ctx"])
if ctx is None:
return web.json_response(
{"error": f"Unknown context: {body['ctx']}"}, status=404
)
@server.add_route(path="/send/channel", method="POST", cog="API")
async def send_channel(self, request: web.Request):
"""API home path."""
body = await request.json()
channel: discord.TextChannel = await self.bot.fetch_channel(body["id"])
await channel.send(body["message"])
self.bot.commands
return web.json_response(
data={"text": "Successfully sent message."}, status=200
)
ctx.args = body.get("args", [])
ctx.kwargs = body.get("kwargs", {})
result = await command.invoke(ctx)
return web.json_response({"text": str(result)})
@server.add_route(path="/command/channel", method="POST", cog="API")
async def command_channel(self, request: web.Request):
"""API home path."""
body = await request.json()
ctx_id = body["ctx"]
cmd = body["cmd"]
if "args" in body:
args = body["args"]
else:
args = []
if "kwargs" in body:
kwargs = body["kwargs"]
else:
kwargs = {}
channel: discord.TextChannel = await self.bot.fetch_channel(body["id"])
command_list = []
for command in self.bot.commands:
command_list.append(dict(name=command.name, doc=command.short_doc))
print(command.name)
if command.name == cmd:
# context = self.build_context(channel, args)
ctx = self.contexts[ctx_id]
try:
print(ctx.args)
print(ctx.kwargs)
ctx.args = args
ctx.kwargs = kwargs
# command.clean_params
# for param in command.params:
# print(command._parse_arguments(ctx))
# await command.prepare(ctx)
# await converter._construct_default(ctx)
# await command.callback(context, __p1=command.clean_params)
# print(command.clean_params)
# print(command.params)
# print(command.invoke())
# ctx.args = args
result = await command.invoke(ctx)
return web.json_response(data={"text": str(result)}, status=200)
except Exception as exception:
traceback.print_exc()
return web.json_response(
data={"error": str(exception), "doc": command.short_doc},
status=500,
)
return web.json_response(
data={"error": "No match found.", "commands": command_list}, status=404
)
@server.add_route(path="/debug/bot", method="GET", cog="API")
async def debug_bot(self, request: web.Request):
"""Return bot debug information."""
return web.json_response({
"id": str(self.bot.user.id) if self.bot.user else None,
"name": str(self.bot.user) if self.bot.user else None,
"guilds": len(self.bot.guilds),
"cogs": list(self.bot.cogs.keys()),
})
"""API home path."""
body = await request.json()
return web.json_response(data=self.bot, status=200)
@server.add_route(path="/live", method="GET", cog="API")
async def live(self, request: web.Request):
"""Healthcheck."""
return web.Response(text="OK")
"""API home path."""
return web.json_response(text="OK", status=200)
@commands.command(name="save-context")
async def save_context(self, ctx: commands.Context, context_id: typing.Optional[str] = None):
async def save_context(self, ctx: commands.Context, context_id: str or None):
"""Save a context since it is really hard to build one from scratch."""
context_id = context_id or str(uuid.uuid1())
self.contexts[context_id] = ctx
await ctx.send(f"> Saved context as **{context_id}**")
@commands.Cog.listener()
async def on_message(self, msg: discord.Message):
"""Nettoie les messages des salons innapropriés, dès que
ceux-ci sont postés."""
if msg.author.bot:
return
def setup(bot: commands.Bot):
bot.add_cog(API(bot))
async def setup(bot: commands.Bot):
await bot.add_cog(API(bot))
+3 -3
View File
@@ -256,7 +256,7 @@ class Backup(commands.Cog):
def get_cron_expression(self, trigger: CronTrigger) -> str:
"""Retourne une représentation de l'expression Cron sous forme de chaîne de caractères."""
return str(trigger)
return f"{trigger.minute} {trigger.hour} {trigger.day} {trigger.month} {trigger.day_of_week}"
async def build_job_embed(self) -> discord.Embed:
"""Affiche les jobs configurés."""
@@ -351,5 +351,5 @@ class Backup(commands.Cog):
except Exception as e:
await ctx.send(f"Erreur: {str(e)}")
def setup(bot):
bot.add_cog(Backup(bot))
async def setup(bot):
await bot.add_cog(Backup(bot))
+2 -2
View File
@@ -541,5 +541,5 @@ class Bank(commands.Cog):
await ctx.send(msg)
def setup(bot):
bot.add_cog(Bank(bot))
async def setup(bot):
await bot.add_cog(Bank(bot))
+2 -2
View File
@@ -376,5 +376,5 @@ class Basic(commands.Cog):
"""Affiche l'id du channel."""
await ctx.send("> "+str(ctx.channel.id))
def setup(bot):
bot.add_cog(Basic(bot))
async def setup(bot):
await bot.add_cog(Basic(bot))
+2 -2
View File
@@ -329,5 +329,5 @@ class BlackJack(commands.Cog):
await self.stop(ctx)
def setup(bot):
bot.add_cog(BlackJack(bot))
async def setup(bot):
await bot.add_cog(BlackJack(bot))
+2 -2
View File
@@ -186,5 +186,5 @@ class Casino(commands.Cog):
return account
def setup(bot):
bot.add_cog(Casino(bot))
async def setup(bot):
await bot.add_cog(Casino(bot))
+2 -2
View File
@@ -226,5 +226,5 @@ class Code(commands.Cog):
await ctx.send(text)
def setup(bot):
bot.add_cog(Code(bot))
async def setup(bot):
await bot.add_cog(Code(bot))
+2 -2
View File
@@ -220,8 +220,8 @@ class Commandes(commands.Cog):
await self.search(ctx, *map(lambda r: r.strip(), req.strip().split("|")), regex=False, show_cmd=False)
def setup(bot: commands.Bot):
bot.add_cog(Commandes(bot))
async def setup(bot: commands.Bot):
await bot.add_cog(Commandes(bot))
# TODO: Test the cog
+2 -2
View File
@@ -155,5 +155,5 @@ class Puissance4(commands.Cog):
await self.message.edit(content="Personne ne gagne." + msg)
def setup(bot):
bot.add_cog(Puissance4(bot))
async def setup(bot):
await bot.add_cog(Puissance4(bot))
+2 -2
View File
@@ -175,6 +175,6 @@ class CountDown(commands.Cog):
await self.bot.wait_until_ready()
def setup(bot):
async def setup(bot):
"""Setup the CountDown cog."""
bot.add_cog(CountDown(bot))
await bot.add_cog(CountDown(bot))
+2 -2
View File
@@ -58,5 +58,5 @@ class Debug(commands.Cog):
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(Debug(bot))
async def setup(bot):
await bot.add_cog(Debug(bot))
+3 -3
View File
@@ -23,7 +23,7 @@ class Docker(commands.Cog):
"""Initialize the ialab cog."""
super().__init__(**kwargs)
self.bot: commands.Bot = bot
self.url: str = os.environ.get("DOCKER_API_URL", "")
self.url: str = os.environ["DOCKER_API_URL"]
self.color = discord.Color(0x4EB3F2)
self.states = dict(
exited=discord.Color.red(),
@@ -324,6 +324,6 @@ class Docker(commands.Cog):
return await ctx.send(f"```md\n{text}\n```")
def setup(bot: commands.Bot):
async def setup(bot: commands.Bot):
"""Setup the ialab cog."""
bot.add_cog(Docker(bot))
await bot.add_cog(Docker(bot))
+2 -2
View File
@@ -40,8 +40,8 @@ class Farm(commands.Cog):
del self.farms[name]
def setup(bot):
bot.add_cog(Farm(bot))
async def setup(bot):
await bot.add_cog(Farm(bot))
"""
+2 -2
View File
@@ -145,5 +145,5 @@ class Game(commands.Cog):
await ctx.send(msg)
def setup(bot):
bot.add_cog(Game(bot))
async def setup(bot):
await bot.add_cog(Game(bot))
+2 -2
View File
@@ -102,5 +102,5 @@ class Git(commands.Cog):
await ctx.send("\n".join([cmd] + out))
def setup(bot):
bot.add_cog(Git(bot))
async def setup(bot):
await bot.add_cog(Git(bot))
+2 -2
View File
@@ -109,6 +109,6 @@ class Groq(commands.Cog):
return answer
def setup(bot):
async def setup(bot):
"""Setup the Shell cog."""
bot.add_cog(Groq(bot))
await bot.add_cog(Groq(bot))
-250
View File
@@ -1,250 +0,0 @@
#!/usr/bin/env python
"""Pont vers un gateway Hermes Agent dedie (profil `discord-bridge`).
Reserve aux membres qui portent le role Discord `hermes` (nom configurable via
`HERMES_BRIDGE_ROLE_NAME`) et aux masters du bot. Parle a l'API server
OpenAI-compatible d'Hermes (services/hermes cote NixOS, loopback uniquement).
L'API server est un listener UNIQUE par hote : un profil secondaire n'a pas son
propre port, il est servi sur le listener du profil default sous /p/<profil>/...
Variables d'environnement (injectees par systemd, cf. services/discord-bot) :
HERMES_BRIDGE_URL defaut http://127.0.0.1:8642/p/discord-bridge/v1/chat/completions
HERMES_BRIDGE_API_KEY bearer token (sops secrets/discord-hermes-bridge.yml)
HERMES_BRIDGE_ROLE_NAME nom du role Discord autorise (defaut "hermes")
HERMES_BRIDGE_ASK_PORT port loopback de la surface de test (defaut 8052)
DISCORD_BOT_API_TOKEN si pose, exige en Bearer (convention de cogs/api.py)
"""
import asyncio
import os
import typing
import discord
import requests
from aiohttp import web
from discord.ext import commands
from config.config import prefix, masters
from utils.tools import create_role_if_missing
DEFAULT_BRIDGE_URL = "http://127.0.0.1:8642/p/discord-bridge/v1/chat/completions"
BRIDGE_URL = os.environ.get("HERMES_BRIDGE_URL", DEFAULT_BRIDGE_URL)
BRIDGE_KEY = os.environ.get("HERMES_BRIDGE_API_KEY")
ROLE_NAME = os.environ.get("HERMES_BRIDGE_ROLE_NAME", "hermes")
MAX_HISTORY = 20 # messages (user + assistant) gardes par utilisateur
REQUEST_TIMEOUT = 120
SYSTEM_PROMPT = (
"Tu es Hermes, relaye ici depuis Discord (pont dedie, profil discord-bridge). "
"Reponds de facon concise, adaptee au format Discord (2000 caracteres max par "
"message), sans mise en forme lourde."
)
class Hermes(commands.Cog):
"""Bridge to a dedicated Hermes Agent gateway (OpenAI-compatible API)."""
def __init__(self, bot: commands.Bot):
"""Initialize the cog with the bot."""
self.bot = bot
self.color: discord.Color = discord.Color.blurple()
self.histories: typing.Dict[int, list] = {}
# Surface de test locale (loopback) : joindre Hermes par HTTP sans client
# Discord, et LIRE sa reponse en JSON. Sert au test de bout en bout du
# pont depuis le serveur (systemd injecte le port).
self.ask_host = os.environ.get("HERMES_BRIDGE_ASK_HOST", "127.0.0.1")
self.ask_port = int(os.environ.get("HERMES_BRIDGE_ASK_PORT", "8052"))
self.api_token = os.environ.get("DISCORD_BOT_API_TOKEN")
self.ask_runner: typing.Optional[web.AppRunner] = None
self.ask_site: typing.Optional[web.TCPSite] = None
self.ask_lock = asyncio.Lock()
# --- autorisation ------------------------------------------------------
@staticmethod
def _has_role(user: discord.abc.User) -> bool:
"""Vrai si l'utilisateur porte le role Hermes (compare sans la casse).
En DM, py-cord rend un `Member` (avec ses roles) quand il a le serveur en
cache, sinon un simple `User` sans attribut `roles` : d'ou le getattr.
"""
return any(
role.name.lower() == ROLE_NAME.lower()
for role in getattr(user, "roles", None) or []
)
def _author_allowed(self, author: discord.abc.User) -> bool:
"""Autorise les masters du bot et les porteurs du role, en DM comme en serveur."""
if author.id in masters:
return True
if self._has_role(author):
return True
for guild in self.bot.guilds:
member = guild.get_member(author.id)
if member and self._has_role(member):
return True
return False
# --- appel Hermes ------------------------------------------------------
async def _ask(self, user_id: int, message: str) -> str:
"""Envoie un message a Hermes et renvoie sa reponse."""
if not BRIDGE_KEY:
return "> Pont Hermes non configure (HERMES_BRIDGE_API_KEY manquant)."
history = self.histories.setdefault(user_id, [])
history.append({"role": "user", "content": message})
payload = {
"model": "discord-bridge",
"messages": [{"role": "system", "content": SYSTEM_PROMPT}]
+ history[-MAX_HISTORY:],
"stream": False,
}
headers = {
"Authorization": f"Bearer {BRIDGE_KEY}",
"Content-Type": "application/json",
}
def _do_request():
"""Requete bloquante, executee dans un thread pour ne pas figer la boucle."""
response = requests.post(
BRIDGE_URL, json=payload, headers=headers, timeout=REQUEST_TIMEOUT
)
response.raise_for_status()
return response.json()
data = await asyncio.to_thread(_do_request)
answer = data["choices"][0]["message"]["content"] or "> (reponse vide)"
history.append({"role": "assistant", "content": answer})
return answer
# --- ecoute Discord ----------------------------------------------------
@commands.Cog.listener(name="on_message")
async def on_message(self, msg: discord.Message):
"""En DM : chaque message est transmis directement a Hermes."""
if msg.author == self.bot.user or msg.author.bot:
return
if not isinstance(msg.channel, discord.DMChannel):
return
if msg.content.startswith(prefix):
return
if not self._author_allowed(msg.author):
return
async with msg.channel.typing():
try:
answer = await self._ask(msg.author.id, msg.content)
except Exception as exc: # noqa: BLE001
answer = f"> Erreur pont Hermes : {exc}"
await self.send_long(msg.channel, answer)
# --- commandes ---------------------------------------------------------
@commands.command(name="hermes")
async def hermes(self, ctx: commands.Context, *, message: str):
"""Parle a Hermes (masters et porteurs du role hermes)."""
if ctx.guild and not self._author_allowed(ctx.author):
return await ctx.send(f"> Acces refuse : reserve au role `{ROLE_NAME}`.")
async with ctx.typing():
try:
answer = await self._ask(ctx.author.id, message)
except Exception as exc: # noqa: BLE001
answer = f"> Erreur pont Hermes : {exc}"
await self.send_long(ctx.channel, answer)
@commands.command(name="hermes-reset")
async def hermes_reset(self, ctx: commands.Context):
"""Oublie l'historique de conversation courant avec Hermes."""
if ctx.guild and not self._author_allowed(ctx.author):
return
self.histories.pop(ctx.author.id, None)
await ctx.send("> Historique Hermes reinitialise.")
@commands.command(name="hermes-setup")
async def hermes_setup(self, ctx: commands.Context):
"""Cree le role Discord hermes et l'attribue aux masters du bot."""
if ctx.author.id not in masters:
return await ctx.send("> Acces refuse : reserve aux masters.")
if not ctx.guild:
return await ctx.send("> Commande utilisable uniquement en serveur.")
await create_role_if_missing(ctx, ROLE_NAME, self.color)
await ctx.send(f"> Role `{ROLE_NAME}` pret.")
# --- surface de test locale (loopback) ---------------------------------
@web.middleware
async def _ask_auth(self, request: web.Request, handler):
"""Meme convention que cogs/api.py : Bearer exige seulement si le token est pose."""
if self.api_token and request.path != "/live":
if request.headers.get("Authorization") != f"Bearer {self.api_token}":
return web.json_response({"error": "Unauthorized"}, status=401)
return await handler(request)
@commands.Cog.listener(name="on_ready")
async def start_ask_server(self):
"""Demarre la surface de test (idempotent : on_ready peut se redeclencher)."""
async with self.ask_lock:
if self.ask_site is not None:
return
app = web.Application(middlewares=[self._ask_auth])
app.router.add_get("/live", self.ask_live)
app.router.add_post("/hermes/ask", self.ask_http)
self.ask_runner = web.AppRunner(app)
await self.ask_runner.setup()
site = web.TCPSite(self.ask_runner, self.ask_host, self.ask_port)
try:
await site.start()
except OSError:
await self.ask_runner.cleanup()
self.ask_runner = None
raise
self.ask_site = site
print(f"Hermes ask server listening on {self.ask_host}:{self.ask_port}")
def cog_unload(self):
"""Arrete la surface de test."""
if self.ask_runner:
asyncio.create_task(self.ask_runner.cleanup())
async def ask_live(self, request: web.Request):
"""Healthcheck."""
return web.Response(text="OK")
async def ask_http(self, request: web.Request):
"""POST {"message": "..."} -> {"answer": "..."} (historique par user_id)."""
try:
body = await request.json()
except Exception: # noqa: BLE001
return web.json_response({"error": "JSON invalide."}, status=400)
body = body or {}
message = body.get("message")
if not message:
return web.json_response({"error": "Champ 'message' manquant."}, status=400)
try:
user_id = int(body.get("user_id") or (masters[0] if masters else 0))
except (TypeError, ValueError):
return web.json_response({"error": "user_id invalide."}, status=400)
try:
answer = await self._ask(user_id, message)
except Exception as exc: # noqa: BLE001
return web.json_response(
{"error": f"{type(exc).__name__}: {exc}"}, status=502
)
return web.json_response({"answer": answer, "user_id": user_id})
# --- utilitaires -------------------------------------------------------
@staticmethod
async def send_long(channel, text: str):
"""Decoupe les reponses de plus de 2000 caracteres (limite Discord)."""
if not text:
text = "> (reponse vide)"
for i in range(0, len(text), 1990):
await channel.send(text[i : i + 1990])
def setup(bot: commands.Bot):
"""Setup du cog Hermes."""
bot.add_cog(Hermes(bot))
+2 -2
View File
@@ -81,6 +81,6 @@ class IALab(commands.Cog):
await self.say(ctx, d["answer"], "en")
def setup(bot):
async def setup(bot):
"""Setup the ialab cog."""
bot.add_cog(IALab(bot))
await bot.add_cog(IALab(bot))
+2 -2
View File
@@ -215,5 +215,5 @@ class Jail(commands.Cog):
# @commands.
def setup(bot):
bot.add_cog(Jail(bot))
async def setup(bot):
await bot.add_cog(Jail(bot))
+2 -2
View File
@@ -20,6 +20,6 @@ class Junior(commands.Cog):
self.bot = bot
def setup(bot):
async def setup(bot):
"""Setup the ialab cog."""
bot.add_cog(Junior(bot))
await bot.add_cog(Junior(bot))
+2 -2
View File
@@ -38,5 +38,5 @@ class LeaderBoard(commands.Cog):
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(LeaderBoard(bot))
async def setup(bot):
await bot.add_cog(LeaderBoard(bot))
+2 -2
View File
@@ -153,6 +153,6 @@ class Leekwars(commands.Cog):
await ctx.send(msg)
def setup(bot):
async def setup(bot):
"""Setup the leekwars cog."""
bot.add_cog(Leekwars(bot))
await bot.add_cog(Leekwars(bot))
-112
View File
@@ -1,112 +0,0 @@
#!/bin/env python
from config.config import access, cluster
from discord.ext import commands, tasks
from discord.ext.commands import MessageNotFound
import os
import discord
import yaml
ROLE = "Library"
class Library(commands.Cog):
"""Store config on discord servers as attachments."""
tmp_directory = os.environ.get("TMP_DIRECTORY") or "/tmp"
def __init__(self, bot: commands.Bot):
self.bot = bot
self.filename = "config.yml"
self.filepath = f"/tmp/{self.filename}"
self.channel_id = int(os.environ.get("CHANNEL_ID_CONFIG") or 0)
# @property
# def storage(self):
# return self.bot.get_cog("Storage")
@property
def channel(self) -> discord.TextChannel:
return self.bot.get_channel(self.channel_id)
async def create_config(self):
"""Créer un fichier de config pour la librairie si une existe pas déjà."""
config = dict(library=dict(channel_id=self.channel_id))
with open(self.filepath, "w") as f:
yaml.safe_dump(config, f)
if not self.channel:
raise ValueError("Discord channel not found")
with open(self.filepath, 'rb') as f:
await self.channel.send(file=discord.File(f, filename=self.filename))
@commands.group(aliases=["lib"])
async def library(self, ctx: commands.Context):
"""Groupe de commandes du cog 'Backup'."""
if not ctx.invoked_subcommand:
if not self.channel_id:
await ctx.send("> Un salon textuel doit être claim pour utiliser la config.")
else:
await ctx.send("> Aucune sous-commande invokée.")
@library.command(name="claim")
@commands.has_role(ROLE)
async def claim_channel(self, ctx: commands.Context):
"""Déclare le salon textuel de la library."""
self.channel_id = ctx.channel.id
await self.create_config()
await ctx.send(f"> Salon textuel {ctx.channel.id} sélectionné pour le stockage de la config.")
@library.command(name="write")
@commands.has_role(ROLE)
async def write_cmd(self, ctx: commands.Context, key: str, value):
"""Écrit une valeur dans la library."""
await self.write(key, value)
# await ctx.send(f"> Salon textuel {ctx.channel.id} sélectionné pour le stockage de la config.")
async def read(self, attribute):
return (await self.get_library())[attribute]
async def write(self, attribute, value):
library = await self.get_library()
print(1, library)
library[attribute] = value
print(2, library)
await self.set_library(library)
async def search_attachment(self, limit: int):
"""Search for the attachment and message."""
async for message in self.channel.history(limit=limit, oldest_first=False): # Adjust limit as needed
for attachment in message.attachments:
print(f"attachement: {attachment.filename}")
if attachment.filename == self.filename:
return (message, attachment)
raise MessageNotFound("No library file found.")
async def get_library(self, limit=10) -> dict:
"""Get the library from discord"""
_, attachment = await self.search_attachment(limit)
await attachment.save(self.filepath)
with open(self.filepath, "r") as f:
d = yaml.safe_load(f)
return d
async def set_library(self, library, limit=10):
"""Set the library to discord"""
message, attachment = await self.search_attachment(limit)
# await message.remove_attachments(attachment)
await message.delete()
with open(self.filepath, "w") as f:
yaml.safe_dump(library, f)
with open(self.filepath, "rb") as f:
# await message.add_files()
file = discord.File(fp=f, filename=self.filename)
await self.channel.send(file=file)
def setup(bot: commands.Bot):
bot.add_cog(Library(bot))
+2 -2
View File
@@ -181,6 +181,6 @@ class Marc(commands.Cog):
return await ctx.send(f"> Stored in **{args[0]}** : **{value}**")
def setup(bot: commands.Bot):
async def setup(bot: commands.Bot):
"""Setup le marc cog."""
bot.add_cog(Marc(bot))
await bot.add_cog(Marc(bot))
+2 -2
View File
@@ -185,5 +185,5 @@ class Messenger(commands.Cog):
return embed
def setup(bot):
bot.add_cog(Messenger(bot))
async def setup(bot):
await bot.add_cog(Messenger(bot))
+2 -2
View File
@@ -273,6 +273,6 @@ class Mongo(commands.Cog):
await ctx.send(f"> {text_path}")
def setup(bot):
async def setup(bot):
"""Setup the cog."""
bot.add_cog(Mongo(bot))
await bot.add_cog(Mongo(bot))
+2 -2
View File
@@ -668,5 +668,5 @@ class Music(commands.Cog):
print("voice_state ok")
def setup(bot):
bot.add_cog(Music(bot))
async def setup(bot):
await bot.add_cog(Music(bot))
+2 -2
View File
@@ -369,5 +369,5 @@ class marcMusic(commands.Cog):
await self.join(ctx)
def setup(bot):
bot.add_cog(marcMusic(bot))
async def setup(bot):
await bot.add_cog(marcMusic(bot))
+2 -2
View File
@@ -234,5 +234,5 @@ class Planning(commands.Cog):
return embed
def setup(bot):
bot.add_cog(Planning(bot))
async def setup(bot):
await bot.add_cog(Planning(bot))
+2 -2
View File
@@ -253,5 +253,5 @@ class PlayList(commands.Cog):
# @playlist.command(name)
def setup(bot):
bot.add_cog(PlayList(bot))
async def setup(bot):
await bot.add_cog(PlayList(bot))
+2 -2
View File
@@ -42,5 +42,5 @@ class Quest(commands.Cog, name="Quête"):
"""Retire une quête."""
def setup(bot: commands.Bot):
bot.add_cog(Quest(bot))
async def setup(bot: commands.Bot):
await bot.add_cog(Quest(bot))
+2 -2
View File
@@ -35,5 +35,5 @@ class Quiz(commands.Cog):
await self.quiz.next(ctx).embed
def setup(bot):
bot.add_cog(Quiz(bot))
async def setup(bot):
await bot.add_cog(Quiz(bot))
+2 -2
View File
@@ -62,5 +62,5 @@ class Record(commands.Cog):
def once_done(self, *args):
print("its done", args)
def setup(bot):
bot.add_cog(Record(bot))
async def setup(bot):
await bot.add_cog(Record(bot))
+2 -2
View File
@@ -110,5 +110,5 @@ class Role(commands.Cog):
# async def role_old(self, ctx:)
def setup(bot):
bot.add_cog(Role(bot))
async def setup(bot):
await bot.add_cog(Role(bot))
+2 -2
View File
@@ -99,6 +99,6 @@ class Shell(commands.Cog):
# shell.close()
def setup(bot):
async def setup(bot):
"""Setup the Shell cog."""
bot.add_cog(Shell(bot))
await bot.add_cog(Shell(bot))
+2 -2
View File
@@ -32,5 +32,5 @@ class Sherlock(commands.Cog):
await ctx.send(embed=embed)
def setup(bot):
bot.add_cog(Sherlock(bot))
async def setup(bot):
await bot.add_cog(Sherlock(bot))
+2 -2
View File
@@ -156,5 +156,5 @@ class Shop(commands.Cog):
pass
def setup(bot):
bot.add_cog(Shop(bot))
async def setup(bot):
await bot.add_cog(Shop(bot))
+2 -2
View File
@@ -129,5 +129,5 @@ class SoundDeck(commands.Cog):
sound.last_used_time = time.time()
def setup(bot):
bot.add_cog(SoundDeck(bot))
async def setup(bot):
await bot.add_cog(SoundDeck(bot))
+6 -3
View File
@@ -13,6 +13,7 @@ import aiofiles
from discord.ext.commands import MessageNotFound
from discord.ext import commands
import server
from aiohttp import web
MAX_CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB
@@ -23,7 +24,7 @@ class Storage(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.channel_id = int(os.environ.get("CHANNEL_ID_NOTIF", "0"))
self.channel_id = int(os.environ["CHANNEL_ID_NOTIF"])
self.password = "password"
# self.tmp_directory = "/tmp"
@@ -108,6 +109,7 @@ class Storage(commands.Cog):
PUBLIC_API_URL = os.environ["PUBLIC_API_URL"]
return f"{PUBLIC_API_URL}/retrieve?filename={filename}&channel_id={channel_id}&timestamp={timestamp}"
@server.add_route(path="/send", method="POST", cog="API")
async def send_file_to_channel(self, request: web.Request):
"""Endpoint to send a file to a specific channel by ID."""
filename: str
@@ -153,6 +155,7 @@ class Storage(commands.Cog):
return web.json_response({"message": message, "url": url}, status=200)
@server.add_route(path="/retrieve", method="GET", cog="API")
async def retrieve_file_from_channel_request(self, request: web.Request):
"""
Endpoint to retrieve, reassemble, and download the original file from a channel
@@ -305,5 +308,5 @@ class Storage(commands.Cog):
# Serve the reassembled file as a download response
return reassembled_filepath
def setup(bot: commands.Bot):
bot.add_cog(Storage(bot))
async def setup(bot: commands.Bot):
await bot.add_cog(Storage(bot))
+2 -2
View File
@@ -110,5 +110,5 @@ class Test(commands.Cog):
await ctx.send(user.id)
def setup(bot):
bot.add_cog(Test(bot))
async def setup(bot):
await bot.add_cog(Test(bot))
+2 -2
View File
@@ -101,5 +101,5 @@ class TextToSpeech(commands.Cog):
ctx.voice_client.play(source, after=after)
def setup(bot):
bot.add_cog(TextToSpeech(bot))
async def setup(bot):
await bot.add_cog(TextToSpeech(bot))
+2 -2
View File
@@ -337,5 +337,5 @@ class TrafficSign(commands.Cog):
"""Compétition sous forme de quiz."""
def setup(bot):
bot.add_cog(TrafficSign(bot))
async def setup(bot):
await bot.add_cog(TrafficSign(bot))
+2 -2
View File
@@ -28,5 +28,5 @@ class Troll(commands.Cog):
await ctx.send("En développement")
def setup(bot):
bot.add_cog(Troll(bot))
async def setup(bot):
await bot.add_cog(Troll(bot))
+2 -2
View File
@@ -70,5 +70,5 @@ class TypingRace(commands.Cog):
await ctx.send(msg)
def setup(bot):
bot.add_cog(TypingRace(bot))
async def setup(bot):
await bot.add_cog(TypingRace(bot))
+2 -2
View File
@@ -269,6 +269,6 @@ class User(commands.Cog, name="Utilisateur"):
)
def setup(bot):
async def setup(bot):
"""Setup the user cog."""
bot.add_cog(User(bot))
await bot.add_cog(User(bot))
+2 -2
View File
@@ -164,5 +164,5 @@ class VoiceChannel(commands.Cog):
break
def setup(bot):
bot.add_cog(VoiceChannel(bot))
async def setup(bot):
await bot.add_cog(VoiceChannel(bot))
+2 -2
View File
@@ -653,5 +653,5 @@ class Web(commands.Cog):
return await ctx.send(shortened_url)
def setup(bot):
bot.add_cog(Web(bot))
async def setup(bot):
await bot.add_cog(Web(bot))
+1 -1
View File
@@ -6,6 +6,6 @@ client_id = os.environ["DISCORD_CLIENT_ID"]
mongo_username = os.environ["DISCORD_MONGO_USERNAME"]
mongo_password = os.environ["DISCORD_MONGO_PASSWORD"]
mongo_cluster = os.environ["DISCORD_MONGO_CLUSTER"]
wolfram_alpha_id = os.environ.get("WOLFRAM_ALPHA_ID", "")
wolfram_alpha_id = os.environ["WOLFRAM_ALPHA_ID"]
mongo_url = f"mongodb+srv://{mongo_username}:{mongo_password}@{mongo_cluster}"
+1 -1
View File
@@ -17,7 +17,7 @@ class Quiz:
owners: str,
timestamp: float = time.time(),
questions: list = [],
seed: int = random.randint(int(1e9), int(1e10) - 1),
seed: int = random.randint(1e9, 1e10 - 1),
n=0,
):
"""Create a quiz using the list of questions."""
Generated
-61
View File
@@ -1,61 +0,0 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1789149629,
"narHash": "sha256-H6GwaZzZf+4npqv0tph94w9tZddSjFjmQrVsW0z78uk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "eaad089433ca2bb662274377d33df3d0e51ef28b",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
-146
View File
@@ -1,146 +0,0 @@
{
description = "discord-bot";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
python = pkgs.python312;
src = ./.;
libs = with pkgs; [
stdenv.cc.cc.lib
zlib
openssl
libffi
libopus
libxcrypt-legacy
postgresql.lib
];
in {
packages.default = pkgs.writeShellApplication {
name = "discord-bot";
runtimeInputs = [
python
pkgs.uv
pkgs.ffmpeg
pkgs.chromedriver
pkgs.chromium
pkgs.ncurses
];
text = ''
export UV_PYTHON=${python.interpreter}
export UV_PYTHON_DOWNLOADS=never
export UV_PYTHON_PREFERENCE=only-system
export UV_PROJECT_ENVIRONMENT="''${RUNTIME_DIRECTORY:-$PWD}/venv"
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath libs}
uv sync --frozen --no-dev --project ${src}
sync
VENV="$UV_PROJECT_ENVIRONMENT"
BOT_DIR="$("$VENV/bin/python" -c 'import discord_bot, pathlib; print(pathlib.Path(discord_bot.__file__).parent)')"
export PYTHONPATH="$BOT_DIR"
cd "$BOT_DIR"
exec "$VENV/bin/python" -m discord_bot
'';
};
devShells.default = pkgs.mkShell {
packages = [
python
pkgs.uv
pkgs.just
pkgs.ffmpeg
pkgs.chromedriver
pkgs.chromium
];
env = {
UV_PYTHON = python.interpreter;
UV_PYTHON_DOWNLOADS = "never";
UV_PYTHON_PREFERENCE = "only-system";
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath libs;
};
shellHook = ''
echo "python $(python --version), uv $(uv --version)"
[ -d .venv ] || uv venv
source .venv/bin/activate
'';
};
})
// {
nixosModules.default = {
config,
lib,
pkgs,
...
}: let
cfg = config.services.discord-bot;
in {
options.services.discord-bot = {
enable = lib.mkEnableOption "discord-bot";
package = lib.mkOption {
type = lib.types.package;
default = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
};
environmentFile = lib.mkOption {type = lib.types.path;};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
};
port = lib.mkOption {
type = lib.types.port;
default = 8050;
};
};
config = lib.mkIf cfg.enable {
users.users.discord-bot = {
isSystemUser = true;
group = "discord-bot";
};
users.groups.discord-bot = {};
systemd.services.discord-bot = {
wantedBy = ["multi-user.target"];
wants = ["network-online.target"];
after = ["network-online.target"];
environment = {
DISCORD_BOT_HOST = cfg.host;
DISCORD_BOT_PORT = toString cfg.port;
UV_CACHE_DIR = "/var/cache/discord-bot/uv";
HOME = "/var/lib/discord-bot";
TERM = "dumb";
};
serviceConfig = {
ExecStart = lib.getExe cfg.package;
EnvironmentFile = cfg.environmentFile;
User = "discord-bot";
Group = "discord-bot";
StateDirectory = "discord-bot";
CacheDirectory = "discord-bot";
RuntimeDirectory = "discord-bot";
RuntimeDirectorySize = "4G";
WorkingDirectory = "/var/lib/discord-bot";
Restart = "always";
RestartSec = 10;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
};
};
};
};
};
}
+3 -4
View File
@@ -3,8 +3,8 @@ name = "discord-bot"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
# requires-python = "==3.8.13"
requires-python = ">=3.12"
requires-python = "==3.8.13"
# requires-python = "==3.12"
dependencies = [
"beautifulsoup4",
"fbchat",
@@ -23,6 +23,7 @@ dependencies = [
"pyyaml",
"youtube-dl",
"python-dotenv",
"dpy-http-server",
"pexpect",
"dnspython",
"requests-futures",
@@ -36,10 +37,8 @@ dependencies = [
"websockets==12.0",
"py-cord[voice]==2.5.0",
"psycopg2-binary",
# "psycopg2",
"aiofiles>=24.1.0",
"apscheduler>=3.10.4",
"async-timeout>=5.0.1",
]
[build-system]
+248 -182
View File
@@ -1,168 +1,213 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
aenum==2.2.6
# via fbchat
aiofiles==24.1.0
# via discord-bot (pyproject.toml)
aiohappyeyeballs==2.4.0
# via aiohttp
aiohttp==3.10.5
# via py-cord
aiosignal==1.3.1
# via aiohttp
annotated-types==0.7.0
# via pydantic
anyio==4.4.0
# uv pip compile requirements.txt --universal --output-file requirements.txt
aenum==2.2.6 ; python_full_version <= '3.8.13'
# via
# groq
# httpx
apscheduler==3.10.4
# via discord-bot (pyproject.toml)
async-timeout==5.0.1
# via discord-bot (pyproject.toml)
attrs==24.2.0
# -r requirements.txt
# fbchat
aiohttp==3.8.5 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# discord-py
# dpy-http-server
# py-cord
aiosignal==1.3.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# aiohttp
async-timeout==4.0.3 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# aiohttp
attrs==23.1.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# aiohttp
# fbchat
# outcome
# trio
beautifulsoup4==4.12.3
beautifulsoup4==4.12.2 ; python_full_version <= '3.8.13'
# via
# discord-bot (pyproject.toml)
# -r requirements.txt
# fbchat
# wikipedia
certifi==2024.8.30
certifi==2023.7.22 ; python_full_version <= '3.8.13'
# via
# httpcore
# httpx
# -r requirements.txt
# requests
# selenium
cffi==1.17.1
cffi==1.15.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# cryptography
# pynacl
charset-normalizer==3.3.2
# via requests
click==7.1.2
# trio
charset-normalizer==3.2.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# aiohttp
# requests
click==7.1.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# gtts
# nightcore
# translate
colorama==0.4.6
# via discord-bot (pyproject.toml)
cryptography==43.0.1
# via pyjwt
deprecated==1.2.14
# via pygithub
distro==1.9.0
# via groq
dnspython==2.6.1
colorama==0.4.6 ; python_full_version <= '3.8.13'
# via -r requirements.txt
cryptography==41.0.3 ; python_full_version <= '3.8.13'
# via
# discord-bot (pyproject.toml)
# -r requirements.txt
# pyjwt
deprecated==1.2.14 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# pygithub
discord-py==2.3.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# dpy-http-server
dnspython==2.4.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# pymongo
fake-useragent==1.5.1
# via discord-bot (pyproject.toml)
fbchat==1.9.7
# via discord-bot (pyproject.toml)
frozenlist==1.4.1
dpy-http-server==0.1.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
exceptiongroup==1.1.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# trio
# trio-websocket
fake-useragent==1.2.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
fbchat==1.9.7 ; python_full_version <= '3.8.13'
# via -r requirements.txt
frozenlist==1.4.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# aiohttp
# aiosignal
future==1.0.0
# via discord-bot (pyproject.toml)
groq==0.9.0
# via discord-bot (pyproject.toml)
gtts==2.2.1
# via discord-bot (pyproject.toml)
h11==0.14.0
future==0.18.3 ; python_full_version <= '3.8.13'
# via -r requirements.txt
gtts==2.2.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
h11==0.14.0 ; python_full_version <= '3.8.13'
# via
# httpcore
# -r requirements.txt
# wsproto
html2text==2024.2.26
# via discord-bot (pyproject.toml)
httpcore==1.0.5
# via httpx
httpx==0.27.2
html2text==2020.1.16 ; python_full_version <= '3.8.13'
# via -r requirements.txt
idna==3.4 ; python_full_version <= '3.8.13'
# via
# groq
# wolframalpha
idna==3.8
# via
# anyio
# httpx
# -r requirements.txt
# requests
# trio
# yarl
jaraco-context==6.0.1
# via wolframalpha
libretranslatepy==2.1.1
# via translate
lxml==5.3.0
# via translate
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
more-itertools==10.5.0
# via wolframalpha
multidict==6.0.5
importlib-resources==6.0.1 ; python_full_version <= '3.8.13'
# via
# aiohttp
# -r requirements.txt
# fake-useragent
jaraco-context==4.3.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# wolframalpha
joblib==1.3.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# scikit-learn
libretranslatepy==2.1.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# translate
lxml==4.9.3 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# translate
markdown-it-py==3.0.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# rich
mdurl==0.1.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# markdown-it-py
more-itertools==10.1.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# wolframalpha
multidict==6.0.4 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# aiohttp
# yarl
nightcore==1.1.0
# via discord-bot (pyproject.toml)
outcome==1.3.0.post0
# via trio
paho-mqtt==1.6.1
# via fbchat
pexpect==4.9.0
# via discord-bot (pyproject.toml)
pillow==10.4.0
# via discord-bot (pyproject.toml)
progressbar2==4.5.0
# via discord-bot (pyproject.toml)
psycopg2==2.9.13
# via discord-bot (pyproject.toml)
ptyprocess==0.7.0
# via pexpect
py-cord==2.5.0
# via discord-bot (pyproject.toml)
pycparser==2.22
# via cffi
pydantic==2.9.0
# via groq
pydantic-core==2.23.2
# via pydantic
pydub==0.23.1
# via nightcore
pygithub==2.4.0
# via discord-bot (pyproject.toml)
pygments==2.18.0
# via rich
pyjwt==2.9.0
# via pygithub
pymongo==4.8.0
# via discord-bot (pyproject.toml)
pynacl==1.5.0
nightcore==1.1.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
numpy==1.24.4 ; python_full_version <= '3.8.13'
# via
# py-cord
# -r requirements.txt
# scikit-learn
# scipy
outcome==1.2.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# trio
paho-mqtt==1.6.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# fbchat
pexpect==4.8.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
pillow==10.0.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
profanity-check==1.0.3 ; python_full_version <= '3.8.13'
# via -r requirements.txt
progressbar2==4.2.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
ptyprocess==0.7.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# pexpect
py-cord==2.4.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
pycparser==2.21 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# cffi
pydub==0.23.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# nightcore
pygithub==1.59.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
pygments==2.16.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# rich
pyjwt==2.8.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# pygithub
pysocks==1.7.1
pymongo==4.4.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
pynacl==1.5.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# pygithub
pysocks==1.7.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# torrequest
# urllib3
python-dotenv==1.0.1
# via discord-bot (pyproject.toml)
python-utils==3.8.2
# via progressbar2
pytz==2024.2
# via apscheduler
pyyaml==6.0.2
# via discord-bot (pyproject.toml)
requests==2.32.3
python-dotenv==1.0.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
python-utils==3.7.0 ; python_full_version <= '3.8.13'
# via
# discord-bot (pyproject.toml)
# -r requirements.txt
# progressbar2
pyyaml==6.0.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
requests==2.31.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# fbchat
# gtts
# pygithub
@@ -170,72 +215,93 @@ requests==2.32.3
# torrequest
# translate
# wikipedia
requests-futures==1.0.1
# via discord-bot (pyproject.toml)
rich==13.8.0
# via discord-bot (pyproject.toml)
selenium==4.24.0
# via discord-bot (pyproject.toml)
six==1.16.0
requests-futures==1.0.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
rich==13.5.2 ; python_full_version <= '3.8.13'
# via -r requirements.txt
scikit-learn==1.3.0 ; python_full_version <= '3.8.13'
# via
# apscheduler
# -r requirements.txt
# profanity-check
scipy==1.10.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# scikit-learn
selenium==4.11.2 ; python_full_version <= '3.8.13'
# via -r requirements.txt
six==1.16.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# gtts
sniffio==1.3.1
sniffio==1.3.0 ; python_full_version <= '3.8.13'
# via
# anyio
# groq
# httpx
# -r requirements.txt
# trio
sortedcontainers==2.4.0
# via trio
soupsieve==2.6
# via beautifulsoup4
stem==1.8.2
# via torrequest
torrequest==0.1.0
# via discord-bot (pyproject.toml)
translate==3.6.1
# via discord-bot (pyproject.toml)
trio==0.26.2
sortedcontainers==2.4.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# trio
soupsieve==2.4.1 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# beautifulsoup4
stem==1.8.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# torrequest
threadpoolctl==3.2.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# scikit-learn
torrequest==0.1.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
translate==3.6.1 ; python_full_version <= '3.8.13'
# via -r requirements.txt
trio==0.22.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# selenium
# trio-websocket
trio-websocket==0.11.1
# via selenium
typing-extensions==4.12.2
trio-websocket==0.10.3 ; python_full_version <= '3.8.13'
# via
# groq
# pydantic
# pydantic-core
# pygithub
# python-utils
# -r requirements.txt
# selenium
tzdata==2024.1
# via pydantic
tzlocal==5.2
# via apscheduler
unidecode==1.3.8
# via discord-bot (pyproject.toml)
urllib3==2.2.2
typing-extensions==4.7.1 ; python_full_version <= '3.8.13'
# via
# pygithub
# -r requirements.txt
# py-cord
# python-utils
# rich
unidecode==1.3.6 ; python_full_version <= '3.8.13'
# via -r requirements.txt
urllib3==2.0.4 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# requests
# selenium
websocket-client==1.8.0
# via selenium
websockets==12.0
# via discord-bot (pyproject.toml)
wikipedia==1.4.0
# via discord-bot (pyproject.toml)
wolframalpha==5.1.3
# via discord-bot (pyproject.toml)
wrapt==1.16.0
# via deprecated
wsproto==1.2.0
# via trio-websocket
xmltodict==0.13.0
# via wolframalpha
yarl==1.10.0
# via aiohttp
youtube-dl==2021.12.17
# via discord-bot (pyproject.toml)
wikipedia==1.4.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
wolframalpha==5.0.0 ; python_full_version <= '3.8.13'
# via -r requirements.txt
wrapt==1.15.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# deprecated
wsproto==1.2.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# trio-websocket
xmltodict==0.13.0 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# wolframalpha
yarl==1.9.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# aiohttp
youtube-dl==2021.12.17 ; python_full_version <= '3.8.13'
# via -r requirements.txt
zipp==3.16.2 ; python_full_version <= '3.8.13'
# via
# -r requirements.txt
# importlib-resources
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env python
"""Tests pour l'API HTTP du bot Discord."""
import requests
import time
import sys
API_BASE = "http://localhost:8050"
def test_server_running():
"""Vérifie que le serveur répond."""
try:
response = requests.get(f"{API_BASE}/live", timeout=5)
print(f"✓ Serveur actif - Status: {response.status_code}")
return True
except requests.exceptions.ConnectionError:
print("✗ Serveur non accessible")
return False
except Exception as e:
print(f"✗ Erreur: {e}")
return False
def test_send_channel():
"""Test d'envoi de message dans un channel."""
data = {
"id": 1548552725808283748, # CHANNEL_ID_NOTIF
"message": "Test API - Message de test automatique"
}
try:
response = requests.post(f"{API_BASE}/send/channel", json=data, timeout=5)
if response.status_code == 200:
print(f"✓ send/channel - {response.json()}")
return True
else:
print(f"✗ send/channel - Status {response.status_code}: {response.text}")
return False
except Exception as e:
print(f"✗ send/channel - Erreur: {e}")
return False
def test_debug_bot():
"""Test de l'endpoint debug."""
try:
response = requests.get(f"{API_BASE}/debug/bot", timeout=5)
if response.status_code == 200:
data = response.json()
print(f"✓ debug/bot - Bot ID: {data.get('id', 'N/A')}")
return True
else:
print(f"✗ debug/bot - Status {response.status_code}")
return False
except Exception as e:
print(f"✗ debug/bot - Erreur: {e}")
return False
def main():
"""Lance tous les tests."""
print("=" * 50)
print("Tests API Discord Bot")
print("=" * 50)
tests = [
("Serveur actif", test_server_running),
("Debug bot", test_debug_bot),
("Send channel", test_send_channel),
]
results = []
for name, test_func in tests:
print(f"\n[{name}]")
result = test_func()
results.append((name, result))
time.sleep(0.5)
print("\n" + "=" * 50)
print("Résumé:")
print("=" * 50)
passed = sum(1 for _, result in results if result)
total = len(results)
for name, result in results:
status = "✓ PASS" if result else "✗ FAIL"
print(f"{status} - {name}")
print(f"\nRésultat: {passed}/{total} tests réussis")
return 0 if passed == total else 1
if __name__ == "__main__":
sys.exit(main())
Generated
+1331 -470
View File
File diff suppressed because it is too large Load Diff