2 Commits

Author SHA1 Message Date
60e95791a9 Merge pull request 'fix: Fix pipeline, add ci and adapted compose' (#2) from pipelines into main
Some checks failed
CD / Build and push Docker image (push) Failing after 1s
CD / Deploy to VPS (push) Has been skipped
CI / Run tests (push) Failing after 1s
Reviewed-on: #2
2026-07-16 19:18:52 +00:00
3fb926ec20 fix: Fix pipeline, add ci and adapted compose
Some checks failed
CI / Run tests (pull_request) Failing after 20s
2026-07-16 21:15:02 +02:00
16 changed files with 6997 additions and 191 deletions

View File

@@ -1,4 +0,0 @@
.gitea/
__pycache__/
.vscode
tests/

View File

@@ -2,15 +2,49 @@ name: CD
on:
push:
branches: [main, v2]
branches: [main]
concurrency:
group: cd-${{ github.ref_name }}
cancel-in-progress: true
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:
name: Deploy to VPS
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Copy files to VPS
- name: Copy compose file to VPS
uses: appleboy/scp-action@v1
with:
host: ${{ secrets.VPS_HOST }}
@@ -19,7 +53,6 @@ jobs:
port: ${{ secrets.VPS_SSH_PORT }}
source: "compose.yaml"
target: "/opt/docker/botmafieux"
- name: Deploy on VPS
uses: appleboy/ssh-action@v1
with:
@@ -28,12 +61,7 @@ jobs:
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_SSH_PORT }}
script: |
set -euo pipefail
cd /opt/docker/botmafieux
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
docker compose -f compose.yaml --env-file .env pull
docker compose -f compose.yaml --env-file .env up -d --remove-orphans

22
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,22 @@
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

2
.gitignore vendored
View File

@@ -165,5 +165,3 @@ data/
*.disabled
config/
tests/

View File

@@ -1,11 +1,11 @@
import discord, json, os, random, re
import collections
from discord.ext import commands, bridge
from discord.ext import commands
from setup.logger import LOGGER
from setup.config import Config
class InsultsCog(commands.Cog):
def __init__(self, bot:bridge.Bot):
def __init__(self, bot:discord.Bot):
self.bot = bot
with open(os.path.join(Config.CONFIG_PATH, "insults.txt")) as f:

View File

