Compare commits
1 Commits
d14ca4ab3e
...
pipelines
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fb926ec20 |
@@ -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
22
.gitea/workflows/ci.yaml
Normal 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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -162,6 +162,4 @@ cython_debug/
|
||||
data/
|
||||
.vscode/
|
||||
|
||||
*.disabled
|
||||
|
||||
config/
|
||||
*.disabled
|
||||
@@ -3,17 +3,12 @@ FROM python:3.13.14-alpine
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
apk add --no-cache firefox geckodriver
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV TOKEN=""
|
||||
ENV URL=""
|
||||
ENV CONFIG_PATH="/config"
|
||||
ENV COGS=""
|
||||
ENV GECKODRIVER_PATH="/usr/bin/geckodriver"
|
||||
|
||||
RUN mkdir ${CONFIG_PATH}
|
||||
|
||||
ENTRYPOINT ["python", "main.py"]
|
||||
34
cogs/cs.py
34
cogs/cs.py
@@ -1,18 +1,15 @@
|
||||
import discord, datetime, os, re, json
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.firefox.service import Service
|
||||
from bs4 import BeautifulSoup
|
||||
from discord.ext import commands, tasks, bridge
|
||||
from setup.logger import LOGGER
|
||||
from setup.config import Config
|
||||
from models.cs_models import *
|
||||
|
||||
BASE_URL = "https://www.hltv.org"
|
||||
|
||||
PAGE_FILE = os.path.join(Config.CONFIG_PATH,"hltv.html")
|
||||
PAGE_FILE = os.path.join("data","hltv.html")
|
||||
|
||||
MATCHES_FILE = os.path.join(Config.CONFIG_PATH,"matches.json")
|
||||
MATCHES_FILE = os.path.join("data","matches.json")
|
||||
|
||||
# Cache duration in seconds (6 hours)
|
||||
DATA_CACHE_DURATION = 3600 * 6
|
||||
@@ -46,27 +43,14 @@ class CSCog(commands.Cog):
|
||||
return False
|
||||
|
||||
def get_page(self) -> None:
|
||||
"""Get the page from https://www.hltv.org/matches/ using a headless Firefox webdriver and save to PAGE_FILE."""
|
||||
options = Options()
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--window-size=1920,1080")
|
||||
"""Get the page from https://www.hltv.org/matches/ using Firefox webdriver and save to PAGE_FILE."""
|
||||
driver = webdriver.Firefox()
|
||||
driver.get("https://www.hltv.org/matches/")
|
||||
html = driver.page_source
|
||||
driver.close()
|
||||
|
||||
geckodriver_path = os.getenv("GECKODRIVER_PATH")
|
||||
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()
|
||||
with open(PAGE_FILE, 'w') as f:
|
||||
f.write(html)
|
||||
|
||||
def save(self) -> None:
|
||||
"""Save self.matches to MATCHES_FILE."""
|
||||
|
||||
@@ -2,13 +2,12 @@ import discord, json, os, random, re
|
||||
import collections
|
||||
from discord.ext import commands
|
||||
from setup.logger import LOGGER
|
||||
from setup.config import Config
|
||||
|
||||
class InsultsCog(commands.Cog):
|
||||
def __init__(self, bot:discord.Bot):
|
||||
self.bot = bot
|
||||
|
||||
with open(os.path.join(Config.CONFIG_PATH, "insults.txt")) as f:
|
||||
with open(os.path.join("data", "insults.txt")) as f:
|
||||
self.insults = [l.strip() for l in f.readlines()]
|
||||
|
||||
@commands.Cog.listener()
|
||||
|
||||
212
cogs/roulette.py
212
cogs/roulette.py
@@ -1,212 +0,0 @@
|
||||
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,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
|
||||
env_file: ./.env
|
||||
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
6778
config/hltv.html
Normal file
File diff suppressed because one or more lines are too long
27
config/insults.txt
Normal file
27
config/insults.txt
Normal 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
1
config/intern.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"mentor_id": 264189554582355969, "guild_id": 897157935006900254, "next_roll": "2026-07-22T00:00:00"}]
|
||||
22
config/intern_takes.csv
Normal file
22
config/intern_takes.csv
Normal 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 ?
|
||||
|
121
main.py
121
main.py
@@ -2,33 +2,130 @@ import os, discord, json, sys, random
|
||||
from argparse import ArgumentParser
|
||||
from datetime import datetime
|
||||
from asyncio import create_task
|
||||
from dotenv import load_dotenv
|
||||
from discord import option
|
||||
from discord.ext import tasks, commands
|
||||
from setup.bot import bot, reload_feature, register_cogs
|
||||
from setup.bot import bot, reload_feature
|
||||
from setup.logger import LOGGER
|
||||
from setup.config import Config
|
||||
|
||||
|
||||
PARSER = ArgumentParser(description='BotMafieux for Discord.')
|
||||
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('-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()
|
||||
selected_cogs = EXEC_ARGS.cogs if EXEC_ARGS.cogs is not None else Config.get_cogs()
|
||||
register_cogs(selected_cogs)
|
||||
|
||||
# @tasks.loop(time=datetime.time(datetime.strptime('00:00','%H:%M')))
|
||||
# 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
|
||||
async def on_ready():
|
||||
if bot.user:
|
||||
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__':
|
||||
token = Config.TOKEN
|
||||
if token:
|
||||
bot.run(token)
|
||||
load_dotenv()
|
||||
TOKEN = os.getenv("TOKEN")
|
||||
if TOKEN:
|
||||
bot.run(TOKEN)
|
||||
else:
|
||||
LOGGER.error('No token found.')
|
||||
sys.exit(1)
|
||||
|
||||
70
setup/bot.py
70
setup/bot.py
@@ -4,7 +4,6 @@ from discord.ext import bridge
|
||||
# from cogs.productivity import ProductivityCog
|
||||
from cogs.cs import CSCog
|
||||
from cogs.insult import InsultsCog
|
||||
from cogs.roulette import RouletteCog
|
||||
from setup.logger import LOGGER
|
||||
|
||||
bot_intents = discord.Intents.default()
|
||||
@@ -16,70 +15,15 @@ bot_intents.reactions = True
|
||||
|
||||
bot = bridge.Bot(command_prefix='$', intents=bot_intents)
|
||||
|
||||
COG_REGISTRY = {
|
||||
'cs': CSCog,
|
||||
'insult': InsultsCog,
|
||||
'roulette': RouletteCog
|
||||
}
|
||||
COGS = [CSCog, InsultsCog]
|
||||
|
||||
for cog in COGS:
|
||||
bot.add_cog(cog(bot))
|
||||
|
||||
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()
|
||||
def reload_feature(feature:str):
|
||||
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}...')
|
||||
target_cog_name = COG_REGISTRY[normalized_feature].__name__
|
||||
for existing_name in list(bot.cogs.keys()):
|
||||
if existing_name == target_cog_name:
|
||||
bot.remove_cog(existing_name)
|
||||
break
|
||||
|
||||
bot.add_cog(COG_REGISTRY[normalized_feature](bot))
|
||||
bot.remove_cog(feature.capitalize()+'Cog')
|
||||
cog = globals()[feature.capitalize()+'Cog']
|
||||
bot.add_cog(cog(bot))
|
||||
LOGGER.debug(f'{feature} reloaded!')
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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()]
|
||||
|
||||
78
tests/test_intern_voice.py
Normal file
78
tests/test_intern_voice.py
Normal 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()
|
||||
Reference in New Issue
Block a user