Compare commits
2 Commits
pipelines
...
d14ca4ab3e
| Author | SHA1 | Date | |
|---|---|---|---|
| d14ca4ab3e | |||
| 78d3a60bd7 |
@@ -2,49 +2,15 @@ name: CD
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, v2]
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: cd-${{ github.ref_name }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
|
||||||
name: Build and push Docker image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v4
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v4
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
|
||||||
- name: Restore Docker build cache
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
- name: Build and push image
|
|
||||||
uses: docker/build-push-action@v7
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ secrets.DOCKER_USERNAME }}/botmafieux:latest
|
|
||||||
${{ secrets.DOCKER_USERNAME }}/botmafieux:${{ github.sha }}
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy to VPS
|
name: Deploy to VPS
|
||||||
needs: build-and-push
|
needs: build-and-push
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Copy compose file to VPS
|
- name: Copy files to VPS
|
||||||
uses: appleboy/scp-action@v1
|
uses: appleboy/scp-action@v1
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.VPS_HOST }}
|
host: ${{ secrets.VPS_HOST }}
|
||||||
@@ -53,6 +19,7 @@ jobs:
|
|||||||
port: ${{ secrets.VPS_SSH_PORT }}
|
port: ${{ secrets.VPS_SSH_PORT }}
|
||||||
source: "compose.yaml"
|
source: "compose.yaml"
|
||||||
target: "/opt/docker/botmafieux"
|
target: "/opt/docker/botmafieux"
|
||||||
|
|
||||||
- name: Deploy on VPS
|
- name: Deploy on VPS
|
||||||
uses: appleboy/ssh-action@v1
|
uses: appleboy/ssh-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -61,7 +28,12 @@ jobs:
|
|||||||
key: ${{ secrets.VPS_SSH_KEY }}
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
port: ${{ secrets.VPS_SSH_PORT }}
|
port: ${{ secrets.VPS_SSH_PORT }}
|
||||||
script: |
|
script: |
|
||||||
set -euo pipefail
|
|
||||||
cd /opt/docker/botmafieux
|
cd /opt/docker/botmafieux
|
||||||
docker compose -f compose.yaml --env-file .env pull
|
|
||||||
docker compose -f compose.yaml --env-file .env up -d --remove-orphans
|
docker compose -f compose.yaml --env-file .env down
|
||||||
|
|
||||||
|
docker compose -f compose.yaml --env-file .env build
|
||||||
|
|
||||||
|
docker compose -f compose.yaml --env-file .env up -d
|
||||||
|
|
||||||
|
docker compose -f compose.yaml --env-file .env.prod ps
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Run tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: 3.13
|
|
||||||
- name: Install dependencies
|
|
||||||
run: python -m pip install --no-cache-dir -r requirements.txt
|
|
||||||
- name: Run pytest
|
|
||||||
run: pytest -q
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -162,4 +162,6 @@ cython_debug/
|
|||||||
data/
|
data/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
*.disabled
|
*.disabled
|
||||||
|
|
||||||
|
config/
|
||||||
@@ -3,12 +3,17 @@ FROM python:3.13.14-alpine
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||||
|
apk add --no-cache firefox geckodriver
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV TOKEN=""
|
ENV TOKEN=""
|
||||||
ENV URL=""
|
ENV URL=""
|
||||||
|
ENV CONFIG_PATH="/config"
|
||||||
|
ENV COGS=""
|
||||||
|
ENV GECKODRIVER_PATH="/usr/bin/geckodriver"
|
||||||
|
|
||||||
|
RUN mkdir ${CONFIG_PATH}
|
||||||
|
|
||||||
ENTRYPOINT ["python", "main.py"]
|
ENTRYPOINT ["python", "main.py"]
|
||||||
34
cogs/cs.py
34
cogs/cs.py
@@ -1,15 +1,18 @@
|
|||||||
import discord, datetime, os, re, json
|
import discord, datetime, os, re, json
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.firefox.options import Options
|
||||||
|
from selenium.webdriver.firefox.service import Service
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from discord.ext import commands, tasks, bridge
|
from discord.ext import commands, tasks, bridge
|
||||||
from setup.logger import LOGGER
|
from setup.logger import LOGGER
|
||||||
|
from setup.config import Config
|
||||||
from models.cs_models import *
|
from models.cs_models import *
|
||||||
|
|
||||||
BASE_URL = "https://www.hltv.org"
|
BASE_URL = "https://www.hltv.org"
|
||||||
|
|
||||||
PAGE_FILE = os.path.join("data","hltv.html")
|
PAGE_FILE = os.path.join(Config.CONFIG_PATH,"hltv.html")
|
||||||
|
|
||||||
MATCHES_FILE = os.path.join("data","matches.json")
|
MATCHES_FILE = os.path.join(Config.CONFIG_PATH,"matches.json")
|
||||||
|
|
||||||
# Cache duration in seconds (6 hours)
|
# Cache duration in seconds (6 hours)
|
||||||
DATA_CACHE_DURATION = 3600 * 6
|
DATA_CACHE_DURATION = 3600 * 6
|
||||||
@@ -43,14 +46,27 @@ class CSCog(commands.Cog):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get_page(self) -> None:
|
def get_page(self) -> None:
|
||||||
"""Get the page from https://www.hltv.org/matches/ using Firefox webdriver and save to PAGE_FILE."""
|
"""Get the page from https://www.hltv.org/matches/ using a headless Firefox webdriver and save to PAGE_FILE."""
|
||||||
driver = webdriver.Firefox()
|
options = Options()
|
||||||
driver.get("https://www.hltv.org/matches/")
|
options.add_argument("--headless=new")
|
||||||
html = driver.page_source
|
options.add_argument("--no-sandbox")
|
||||||
driver.close()
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
|
options.add_argument("--window-size=1920,1080")
|
||||||
|
|
||||||
with open(PAGE_FILE, 'w') as f:
|
geckodriver_path = os.getenv("GECKODRIVER_PATH")
|
||||||
f.write(html)
|
service = Service(executable_path=geckodriver_path) if geckodriver_path else None
|
||||||
|
|
||||||
|
driver = None
|
||||||
|
try:
|
||||||
|
driver = webdriver.Firefox(service=service, options=options)
|
||||||
|
driver.get("https://www.hltv.org/matches/")
|
||||||
|
html = driver.page_source
|
||||||
|
|
||||||
|
with open(PAGE_FILE, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(html)
|
||||||
|
finally:
|
||||||
|
if driver is not None:
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
"""Save self.matches to MATCHES_FILE."""
|
"""Save self.matches to MATCHES_FILE."""
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import discord, json, os, random, re
|
|||||||
import collections
|
import collections
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from setup.logger import LOGGER
|
from setup.logger import LOGGER
|
||||||
|
from setup.config import Config
|
||||||
|
|
||||||
class InsultsCog(commands.Cog):
|
class InsultsCog(commands.Cog):
|
||||||
def __init__(self, bot:discord.Bot):
|
def __init__(self, bot:discord.Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
with open(os.path.join("data", "insults.txt")) as f:
|
with open(os.path.join(Config.CONFIG_PATH, "insults.txt")) as f:
|
||||||
self.insults = [l.strip() for l in f.readlines()]
|
self.insults = [l.strip() for l in f.readlines()]
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
|
|||||||
212
cogs/roulette.py
Normal file
212
cogs/roulette.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import option
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
|
||||||
|
class RouletteCog(commands.Cog):
|
||||||
|
def __init__(self, bot: discord.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.cooldown = 60 * 3
|
||||||
|
self.active_games = {}
|
||||||
|
|
||||||
|
def _build_view(self, game: dict) -> discord.ui.View:
|
||||||
|
view = discord.ui.View(timeout=None)
|
||||||
|
|
||||||
|
if game["status"] == "pending":
|
||||||
|
async def accept_callback(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != game["target"].id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Only the challenged member can accept this duel.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
game["status"] = "ready"
|
||||||
|
game["current_player"] = game["author"]
|
||||||
|
|
||||||
|
if game.get("message"):
|
||||||
|
await game["message"].edit(
|
||||||
|
content=(
|
||||||
|
f"{game['author'].mention} challenged {game['target'].mention} to a Russian roulette duel.\n"
|
||||||
|
f"✅ {game['target'].mention} accepted. {game['current_player'].mention}, click Fire!"
|
||||||
|
),
|
||||||
|
view=self._build_view(game),
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
async def decline_callback(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != game["target"].id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"Only the challenged member can decline this duel.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
game["status"] = "declined"
|
||||||
|
self.active_games.pop((game["guild_id"], game["author"].id, game["target"].id), None)
|
||||||
|
|
||||||
|
if game.get("message"):
|
||||||
|
await game["message"].edit(
|
||||||
|
content=f"{game['target'].mention} declined the challenge.",
|
||||||
|
view=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
accept_button = discord.ui.Button(label="Accept", style=discord.ButtonStyle.green)
|
||||||
|
accept_button.callback = accept_callback
|
||||||
|
view.add_item(accept_button)
|
||||||
|
|
||||||
|
decline_button = discord.ui.Button(label="Decline", style=discord.ButtonStyle.red)
|
||||||
|
decline_button.callback = decline_callback
|
||||||
|
view.add_item(decline_button)
|
||||||
|
|
||||||
|
elif game["status"] in {"ready", "playing"}:
|
||||||
|
async def fire_callback(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != game["current_player"].id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Wait for {game['current_player'].mention} to fire.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if interaction.user.id in game["fired"]:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"You already fired once. Only one shot per player is allowed.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
game["fired"].add(interaction.user.id)
|
||||||
|
is_shot = random.randint(1, 6) == 1
|
||||||
|
|
||||||
|
if is_shot:
|
||||||
|
game["status"] = "finished"
|
||||||
|
self.active_games.pop((game["guild_id"], game["author"].id, game["target"].id), None)
|
||||||
|
|
||||||
|
await self._mute_member(interaction.user, 60)
|
||||||
|
|
||||||
|
if game.get("message"):
|
||||||
|
await game["message"].edit(
|
||||||
|
content=(
|
||||||
|
f"💥 {interaction.user.mention} pulled the trigger and got shot.\n"
|
||||||
|
f"They were muted for 60 seconds."
|
||||||
|
),
|
||||||
|
view=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if len(game["fired"]) >= 2:
|
||||||
|
game["status"] = "finished"
|
||||||
|
self.active_games.pop((game["guild_id"], game["author"].id, game["target"].id), None)
|
||||||
|
|
||||||
|
if game.get("message"):
|
||||||
|
await game["message"].edit(
|
||||||
|
content=(
|
||||||
|
f"🔫 {interaction.user.mention} survived and both players have fired once.\n"
|
||||||
|
f"No one was shot. The duel ends in a draw."
|
||||||
|
),
|
||||||
|
view=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
game["status"] = "playing"
|
||||||
|
game["current_player"] = game["target"] if interaction.user.id == game["author"].id else game["author"]
|
||||||
|
|
||||||
|
if game.get("message"):
|
||||||
|
await game["message"].edit(
|
||||||
|
content=(
|
||||||
|
f"🔫 {interaction.user.mention} survived.\n"
|
||||||
|
f"Next turn: {game['current_player'].mention}"
|
||||||
|
),
|
||||||
|
view=self._build_view(game),
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
fire_button = discord.ui.Button(label="Fire", style=discord.ButtonStyle.danger)
|
||||||
|
fire_button.callback = fire_callback
|
||||||
|
view.add_item(fire_button)
|
||||||
|
|
||||||
|
return view
|
||||||
|
|
||||||
|
async def _mute_member(self, member: discord.Member, seconds: int = 60):
|
||||||
|
until = discord.utils.utcnow() + datetime.timedelta(seconds=seconds)
|
||||||
|
try:
|
||||||
|
await member.timeout(until=until, reason="Russian roulette loss")
|
||||||
|
except AttributeError:
|
||||||
|
await member.edit(timed_out_until=until, reason="Russian roulette loss")
|
||||||
|
|
||||||
|
@commands.slash_command(
|
||||||
|
name="roulette",
|
||||||
|
description="Challenge other server members. The loser is getting muted for 1 minute",
|
||||||
|
)
|
||||||
|
@option(
|
||||||
|
name="target",
|
||||||
|
description="The member to challenge",
|
||||||
|
input_type=discord.Member,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
async def roulette(
|
||||||
|
self,
|
||||||
|
ctx: discord.ApplicationContext,
|
||||||
|
target: discord.Member,
|
||||||
|
):
|
||||||
|
author = ctx.author
|
||||||
|
self_test = author.id == target.id
|
||||||
|
|
||||||
|
if self_test:
|
||||||
|
target = author
|
||||||
|
|
||||||
|
game = {
|
||||||
|
"author": author,
|
||||||
|
"target": target,
|
||||||
|
"status": "pending",
|
||||||
|
"current_player": None,
|
||||||
|
"guild_id": ctx.guild_id,
|
||||||
|
"message": None,
|
||||||
|
"fired": set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.active_games[(ctx.guild_id, author.id, target.id)] = game
|
||||||
|
|
||||||
|
await ctx.send_response(
|
||||||
|
f"{author.mention} challenged {target.mention} to a Russian roulette duel.\n"
|
||||||
|
f"The target has 60 seconds to accept or decline.",
|
||||||
|
view=self._build_view(game),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self_test:
|
||||||
|
await ctx.send_followup(
|
||||||
|
"**Self-challenge enabled for testing.**\n"
|
||||||
|
"The game will start immediately and you can test the flow.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
game["message"] = await ctx.interaction.original_response()
|
||||||
|
except Exception:
|
||||||
|
game["message"] = None
|
||||||
|
|
||||||
|
async def timeout_handler():
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if game.get("status") == "pending":
|
||||||
|
game["status"] = "timeout"
|
||||||
|
self.active_games.pop((ctx.guild_id, author.id, target.id), None)
|
||||||
|
|
||||||
|
await self._mute_member(target, 60)
|
||||||
|
|
||||||
|
if game.get("message"):
|
||||||
|
await game["message"].edit(
|
||||||
|
content=f"⏰ {target.mention} did not respond in time and was muted for 60 seconds.",
|
||||||
|
view=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(timeout_handler())
|
||||||
25
compose.yaml
25
compose.yaml
@@ -1,22 +1,9 @@
|
|||||||
networks:
|
|
||||||
internal:
|
|
||||||
internal: true
|
|
||||||
outbound:
|
|
||||||
external: true
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
botmafieux:
|
botmafieux:
|
||||||
image: nebulo9/botmafieux:latest
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
|
tags:
|
||||||
|
- nebulo9/botmafieux:v2
|
||||||
container_name: botmafieux
|
container_name: botmafieux
|
||||||
restart: unless-stopped
|
env_file: ./.env
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
- outbound
|
|
||||||
env_file: ./.env
|
|
||||||
volumes:
|
|
||||||
- ./config:/config
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '0.25'
|
|
||||||
memory: '256M'
|
|
||||||
6778
config/hltv.html
6778
config/hltv.html
File diff suppressed because one or more lines are too long
@@ -1,27 +0,0 @@
|
|||||||
Ta gueule.
|
|
||||||
Qui a demandé ?
|
|
||||||
Mais tais-toi!
|
|
||||||
zZzZzZzZ......
|
|
||||||
Tout ça ?
|
|
||||||
On peut se passer du message je pense.
|
|
||||||
C'est chiant le message là.
|
|
||||||
Mdr on s'en fout.
|
|
||||||
Pas lu.
|
|
||||||
Trop long j'ai décroché.
|
|
||||||
Il faut quitter le serveur, vous gênez l'espace s'il vous plaît.
|
|
||||||
Gênant...
|
|
||||||
Si je devais noter la pertinence du message ce serait genre 1/10.
|
|
||||||
Tu es ridicule, arrête d'écrire.
|
|
||||||
Ça a intéréssé quelqu'un ?
|
|
||||||
Franchement c'était nécessaire d'écrire ça ?
|
|
||||||
Honestly the most useless thing I read today.
|
|
||||||
Dommage que les hommes soient cons…
|
|
||||||
Ta mère mérite la prison pour avoir mis au monde un HOMME.
|
|
||||||
T'as pas beaucoup de neurones.
|
|
||||||
Arrêtons de normaliser les hommes svp.
|
|
||||||
🚮🚮
|
|
||||||
Ton message me donne envie de vomir.
|
|
||||||
J'ai pas ri perso.
|
|
||||||
Tiens tiens encore une raison de détester les hommes !
|
|
||||||
Y'a moyen ton père c'est Macron.
|
|
||||||
Tu préfères devenir un être humain sympa ou rester un homme ?
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"mentor_id": 264189554582355969, "guild_id": 897157935006900254, "next_roll": "2026-07-22T00:00:00"}]
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
target;weight;take
|
|
||||||
any;1.0;Tu as besoin d'un café ?
|
|
||||||
any;1.0;Je peux te faire un café si tu veux ?
|
|
||||||
any;1.0;Tu as besoin d'un thé ?
|
|
||||||
any;1.0;Je peux te faire un thé si tu veux ?
|
|
||||||
any;1.0;Tu as besoin d'un matcha latte?
|
|
||||||
any;1.0;Je peux te faire un matcha latte si tu veux ?
|
|
||||||
any;1.0;Tu peux m'aider sur un truc ?
|
|
||||||
any;1.0;Désolé, j'ai pas compris.
|
|
||||||
any;1.0;Attends, tu peux réexpliquer ?
|
|
||||||
any;1.0;J'ai fait une bêtise je crois...
|
|
||||||
nebulo;2.0;J'ai un bug dans mon code tu peux regarder ?
|
|
||||||
bluebro;2.0;J'ai un bug dans mon code tu peux regarder ?
|
|
||||||
viebah;2.0;J'ai un bug dans mon code tu peux regarder ?
|
|
||||||
merdix;2.0;Mon modèle 3D est cassé, je fais quoi ?
|
|
||||||
goupzy;2.0;Mon modèle 3D est cassé, je fais quoi ?
|
|
||||||
viebah;2.0;Mon modèle 3D est cassé, je fais quoi ?
|
|
||||||
goupzy;2.0;Mon modèle 3D est cassé, je fais quoi ?
|
|
||||||
nino274;2.0;Mon modèle 3D est cassé, je fais quoi ?
|
|
||||||
cemesah;2.0;J'ai fait une erreur, il faut réimprimer les cartes...
|
|
||||||
cemesah;2.0;J'ai pas sauvegardé et InDesign a planté...
|
|
||||||
cemesah;2.0;Tu veux du bouillon ?
|
|
||||||
|
121
main.py
121
main.py
@@ -2,130 +2,33 @@ import os, discord, json, sys, random
|
|||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from asyncio import create_task
|
from asyncio import create_task
|
||||||
from dotenv import load_dotenv
|
|
||||||
from discord import option
|
from discord import option
|
||||||
from discord.ext import tasks, commands
|
from discord.ext import tasks, commands
|
||||||
from setup.bot import bot, reload_feature
|
from setup.bot import bot, reload_feature, register_cogs
|
||||||
from setup.logger import LOGGER
|
from setup.logger import LOGGER
|
||||||
|
from setup.config import Config
|
||||||
|
|
||||||
|
|
||||||
PARSER = ArgumentParser(description='BotMafieux for Discord.')
|
PARSER = ArgumentParser(description='BotMafieux for Discord.')
|
||||||
PARSER.add_argument('--guild_id',type=int,help='The guild ID.')
|
PARSER.add_argument('--guild_id',type=int,help='The guild ID.')
|
||||||
PARSER.add_argument('--channel_id',type=int,help='The channel ID.')
|
PARSER.add_argument('--channel_id',type=int,help='The channel ID.')
|
||||||
PARSER.add_argument('-r','--reload',action='store_true',help='Reload the bot.')
|
PARSER.add_argument('-r','--reload',action='store_true',help='Reload the bot.')
|
||||||
|
PARSER.add_argument('--cogs',nargs='+',default=None,help='Select which cogs to load (for example: --cogs cs insult).')
|
||||||
EXEC_ARGS = PARSER.parse_args()
|
EXEC_ARGS = PARSER.parse_args()
|
||||||
|
selected_cogs = EXEC_ARGS.cogs if EXEC_ARGS.cogs is not None else Config.get_cogs()
|
||||||
# @tasks.loop(time=datetime.time(datetime.strptime('00:00','%H:%M')))
|
register_cogs(selected_cogs)
|
||||||
# async def birthday_anouncements_task():
|
|
||||||
# LOGGER.debug('birthday_anouncements_task started.')
|
|
||||||
# for guild in bot.guilds:
|
|
||||||
# birthday_settings = db.select('birthday_settings',f'guild_id = {guild.id}')
|
|
||||||
# if birthday_settings:
|
|
||||||
# if birthday_settings['is_enabled']:
|
|
||||||
# for user in guild.members:
|
|
||||||
# user_productivity_data = db.select('guild_user_productivity',f'user_id = {user.id} AND guild_id = {guild.id}')
|
|
||||||
# if user_productivity_data:
|
|
||||||
# if user_productivity_data['is_enabled']:
|
|
||||||
# user = guild.get_member(user.id)
|
|
||||||
# if user:
|
|
||||||
# birthday = user_productivity_data['birthday']
|
|
||||||
# today = datetime.today().strftime('%d/%m')
|
|
||||||
# if birthday == today:
|
|
||||||
# channel = guild.get_channel(user_productivity_data['channel_id'])
|
|
||||||
# if channel:
|
|
||||||
# message = user_productivity_data['birthday_message']
|
|
||||||
# if message:
|
|
||||||
# message = message.replace('{user}',user.mention)
|
|
||||||
# await channel.send(message)
|
|
||||||
# else:
|
|
||||||
# LOGGER.debug(f'Channel {user_productivity_data["channel_id"]} not found.')
|
|
||||||
# else:
|
|
||||||
# LOGGER.debug(f'No birthday today for user {user.name} ({user.id}).')
|
|
||||||
# else:
|
|
||||||
# LOGGER.debug(f'User {user.name} ({user.id}) not found.')
|
|
||||||
# else:
|
|
||||||
# LOGGER.debug(f'User {user.name} ({user.id}) has birthday announcements disabled.')
|
|
||||||
# else:
|
|
||||||
# LOGGER.debug(f'No birthday data found for user {user.name} ({user.id}).')
|
|
||||||
# else:
|
|
||||||
# LOGGER.debug(f'No productivity settings found for guild {guild.name} ({guild.id}).')
|
|
||||||
# LOGGER.debug('birthday_anouncements_task ended.')
|
|
||||||
|
|
||||||
# @bot.slash_command(name='help',description='Displays the help message.')
|
|
||||||
# async def help(ctx:discord.ApplicationContext):
|
|
||||||
# """Displays this message."""
|
|
||||||
# command_name = 'help'
|
|
||||||
# LOGGER.debug(f'{ctx.author.name} used /{command_name}.')
|
|
||||||
# embed = discord.Embed(title='Help',description='List of commands and their descriptions.',color=discord.Color.from_rgb(171,0,219))
|
|
||||||
# embed.set_author(name=bot.user.name,icon_url=bot.user.avatar.url)
|
|
||||||
# for command in bot.commands:
|
|
||||||
# embed.add_field(name=command.name,value=command.description,inline=False)
|
|
||||||
# await ctx.send_response(embed=embed,ephemeral=True)
|
|
||||||
|
|
||||||
# @bot.slash_command(name='reload',description='Reloads the bot.')
|
|
||||||
# @option(name='feature',description='The feature to reload.',required=True,choices=['birthday','productivity'])
|
|
||||||
# @commands.is_owner()
|
|
||||||
# async def reload(ctx:discord.ApplicationContext, feature:str):
|
|
||||||
# """Reloads the bot."""
|
|
||||||
# command_name = 'reload'
|
|
||||||
# guild = ctx.guild
|
|
||||||
# channel = ctx.channel
|
|
||||||
# LOGGER.debug(f'{ctx.author.name} used /{command_name} {feature}.')
|
|
||||||
# try:
|
|
||||||
# reload_feature(feature)
|
|
||||||
# await ctx.send_response(f'{feature} reloaded!',ephemeral=True)
|
|
||||||
# except Exception as e:
|
|
||||||
# LOGGER.error(f'Error reloading feature {feature}: {e}')
|
|
||||||
# await ctx.send_response(f'Error reloading feature {feature}: {e}',ephemeral=True)
|
|
||||||
|
|
||||||
# @bot.event
|
|
||||||
# async def on_member_join(member:discord.Member):
|
|
||||||
# if not member.bot:
|
|
||||||
# user_id = member.id
|
|
||||||
# user_name = member.name
|
|
||||||
# user_mention = member.mention
|
|
||||||
# db_user = db.select('global_user',f'WHERE user_id = {user_id}')
|
|
||||||
# if not db_user:
|
|
||||||
# db.insert('global_user',user_id=user_id,user_name=user_name,user_mention=user_mention)
|
|
||||||
|
|
||||||
# @bot.event
|
|
||||||
# async def on_raw_member_remove(payload:discord.RawMemberRemoveEvent):
|
|
||||||
# user = payload.user
|
|
||||||
# if not user.bot:
|
|
||||||
# user_id = user.id
|
|
||||||
# guild_id = payload.guild_id
|
|
||||||
# db.delete('guild_user_birthday',f'user_id = {user_id} AND guild_id = {guild_id}')
|
|
||||||
# db.delete('guild_user_productivity',f'user_id = {user_id} AND guild_id = {guild_id}')
|
|
||||||
|
|
||||||
# @bot.event
|
|
||||||
# async def on_guild_join(guild:discord.Guild):
|
|
||||||
# guild_id = guild.id
|
|
||||||
# guild_name = guild.name
|
|
||||||
# LOGGER.info(f'Bot joined guild {guild_name} ({guild_id}).')
|
|
||||||
# db.insert('guild',guild_id=guild_id)
|
|
||||||
# db.insert('birthday_settings',guild_id=guild_id,is_enabled=False,channel_id=random.choice(guild.text_channels).id)
|
|
||||||
# db.insert('productivity_settings',guild_id=guild_id,is_enabled=False)
|
|
||||||
|
|
||||||
# @bot.event
|
|
||||||
# async def on_guild_remove(guild:discord.Guild):
|
|
||||||
# guild_id = guild.id
|
|
||||||
# guild_name = guild.name
|
|
||||||
# LOGGER.info(f'Bot left guild {guild_name} ({guild_id}).')
|
|
||||||
# db.delete('birthday_settings',f'guild_id = {guild_id}')
|
|
||||||
# db.delete('productivity_settings',f'guild_id = {guild_id}')
|
|
||||||
# db.delete('guild',f'guild_id = {guild_id}')
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
if bot.user:
|
if bot.user:
|
||||||
LOGGER.info(f'{bot.user.name} has connected to Discord!')
|
LOGGER.info(f'{bot.user.name} has connected to Discord!')
|
||||||
LOGGER.debug(f'Guilds: {','.join([guild.name for guild in bot.guilds])}')
|
LOGGER.debug(f"Guilds: {','.join([guild.name for guild in bot.guilds])}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
load_dotenv()
|
token = Config.TOKEN
|
||||||
TOKEN = os.getenv("TOKEN")
|
if token:
|
||||||
if TOKEN:
|
bot.run(token)
|
||||||
bot.run(TOKEN)
|
|
||||||
else:
|
else:
|
||||||
LOGGER.error('No token found.')
|
LOGGER.error('No token found.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
70
setup/bot.py
70
setup/bot.py
@@ -4,6 +4,7 @@ from discord.ext import bridge
|
|||||||
# from cogs.productivity import ProductivityCog
|
# from cogs.productivity import ProductivityCog
|
||||||
from cogs.cs import CSCog
|
from cogs.cs import CSCog
|
||||||
from cogs.insult import InsultsCog
|
from cogs.insult import InsultsCog
|
||||||
|
from cogs.roulette import RouletteCog
|
||||||
from setup.logger import LOGGER
|
from setup.logger import LOGGER
|
||||||
|
|
||||||
bot_intents = discord.Intents.default()
|
bot_intents = discord.Intents.default()
|
||||||
@@ -15,15 +16,70 @@ bot_intents.reactions = True
|
|||||||
|
|
||||||
bot = bridge.Bot(command_prefix='$', intents=bot_intents)
|
bot = bridge.Bot(command_prefix='$', intents=bot_intents)
|
||||||
|
|
||||||
COGS = [CSCog, InsultsCog]
|
COG_REGISTRY = {
|
||||||
|
'cs': CSCog,
|
||||||
|
'insult': InsultsCog,
|
||||||
|
'roulette': RouletteCog
|
||||||
|
}
|
||||||
|
|
||||||
for cog in COGS:
|
|
||||||
bot.add_cog(cog(bot))
|
|
||||||
|
|
||||||
def reload_feature(feature:str):
|
def _normalize_cog_names(cogs=None):
|
||||||
|
if cogs is None:
|
||||||
|
return list(COG_REGISTRY.keys())
|
||||||
|
|
||||||
|
if isinstance(cogs, str):
|
||||||
|
cogs = [cogs]
|
||||||
|
|
||||||
|
selected_cogs = []
|
||||||
|
for cog_name in cogs:
|
||||||
|
name = str(cog_name).strip().lower()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
if name in {'all', '*'}:
|
||||||
|
return list(COG_REGISTRY.keys())
|
||||||
|
if name in COG_REGISTRY:
|
||||||
|
selected_cogs.append(name)
|
||||||
|
else:
|
||||||
|
LOGGER.warning("Unknown cog '%s'. Available cogs: %s", cog_name, ', '.join(COG_REGISTRY.keys()))
|
||||||
|
|
||||||
|
return list(dict.fromkeys(selected_cogs))
|
||||||
|
|
||||||
|
|
||||||
|
def register_cogs(cogs=None):
|
||||||
|
selected_cogs = _normalize_cog_names(cogs)
|
||||||
|
|
||||||
|
existing_cog_names = set(COG_REGISTRY.values())
|
||||||
|
for existing_name in list(bot.cogs.keys()):
|
||||||
|
if existing_name in {cog_class.__name__ for cog_class in existing_cog_names}:
|
||||||
|
bot.remove_cog(existing_name)
|
||||||
|
|
||||||
|
for cog_name in selected_cogs:
|
||||||
|
cog_class = COG_REGISTRY[cog_name]
|
||||||
|
bot.add_cog(cog_class(bot))
|
||||||
|
|
||||||
|
return selected_cogs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def reload_feature(feature: str):
|
||||||
|
normalized_feature = str(feature).strip().lower()
|
||||||
LOGGER.debug(bot.cogs)
|
LOGGER.debug(bot.cogs)
|
||||||
|
|
||||||
|
if normalized_feature in {'all', '*'}:
|
||||||
|
register_cogs()
|
||||||
|
LOGGER.debug('All cogs reloaded!')
|
||||||
|
return
|
||||||
|
|
||||||
|
if normalized_feature not in COG_REGISTRY:
|
||||||
|
LOGGER.warning("Unknown cog '%s'. Available cogs: %s", feature, ', '.join(COG_REGISTRY.keys()))
|
||||||
|
return
|
||||||
|
|
||||||
LOGGER.debug(f'Reloading {feature}...')
|
LOGGER.debug(f'Reloading {feature}...')
|
||||||
bot.remove_cog(feature.capitalize()+'Cog')
|
target_cog_name = COG_REGISTRY[normalized_feature].__name__
|
||||||
cog = globals()[feature.capitalize()+'Cog']
|
for existing_name in list(bot.cogs.keys()):
|
||||||
bot.add_cog(cog(bot))
|
if existing_name == target_cog_name:
|
||||||
|
bot.remove_cog(existing_name)
|
||||||
|
break
|
||||||
|
|
||||||
|
bot.add_cog(COG_REGISTRY[normalized_feature](bot))
|
||||||
LOGGER.debug(f'{feature} reloaded!')
|
LOGGER.debug(f'{feature} reloaded!')
|
||||||
|
|||||||
18
setup/config.py
Normal file
18
setup/config.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
TOKEN = str(os.getenv("TOKEN", ""))
|
||||||
|
CONFIG_PATH = str(os.getenv("CONFIG_PATH"))
|
||||||
|
COGS = os.getenv("COGS", "")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_cogs(cls):
|
||||||
|
raw_value = cls.COGS.strip()
|
||||||
|
if not raw_value:
|
||||||
|
return None
|
||||||
|
return [item.strip().lower() for item in raw_value.split(",") if item.strip()]
|
||||||
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
|
|
||||||
from cogs.intern import InternCog
|
|
||||||
|
|
||||||
|
|
||||||
class InternVoiceBehaviorTests(unittest.TestCase):
|
|
||||||
def make_cog(self, voice_client=None):
|
|
||||||
cog = InternCog.__new__(InternCog)
|
|
||||||
cog.bot = SimpleNamespace(voice_clients=[voice_client] if voice_client else [])
|
|
||||||
cog.data = [{"guild_id": 123, "mentor_id": 456}]
|
|
||||||
cog.connected_guild_ids = set()
|
|
||||||
cog.in_voice_channel = False
|
|
||||||
return cog
|
|
||||||
|
|
||||||
def make_channel(self, channel_id, guild, connect_mock=None, name="test-channel"):
|
|
||||||
return SimpleNamespace(id=channel_id, guild=guild, connect=connect_mock or AsyncMock(), name=name)
|
|
||||||
|
|
||||||
def make_member(self, member_id):
|
|
||||||
return SimpleNamespace(id=member_id)
|
|
||||||
|
|
||||||
async def _run_handler(self, cog, before_channel, after_channel, voice_client):
|
|
||||||
before = SimpleNamespace(channel=before_channel)
|
|
||||||
after = SimpleNamespace(channel=after_channel)
|
|
||||||
member = self.make_member(456)
|
|
||||||
await cog.on_voice_state_update(member, before, after)
|
|
||||||
|
|
||||||
def test_initial_join_only_connects_with_25_percent_chance(self):
|
|
||||||
cog = self.make_cog()
|
|
||||||
connect_mock = AsyncMock()
|
|
||||||
guild = SimpleNamespace(id=123, voice_client=None)
|
|
||||||
channel = self.make_channel(1, guild, connect_mock=connect_mock)
|
|
||||||
before = None
|
|
||||||
after = SimpleNamespace(channel=channel)
|
|
||||||
member = self.make_member(456)
|
|
||||||
|
|
||||||
with patch("cogs.intern.random.random", return_value=0.1):
|
|
||||||
import asyncio
|
|
||||||
asyncio.run(cog.on_voice_state_update(member, before, after))
|
|
||||||
connect_mock.assert_awaited_once()
|
|
||||||
|
|
||||||
cog = self.make_cog()
|
|
||||||
connect_mock = AsyncMock()
|
|
||||||
guild = SimpleNamespace(id=123, voice_client=None)
|
|
||||||
channel = self.make_channel(1, guild, connect_mock=connect_mock)
|
|
||||||
before = None
|
|
||||||
after = SimpleNamespace(channel=channel)
|
|
||||||
member = self.make_member(456)
|
|
||||||
|
|
||||||
with patch("cogs.intern.random.random", return_value=0.9):
|
|
||||||
import asyncio
|
|
||||||
asyncio.run(cog.on_voice_state_update(member, before, after))
|
|
||||||
connect_mock.assert_not_awaited()
|
|
||||||
|
|
||||||
def test_move_always_moves_the_bot_when_connected(self):
|
|
||||||
guild = SimpleNamespace(id=123, voice_client=None)
|
|
||||||
voice_client = SimpleNamespace(move_to=AsyncMock(), disconnect=AsyncMock(), guild=guild)
|
|
||||||
cog = self.make_cog(voice_client=voice_client)
|
|
||||||
before_channel = self.make_channel(1, guild)
|
|
||||||
after_channel = self.make_channel(2, guild)
|
|
||||||
import asyncio
|
|
||||||
asyncio.run(self._run_handler(cog, before_channel, after_channel, voice_client))
|
|
||||||
voice_client.move_to.assert_awaited_once_with(after_channel)
|
|
||||||
|
|
||||||
def test_disconnect_always_disconnects_when_mentor_leaves(self):
|
|
||||||
guild = SimpleNamespace(id=123, voice_client=None)
|
|
||||||
voice_client = SimpleNamespace(move_to=AsyncMock(), disconnect=AsyncMock(), guild=guild)
|
|
||||||
cog = self.make_cog(voice_client=voice_client)
|
|
||||||
before_channel = self.make_channel(1, guild)
|
|
||||||
after_channel = None
|
|
||||||
import asyncio
|
|
||||||
asyncio.run(self._run_handler(cog, before_channel, after_channel, voice_client))
|
|
||||||
voice_client.disconnect.assert_awaited_once_with(force=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
Reference in New Issue
Block a user