@@ -1,149 +0,0 @@
import discord, json, os, random, re, datetime
import collections
from discord.ext import commands, bridge, tasks
from setup.logger import LOGGER
from setup.config import Config
class InternCog(commands.Cog):
def __init__(self, bot:commands.Bot) -> None:
self.bot = bot
self.in_voice_channel = False
self.connected_guild_ids = set()
def _get_guild_data(self, guild_id):
return next((data for data in self.data if data["guild_id"] == guild_id), None)
def _get_guild_voice_client(self, guild_id):
return next(
(
voice_client
for voice_client in self.bot.voice_clients
if getattr(getattr(voice_client, "guild", None), "id", None) == guild_id
),
None,
)
def _resolve_voice_action(self, before_channel, after_channel, guild_voice_client):
if after_channel is None and before_channel is not None:
return "disconnect"
if before_channel is not None and after_channel is not None:
if before_channel.id != after_channel.id:
return "move"
return None
if after_channel is not None and before_channel is None:
if guild_voice_client is not None:
return "move"
return "connect" if random.random() <= 0.25 else None
return None
def load_from_file(self):
try:
with open(Config.INTERN_MEMORY_PATH) as f:
self.data = json.load(f)
if len(self.data) == 0: raise FileNotFoundError
for guild_data in self.data:
guild = self.bot.get_guild(guild_data["guild_id"])
if guild:
mentor = guild.get_member(guild_data["mentor_id"])
LOGGER.debug(f"Loaded mentor: {mentor.name} for guild {guild.name}")
except FileNotFoundError:
today = datetime.datetime.now()
next_roll = datetime.datetime(year=today.year,month=today.month,day=today.day) + datetime.timedelta(days=7)
self.data = [
{
"mentor_id": random.choice(guild.members).id,
"guild_id": guild.id,
"next_roll": next_roll.isoformat()
}
for guild in self.bot.guilds
]
def load_from_database(self):
pass
def save_to_file(self):
with open(Config.INTERN_MEMORY_PATH, 'w') as f:
json.dump(self.data,f)
def save_to_database(self):
pass
@tasks.loop(hours=24)
async def update(self):
today = datetime.datetime.now()
for guild_data in self.data:
next_roll = datetime.datetime.fromisoformat(guild_data["next_roll"])
if today > next_roll:
guild = self.bot.get_guild(guild_data["guild_id"])
if guild:
members = [
member
for member in guild.get_role(1516232292576788530).members
if member not in guild.get_role(1526911990495445143).members
and member.id != guild_data["mentor_id"]
]
new_mentor = random.choice(members)
new_mentor_id = new_mentor.id
guild_data["mentor_id"] = new_mentor_id
new_next_roll = datetime.datetime(year=today.year,month=today.month,day=today.day) + datetime.timedelta(days=7)
guild_data["next_roll"] = new_next_roll.isoformat()
LOGGER.debug(f"New mentor: {new_mentor.name} for guild {guild.name}")
self.save_to_file()
@commands.Cog.listener()
async def on_ready(self):
self.load_from_file()
self.update.start()
@commands.Cog.listener()
async def on_voice_state_update(self, member:discord.Member, before:discord.VoiceState, after:discord.VoiceState):
if not self.data:
return
guild_id = None
if before and before.channel and before.channel.guild:
guild_id = before.channel.guild.id
elif after and after.channel and after.channel.guild:
guild_id = after.channel.guild.id
if guild_id is None:
return
guild_data = self._get_guild_data(guild_id)
if not guild_data or member.id != guild_data["mentor_id"]:
return
if before and before.channel:
channel_name = getattr(before.channel, "name", "unknown")
LOGGER.debug(f"Mentor left channel {channel_name}")
if after and after.channel:
channel_name = getattr(after.channel, "name", "unknown")
LOGGER.debug(f"Mentor joined channel {channel_name}")
guild_voice_client = self._get_guild_voice_client(guild_id)
action = self._resolve_voice_action(before.channel if before else None, after.channel if after else None, guild_voice_client)
if action == "connect":
try:
await after.channel.connect()
self.connected_guild_ids.add(guild_id)
self.in_voice_channel = True
except discord.ClientException as exc:
LOGGER.warning(f"Could not connect to voice channel: {exc}")
elif action == "move":
if guild_voice_client:
await guild_voice_client.move_to(after.channel)
else:
await after.channel.connect()
self.connected_guild_ids.add(guild_id)
self.in_voice_channel = True
elif action == "disconnect":
if guild_voice_client:
await guild_voice_client.disconnect(force=True)
self.connected_guild_ids.discard(guild_id)
self.in_voice_channel = False

View File

@@ -1,9 +1,22 @@
networks:
internal:
internal: true
outbound:
external: true
services:
botmafieux:
build:
context: .
dockerfile: ./Dockerfile
tags:
- nebulo9/botmafieux:v2
image: nebulo9/botmafieux:latest
container_name: botmafieux
restart: unless-stopped
networks:
- internal
- outbound
env_file: ./.env
volumes:
- ./config:/config
deploy:
resources:
limits:
cpus: '0.25'
memory: '256M'

6778
config/hltv.html Normal file

File diff suppressed because one or more lines are too long

27
config/insults.txt Normal file
View File

@@ -0,0 +1,27 @@
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
config/intern.json Normal file
View File

@@ -0,0 +1 @@
[{"mentor_id": 264189554582355969, "guild_id": 897157935006900254, "next_roll": "2026-07-22T00:00:00"}]

22
config/intern_takes.csv Normal file
View File

@@ -0,0 +1,22 @@
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 ?
1 target weight take
2 any 1.0 Tu as besoin d'un café ?
3 any 1.0 Je peux te faire un café si tu veux ?
4 any 1.0 Tu as besoin d'un thé ?
5 any 1.0 Je peux te faire un thé si tu veux ?
6 any 1.0 Tu as besoin d'un matcha latte?
7 any 1.0 Je peux te faire un matcha latte si tu veux ?
8 any 1.0 Tu peux m'aider sur un truc ?
9 any 1.0 Désolé, j'ai pas compris.
10 any 1.0 Attends, tu peux réexpliquer ?
11 any 1.0 J'ai fait une bêtise je crois...
12 nebulo 2.0 J'ai un bug dans mon code tu peux regarder ?
13 bluebro 2.0 J'ai un bug dans mon code tu peux regarder ?
14 viebah 2.0 J'ai un bug dans mon code tu peux regarder ?
15 merdix 2.0 Mon modèle 3D est cassé, je fais quoi ?
16 goupzy 2.0 Mon modèle 3D est cassé, je fais quoi ?
17 viebah 2.0 Mon modèle 3D est cassé, je fais quoi ?
18 goupzy 2.0 Mon modèle 3D est cassé, je fais quoi ?
19 nino274 2.0 Mon modèle 3D est cassé, je fais quoi ?
20 cemesah 2.0 J'ai fait une erreur, il faut réimprimer les cartes...
21 cemesah 2.0 J'ai pas sauvegardé et InDesign a planté...
22 cemesah 2.0 Tu veux du bouillon ?

View File

@@ -2,6 +2,7 @@ import os, discord, json, sys, random
from argparse import ArgumentParser
from datetime import datetime
from asyncio import create_task
from discord import option
from discord.ext import tasks, commands
from setup.bot import bot, reload_feature, register_cogs
from setup.logger import LOGGER

View File

@@ -1,13 +1,10 @@
aiohappyeyeballs==2.7.1
aiohappyeyeballs==2.6.2
aiohttp==3.14.1
aiosignal==1.4.0
asarPy==1.0.1
async-timeout==5.0.1
attrs==26.1.0
audioop-lts==0.2.2
beautifulsoup4==4.15.0
certifi==2026.5.20
cffi==2.1.0
charset-normalizer==3.4.7
frozenlist==1.8.0
h11==0.16.0
@@ -18,8 +15,6 @@ outcome==1.3.0.post0
propcache==0.5.2
psycopg2-binary==2.9.12
py-cord==2.8.0
pycparser==3.0
PyNaCl==1.6.2
PySocks==1.7.1
python-dotenv==1.2.2
requests==2.34.2
@@ -29,9 +24,8 @@ sortedcontainers==2.4.0
soupsieve==2.8.4
trio==0.33.0
trio-websocket==0.12.2
typing_extensions==4.16.0
typing_extensions==4.15.0
urllib3==2.7.0
wavelink==3.5.2
websocket-client==1.9.0
wsproto==1.3.2
yarl==1.24.2

View File

@@ -1,9 +1,10 @@
import discord
from discord.ext import commands
from discord.ext import bridge
# from cogs.birthday import BirthdayCog
# from cogs.productivity import ProductivityCog
from cogs.cs import CSCog
from cogs.insult import InsultsCog
from cogs.roulette import RouletteCog
from cogs.intern import InternCog
from setup.logger import LOGGER
bot_intents = discord.Intents.default()
@@ -12,15 +13,13 @@ bot_intents.members = True
bot_intents.presences = True
bot_intents.guilds = True
bot_intents.reactions = True
bot_intents.voice_states = True
bot = commands.Bot(command_prefix='$', intents=bot_intents)
bot = bridge.Bot(command_prefix='$', intents=bot_intents)
COG_REGISTRY = {
'cs': CSCog,
'insult': InsultsCog,
'roulette': RouletteCog,
'intern': InternCog
'roulette': RouletteCog
}

View File

@@ -7,8 +7,6 @@ class Config:
TOKEN = str(os.getenv("TOKEN", ""))
CONFIG_PATH = str(os.getenv("CONFIG_PATH"))
INTERN_MEMORY_PATH = os.path.join(CONFIG_PATH,"intern.json")
INTERN_TAKES_PATH = os.path.join(CONFIG_PATH,"intern_takes.csv")
COGS = os.getenv("COGS", "")
@classmethod

View File

@@ -0,0 +1,78 @@
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()