Compare commits
10 Commits
productivi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 60e95791a9 | |||
| 3fb926ec20 | |||
| d14ca4ab3e | |||
| 78d3a60bd7 | |||
| 746ce33977 | |||
| 80d34e9660 | |||
| 2cf7cc94bb | |||
| 425ce22e2c | |||
| b276afd6bb | |||
| 08da73fbda |
67
.gitea/workflows/cd.yaml
Normal file
67
.gitea/workflows/cd.yaml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
name: CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
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 compose file to VPS
|
||||||
|
uses: appleboy/scp-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
port: ${{ secrets.VPS_SSH_PORT }}
|
||||||
|
source: "compose.yaml"
|
||||||
|
target: "/opt/docker/botmafieux"
|
||||||
|
- name: Deploy on VPS
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: ${{ secrets.VPS_USER }}
|
||||||
|
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 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
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -160,4 +160,8 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
data/
|
data/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
*.disabled
|
||||||
|
|
||||||
|
config/
|
||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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"]
|
||||||
31
README.md
31
README.md
@@ -1,2 +1,31 @@
|
|||||||
# botmafieux
|
# botmafieux
|
||||||
Le Bot Discord du serveur des Mafieux
|
Le Bot Discord du serveur des Mafieux.
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
### Annonces d'anniversaire
|
||||||
|
|
||||||
|
Chaque utilisateur peut paramétrer sa date d'anniversaire, un message personnalisé qui sera envoyé à minuit le jour J et l'activation de l'envoi à l'aide de la commande `birthday_set` dans le salon défini.
|
||||||
|
La fonctionnalité peut être configurée par un administrateur avec la commande `birthday_config` qui peut activer/désactiver la fonctionnalité sur le serveur courant et choisir le salon dans lequel les annonces d'anniversaires seront envoyées.
|
||||||
|
|
||||||
|
### Rappels de productivité
|
||||||
|
|
||||||
|
Chaque utilisateur peut paramétrer un message à envoyer tous les $x$ jours dans le salon choisi et son activation à l'aide de la commande `productivity_set`. Un message sera envoyé tous les $x$ jours après le dernier message de l'utilisateur dans le salon défini.
|
||||||
|
La fonctionnalité peut être configurée par un administrateur avec la commande `productivity_config` qui peut activer/désactiver la fonctionnalité sur le serveur courant.
|
||||||
|
|
||||||
|
## Commandes
|
||||||
|
|
||||||
|
### Commandes utilisateurs
|
||||||
|
|
||||||
|
| Commande | Description | Options requises | Options facultatives |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `help` | Afficher la description des commandes | | |
|
||||||
|
| `birthday_set` | Paramétrer son anniversaire | `date`: date d'anniversaire au format "*JJ/MM*"<br>`announcements`: activation/désactivation des annonces d'anniversaire | `message`: message personnalisé à envoyer à minuit le jour J<br>`for_user`: utilisateur à paramétrer (administrateurs uniquement) |
|
||||||
|
| `productivity_set` | Paramétrer ses rappels de productivité | `days`: nombre de jours entre chaque rappel<br>`channel`: salon dans lequel envoyer les rappels<br>`enable`: activation/désactivation des rappels de productivité | `message`: message à envoyer<br>`for_user`: utilisateur à paramétrer (administrateurs uniquement) |
|
||||||
|
|
||||||
|
### Commandes administrateurs
|
||||||
|
| Commande | Description | Options requises | Options facultatives |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `birthday_config` | Configurer les annonces d'anniversaire sur le serveur | `enable`: activation/désactivation des annonces d'anniversaire<br>`channel`: salon dans lequel envoyer les annonces d'anniversaire | |
|
||||||
|
| `productivity_config` | Configurer les rappels de productivité sur le serveur | `enable`: activation/désactivation des rappels de productivité | |
|
||||||
|
| `reload` | Recharger une fonctionnalité | `feature`: fonctionnalité à recharger | |
|
||||||
391
cogs/cs.py
Normal file
391
cogs/cs.py
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
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")
|
||||||
|
|
||||||
|
MATCHES_FILE = os.path.join(Config.CONFIG_PATH,"matches.json")
|
||||||
|
|
||||||
|
# Cache duration in seconds (6 hours)
|
||||||
|
DATA_CACHE_DURATION = 3600 * 6
|
||||||
|
|
||||||
|
# Match age threshold in seconds (2 days)
|
||||||
|
OLD_MATCH_DURATION = 3600 * 24 * 2
|
||||||
|
|
||||||
|
# Reaction emojis for match events
|
||||||
|
EMOJI_CREATE_EVENT = "✅" # Create guild event for the match
|
||||||
|
EMOJI_REMOVE_EVENT = "❌" # Remove guild event for the match
|
||||||
|
|
||||||
|
class CSCog(commands.Cog):
|
||||||
|
def __init__(self, bot:discord.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.matches:list[Match] = []
|
||||||
|
self.message_match_map = {} # Maps message IDs to Match objects
|
||||||
|
self.get_matches.start()
|
||||||
|
|
||||||
|
def is_up_to_date(self) -> bool:
|
||||||
|
"""Check if PAGE_FILE and MATCHES_FILE are up to date."""
|
||||||
|
try:
|
||||||
|
page_modified = datetime.datetime.fromtimestamp(os.stat(PAGE_FILE).st_ctime)
|
||||||
|
matches_modified = datetime.datetime.fromtimestamp(os.stat(MATCHES_FILE).st_ctime)
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
page_age = now.timestamp() - page_modified.timestamp()
|
||||||
|
matches_age = now.timestamp() - matches_modified.timestamp()
|
||||||
|
|
||||||
|
return page_age <= DATA_CACHE_DURATION and matches_age <= DATA_CACHE_DURATION
|
||||||
|
except FileNotFoundError:
|
||||||
|
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")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def save(self) -> None:
|
||||||
|
"""Save self.matches to MATCHES_FILE."""
|
||||||
|
matches_data = [match.to_dict() for match in self.matches]
|
||||||
|
|
||||||
|
with open(MATCHES_FILE, 'w') as f:
|
||||||
|
json.dump(matches_data, f, indent=2)
|
||||||
|
|
||||||
|
def remove_old_matches(self) -> None:
|
||||||
|
"""Remove matches that are older than OLD_MATCH_DURATION."""
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
filtered_matches = []
|
||||||
|
|
||||||
|
for match in self.matches:
|
||||||
|
if match.date is None:
|
||||||
|
# Keep matches with no date
|
||||||
|
filtered_matches.append(match)
|
||||||
|
else:
|
||||||
|
match_age = now.timestamp() - match.date.timestamp()
|
||||||
|
if match_age <= OLD_MATCH_DURATION:
|
||||||
|
filtered_matches.append(match)
|
||||||
|
|
||||||
|
self.matches = filtered_matches
|
||||||
|
|
||||||
|
@tasks.loop(minutes=5)
|
||||||
|
async def get_matches(self):
|
||||||
|
if self.is_up_to_date():
|
||||||
|
# Load from MATCHES_FILE
|
||||||
|
try:
|
||||||
|
with open(MATCHES_FILE) as f:
|
||||||
|
matches_data = json.load(f)
|
||||||
|
|
||||||
|
self.matches = [Match.from_dict(match_data) for match_data in matches_data]
|
||||||
|
self.remove_old_matches()
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.error(f"Error loading matches from cache: {e}")
|
||||||
|
self.matches = []
|
||||||
|
else:
|
||||||
|
# Fetch new data
|
||||||
|
self.get_page()
|
||||||
|
|
||||||
|
# Read and parse the page
|
||||||
|
with open(PAGE_FILE) as f:
|
||||||
|
html = f.read()
|
||||||
|
|
||||||
|
page = BeautifulSoup(html)
|
||||||
|
|
||||||
|
all_matches = []
|
||||||
|
for section in page.find_all("div", {"class": "matches-chronologically"}):
|
||||||
|
all_matches.extend(section.find_all("div", {"class": "match-wrapper"}))
|
||||||
|
|
||||||
|
self.matches = []
|
||||||
|
for match_event in all_matches:
|
||||||
|
event = match_event.find("div", {"class": "match-event"})
|
||||||
|
event_name = event.attrs["data-event-headline"]
|
||||||
|
|
||||||
|
# Extract event logo
|
||||||
|
event_logo_container = event.find("div", {"class": "match-event-logo-container"})
|
||||||
|
event_logo_url = None
|
||||||
|
if event_logo_container:
|
||||||
|
logo_img = event_logo_container.find("img")
|
||||||
|
if logo_img and logo_img.attrs.get("src"):
|
||||||
|
event_logo_url = logo_img.attrs["src"]
|
||||||
|
|
||||||
|
event_obj = Event(int(event.attrs["data-event-id"]), event_name, event_logo_url)
|
||||||
|
|
||||||
|
href = str(match_event.find("a", {"class": "match-top"}).attrs["href"])
|
||||||
|
match_href = "https://hltv.org" + href
|
||||||
|
|
||||||
|
match_time_elt = match_event.find("div", {"class": "match-time"})
|
||||||
|
if match_time_elt:
|
||||||
|
match_timestamp = int(match_time_elt.attrs["data-unix"])
|
||||||
|
match_date = datetime.datetime.fromtimestamp(match_timestamp/1000)
|
||||||
|
else:
|
||||||
|
match_date = None
|
||||||
|
|
||||||
|
match_format = match_event.find("div", {"class": "match-meta"}).text
|
||||||
|
|
||||||
|
teams = match_event.find_all("div", {"class": "match-team"})
|
||||||
|
match_teams = []
|
||||||
|
for match_team in teams:
|
||||||
|
team = match_team.find("div", {"class": "match-teamname"})
|
||||||
|
team_obj = Team(team.text, match_team.find("img").attrs["src"])
|
||||||
|
match_teams.append(team_obj)
|
||||||
|
|
||||||
|
if len(match_teams) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match_obj = Match(match_href, match_format, event_obj, match_teams, match_date)
|
||||||
|
self.matches.append(match_obj)
|
||||||
|
|
||||||
|
# Remove old matches and save to cache
|
||||||
|
self.remove_old_matches()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
if self.matches:
|
||||||
|
print(str(self.matches[0]))
|
||||||
|
|
||||||
|
def check_event(self, match: Match, event: discord.ScheduledEvent) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a scheduled event matches the given match.
|
||||||
|
Returns True if both team names appear in the event name (case-insensitive).
|
||||||
|
"""
|
||||||
|
event_name_lower = event.name.lower()
|
||||||
|
team1_name_lower = match.teams[0].name.lower()
|
||||||
|
team2_name_lower = match.teams[1].name.lower()
|
||||||
|
|
||||||
|
# Check if both team names are in the event name
|
||||||
|
return team1_name_lower in event_name_lower and team2_name_lower in event_name_lower
|
||||||
|
|
||||||
|
async def create_guild_event(self, guild: discord.Guild, match: Match) -> discord.ScheduledEvent | None:
|
||||||
|
"""
|
||||||
|
Create a scheduled guild event for the match.
|
||||||
|
Returns the created event or None if it already exists or creation fails.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check if event already exists
|
||||||
|
for event in guild.scheduled_events:
|
||||||
|
if self.check_event(match, event):
|
||||||
|
return event
|
||||||
|
|
||||||
|
# Create event name
|
||||||
|
event_name = f"{match.teams[0].name} vs {match.teams[1].name}"
|
||||||
|
|
||||||
|
# Set start and end times - use UTC timezone to match the embed display
|
||||||
|
if match.date:
|
||||||
|
# Convert the timestamp to UTC-aware datetime
|
||||||
|
timestamp = int(match.date.timestamp())
|
||||||
|
start_time = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
||||||
|
end_time = start_time + datetime.timedelta(hours=2) # Assume 2 hour match
|
||||||
|
else:
|
||||||
|
start_time = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||||
|
end_time = start_time + datetime.timedelta(hours=2)
|
||||||
|
|
||||||
|
# Create the event
|
||||||
|
event = await guild.create_scheduled_event(
|
||||||
|
name=event_name,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
description=f"Event: {match.event.name}\nFormat: {match.format}\nWatch on HLTV: {match.url}",
|
||||||
|
location=""
|
||||||
|
)
|
||||||
|
LOGGER.info(f"Created guild event for {event_name} in {guild.name}")
|
||||||
|
return event
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.error(f"Error creating guild event in {guild.name}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def remove_guild_event(self, guild: discord.Guild, match: Match) -> bool:
|
||||||
|
"""
|
||||||
|
Remove the scheduled guild event for the match.
|
||||||
|
Returns True if an event was removed, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
for event in guild.scheduled_events:
|
||||||
|
if self.check_event(match, event):
|
||||||
|
await event.delete()
|
||||||
|
LOGGER.info(f"Removed guild event {event.name} from {guild.name}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.error(f"Error removing guild event from {guild.name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_match_embed(self, match: Match) -> discord.Embed:
|
||||||
|
"""
|
||||||
|
Create a Discord embed message containing match information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
match: The Match object to display
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
discord.Embed: A formatted embed with match details
|
||||||
|
"""
|
||||||
|
# Create embed with title as the matchup
|
||||||
|
title = f"{match.teams[0].name} vs {match.teams[1].name}"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=title,
|
||||||
|
description=f"**Event:** {match.event.name}",
|
||||||
|
url=match.url,
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add match format
|
||||||
|
embed.add_field(
|
||||||
|
name="Format",
|
||||||
|
value=match.format,
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add match date if available
|
||||||
|
if match.date:
|
||||||
|
embed.add_field(
|
||||||
|
name="Date & Time",
|
||||||
|
value=f"<t:{int(match.date.timestamp())}:F>",
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add team information with logos
|
||||||
|
teams_info = f"[{match.teams[0].name}]({match.url})\n[{match.teams[1].name}]({match.url})"
|
||||||
|
embed.add_field(
|
||||||
|
name="Teams",
|
||||||
|
value=teams_info,
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set thumbnail to event logo if available, otherwise use first team logo
|
||||||
|
if match.event.logo_url:
|
||||||
|
embed.set_thumbnail(url=match.event.logo_url)
|
||||||
|
elif match.teams[0].logo_url:
|
||||||
|
embed.set_thumbnail(url=match.teams[0].logo_url)
|
||||||
|
|
||||||
|
# Set footer with HLTV link
|
||||||
|
embed.set_footer(
|
||||||
|
text="HLTV.org",
|
||||||
|
icon_url="https://www.hltv.org/img/static/TopLogo2x.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
return embed
|
||||||
|
|
||||||
|
@bridge.bridge_command()
|
||||||
|
async def test_match_embed(self, ctx:bridge.BridgeExtContext):
|
||||||
|
"""Send the first match as an embed to the test channel."""
|
||||||
|
TEST_CHANNEL_ID = 1305147782071451718
|
||||||
|
|
||||||
|
# Check if there are matches available
|
||||||
|
if not self.matches:
|
||||||
|
await ctx.send("❌ No matches available. Try running the scraper first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the first match
|
||||||
|
first_match = self.matches[0]
|
||||||
|
|
||||||
|
# Create the embed
|
||||||
|
embed = self.create_match_embed(first_match)
|
||||||
|
|
||||||
|
# Send to test channel
|
||||||
|
try:
|
||||||
|
channel = self.bot.get_channel(TEST_CHANNEL_ID)
|
||||||
|
if channel is None:
|
||||||
|
await ctx.send(f"❌ Could not find channel with ID {TEST_CHANNEL_ID}")
|
||||||
|
return
|
||||||
|
|
||||||
|
message = await channel.send(embed=embed)
|
||||||
|
|
||||||
|
# Add reactions
|
||||||
|
await message.add_reaction(EMOJI_CREATE_EVENT)
|
||||||
|
await message.add_reaction(EMOJI_REMOVE_EVENT)
|
||||||
|
|
||||||
|
# Track the message -> match mapping
|
||||||
|
self.message_match_map[message.id] = first_match
|
||||||
|
|
||||||
|
await ctx.reply(f"✅ Sent embed for match: {first_match.teams[0].name} vs {first_match.teams[1].name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.error(f"Error sending embed: {e}")
|
||||||
|
await ctx.send(f"❌ Error sending embed: {e}")
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
|
||||||
|
"""Handle reactions added to match embed messages."""
|
||||||
|
# Ignore reactions from the bot itself
|
||||||
|
if payload.user_id == self.bot.user.id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if this is a tracked message
|
||||||
|
if payload.message_id not in self.message_match_map:
|
||||||
|
return
|
||||||
|
|
||||||
|
match = self.message_match_map[payload.message_id]
|
||||||
|
emoji = payload.emoji.name
|
||||||
|
|
||||||
|
# Get the guild
|
||||||
|
guild = self.bot.get_guild(payload.guild_id)
|
||||||
|
if guild is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the message for replies
|
||||||
|
message = await self.get_reaction_message(payload)
|
||||||
|
if not message:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if emoji == EMOJI_CREATE_EVENT:
|
||||||
|
# Create event
|
||||||
|
event = await self.create_guild_event(guild, match)
|
||||||
|
if event:
|
||||||
|
await message.reply(f"✅ Created guild event: **{event.name}**")
|
||||||
|
elif emoji == EMOJI_REMOVE_EVENT:
|
||||||
|
# Remove event
|
||||||
|
removed = await self.remove_guild_event(guild, match)
|
||||||
|
if removed:
|
||||||
|
await message.reply(
|
||||||
|
f"✅ Removed guild event for {match.teams[0].name} vs {match.teams[1].name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await message.reply(
|
||||||
|
f"⚠️ No guild event found for this match"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.error(f"Error handling reaction {emoji}: {e}")
|
||||||
|
|
||||||
|
async def get_reaction_message(self, payload: discord.RawReactionActionEvent) -> discord.Message | None:
|
||||||
|
"""Get the message object from a raw reaction payload."""
|
||||||
|
try:
|
||||||
|
channel = self.bot.get_channel(payload.channel_id)
|
||||||
|
if channel:
|
||||||
|
return await channel.fetch_message(payload.message_id)
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.error(f"Error fetching message: {e}")
|
||||||
|
return None
|
||||||
|
async def check_events(self):
|
||||||
|
"""Check scheduled events for matching CS matches."""
|
||||||
|
for match in self.matches:
|
||||||
|
for guild in self.bot.guilds:
|
||||||
|
events = [event for event in guild.scheduled_events if self.check_event(match, event)]
|
||||||
|
# print(guild.name, match)
|
||||||
|
|
||||||
|
# @commands.Cog.listener()
|
||||||
|
# async def on_ready(self):
|
||||||
|
# self.check_events.start()
|
||||||
|
|
||||||
32
cogs/insult.py
Normal file
32
cogs/insult.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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:
|
||||||
|
self.insults = [l.strip() for l in f.readlines()]
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_message(self, message:discord.Message):
|
||||||
|
if len(message.content.split()) > 3:
|
||||||
|
author = message.author
|
||||||
|
if any(author.id == u.id for u in self.men):
|
||||||
|
r = int(random.random()*100)
|
||||||
|
print(r)
|
||||||
|
if r < 5:
|
||||||
|
insult = random.choice(self.insults)
|
||||||
|
await message.reply(insult)
|
||||||
|
|
||||||
|
@commands.Cog.listener()
|
||||||
|
async def on_ready(self):
|
||||||
|
self.men:list[discord.Member] = []
|
||||||
|
for guild in self.bot.guilds:
|
||||||
|
for role in guild.roles:
|
||||||
|
if re.match(r'^([hH]im|[hH]e).*$',role.name):
|
||||||
|
self.men.extend(role.members)
|
||||||
|
self.men = list(set(self.men))
|
||||||
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())
|
||||||
22
compose.yaml
Normal file
22
compose.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
internal: true
|
||||||
|
outbound:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
botmafieux:
|
||||||
|
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
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 ?
|
||||||
|
159
main.py
159
main.py
@@ -1,167 +1,34 @@
|
|||||||
import os, discord, json, sys, random
|
import os, discord, json, sys, random
|
||||||
import modules.setup.db as db
|
|
||||||
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 modules.setup.bot import bot, reload_feature
|
from setup.bot import bot, reload_feature, register_cogs
|
||||||
from modules.setup.logger import LOGGER
|
from setup.logger import LOGGER
|
||||||
from modules.setup.data import get_guild_data, create_guild_data, delete_guild_data, save_guild_data
|
from setup.config import Config
|
||||||
from modules.cogs.productivity import TASKS as PRODUCTIVITY_TASKS, send_reminder
|
|
||||||
|
|
||||||
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():
|
||||||
LOGGER.info(f'{bot.user.name} has connected to Discord!')
|
if bot.user:
|
||||||
LOGGER.debug(f'Guilds: {bot.guilds}')
|
LOGGER.info(f'{bot.user.name} has connected to Discord!')
|
||||||
if EXEC_ARGS.reload and EXEC_ARGS.guild_id and EXEC_ARGS.channel_id:
|
LOGGER.debug(f"Guilds: {','.join([guild.name for guild in bot.guilds])}")
|
||||||
guild = bot.get_guild(EXEC_ARGS.guild_id)
|
|
||||||
channel = guild.get_channel_or_thread(EXEC_ARGS.channel_id)
|
|
||||||
await channel.send(f'{bot.user.name} Bot reloaded!',silent=True)
|
|
||||||
birthday_anouncements_task.start()
|
|
||||||
for guild in bot.guilds:
|
|
||||||
for user in guild.members:
|
|
||||||
user_id = user.id
|
|
||||||
db_user = db.select('global_user',f'user_id = {user_id}')
|
|
||||||
if not db_user:
|
|
||||||
db.insert('global_user',user_id=user_id,user_name=user.name,user_mention=user.mention)
|
|
||||||
|
|
||||||
productivity_settings = db.select('productivity_settings',f'guild_id = {guild.id}')
|
|
||||||
if productivity_settings:
|
|
||||||
if productivity_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']:
|
|
||||||
next_reminder = user_productivity_data['next_reminder']
|
|
||||||
if next_reminder > datetime.now().timestamp():
|
|
||||||
cooldown = next_reminder - datetime.now().timestamp()
|
|
||||||
channel = guild.get_channel(user_productivity_data['channel_id'])
|
|
||||||
reminder_message = user_productivity_data['reminder_message']
|
|
||||||
PRODUCTIVITY_TASKS[str(user.id)] = create_task(send_reminder(cooldown,user,channel,reminder_message))
|
|
||||||
else:
|
|
||||||
user_productivity_data['next_reminder'] = datetime.now().timestamp() + user_productivity_data['cooldown']
|
|
||||||
db.update('guild_user_productivity',f'user_id = {user.id} AND guild_id = {guild.id}',next_reminder=user_productivity_data['next_reminder'])
|
|
||||||
channel = guild.get_channel(user_productivity_data['channel_id'])
|
|
||||||
reminder_message = user_productivity_data['reminder_message']
|
|
||||||
PRODUCTIVITY_TASKS[str(user.id)] = create_task(send_reminder(user_productivity_data['cooldown'],user,channel,reminder_message))
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'No productivity settings found for guild {guild.name} ({guild.id}).')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
db.check_conn()
|
token = Config.TOKEN
|
||||||
result = db.select('token','token_name = \'discord\'','token_value')
|
if token:
|
||||||
if result:
|
bot.run(token)
|
||||||
TOKEN = result['token_value']
|
|
||||||
bot.run(TOKEN)
|
|
||||||
else:
|
else:
|
||||||
LOGGER.error('No token found.')
|
LOGGER.error('No token found.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
119
models/cs_models.py
Normal file
119
models/cs_models.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
def __init__(self, event_id:int, name:str, logo_url:str = None) -> None:
|
||||||
|
self.__event_id = event_id
|
||||||
|
self.__name = name
|
||||||
|
self.__logo_url = logo_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_id(self):
|
||||||
|
return self.__event_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logo_url(self):
|
||||||
|
return self.__logo_url
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'event_id': self.event_id,
|
||||||
|
'name': self.name,
|
||||||
|
'logo_url': self.logo_url
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> 'Event':
|
||||||
|
return cls(data['event_id'], data['name'], data.get('logo_url'))
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"<Event event_id={self.event_id} name='{self.name}'>"
|
||||||
|
|
||||||
|
|
||||||
|
class Team:
|
||||||
|
def __init__(self, name:str, logo_url:str) -> None:
|
||||||
|
self.__name = name
|
||||||
|
self.__logo_url = logo_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def logo_url(self):
|
||||||
|
return self.__logo_url
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'name': self.name,
|
||||||
|
'logo_url': self.logo_url
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> 'Team':
|
||||||
|
return cls(data['name'], data['logo_url'])
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"<Team logo_url='{self.logo_url}' name='{self.name}'>"
|
||||||
|
|
||||||
|
class Match:
|
||||||
|
def __init__(self, url:str, format:str, event:Event, teams:list[Team], date=None):
|
||||||
|
self.__date = date
|
||||||
|
self.__url = url
|
||||||
|
self.__format = format
|
||||||
|
self.__event = event
|
||||||
|
self.__teams = tuple(teams)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
return self.__date
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return self.__url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format(self):
|
||||||
|
return self.__format
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event(self):
|
||||||
|
return self.__event
|
||||||
|
|
||||||
|
@property
|
||||||
|
def teams(self):
|
||||||
|
return self.__teams
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_iso(self):
|
||||||
|
return self.__date.isoformat()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def teams_names(self):
|
||||||
|
return (self.__teams[0].name, self.__teams[1].name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def teams_logos(self):
|
||||||
|
return (self.__teams[0].logo_url, self.__teams[1].logo_url)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
return {
|
||||||
|
'url': self.url,
|
||||||
|
'format': self.format,
|
||||||
|
'event': self.event.to_dict(),
|
||||||
|
'teams': [team.to_dict() for team in self.teams],
|
||||||
|
'date': self.date.isoformat() if self.date else None
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict) -> 'Match':
|
||||||
|
event = Event.from_dict(data['event'])
|
||||||
|
teams = [Team.from_dict(team_data) for team_data in data['teams']]
|
||||||
|
date = datetime.datetime.fromisoformat(data['date']) if data['date'] else None
|
||||||
|
return cls(data['url'], data['format'], event, teams, date)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"<Match date='{self.date}' url='{self.url}' format='{self.url}' event={self.event} teams='{','.join(self.teams_names)}'>"
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import discord, re
|
|
||||||
from discord import option
|
|
||||||
from discord.ext import commands
|
|
||||||
from datetime import datetime
|
|
||||||
from ..setup.logger import LOGGER
|
|
||||||
from ..setup.data import get_guild_data, save_guild_data, is_feature_enabled
|
|
||||||
from ..setup import db
|
|
||||||
|
|
||||||
class BirthdayCog(commands.Cog):
|
|
||||||
def __init__(self, bot):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.slash_command(description='Sets birthday date. Must be in DAY/MONTH format.')
|
|
||||||
@option(name='date',description='Birthday date.',required=True)
|
|
||||||
@option(name='announcements',description='Enable or disable birthday announcements.',required=True,type=bool)
|
|
||||||
@option(name='message',description='The message to send when it is the user\'s birthday.',required=False)
|
|
||||||
@option(name='for_user',description='The user to set the birthday date for.',required=False,type=discord.Member)
|
|
||||||
async def birthday_set(self,ctx:discord.ApplicationContext, date:str,announcements:bool,message:str,for_user:discord.Member):
|
|
||||||
"""Sets birthday date. Must be in DAY/MONTH format."""
|
|
||||||
command_name = 'birthday_set'
|
|
||||||
guild_id = ctx.guild.id
|
|
||||||
author = ctx.author
|
|
||||||
birthday_settings = db.select('birthday_settings',f'guild_id = {guild_id}')
|
|
||||||
if birthday_settings:
|
|
||||||
if birthday_settings['is_enabled']:
|
|
||||||
if for_user:
|
|
||||||
if author.guild_permissions.administrator:
|
|
||||||
if re.match(r'^\d{1,2}\/\d{1,2}$', date):
|
|
||||||
user_data = db.select('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {for_user.id}')
|
|
||||||
if user_data:
|
|
||||||
# Update user data
|
|
||||||
if message:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message} for {for_user.name}.')
|
|
||||||
db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {for_user.id}',birthday=date,is_enabled=announcements,birthday_message=message)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} for {for_user.name}.')
|
|
||||||
db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {for_user.id}',birthday=date,is_enabled=announcements)
|
|
||||||
await ctx.send_response(f'{for_user.name}\'s birthday has been set to {date}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
# Create user data
|
|
||||||
if message:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message} for {for_user.name}.')
|
|
||||||
db.insert('guild_user_birthday',guild_id=guild_id,user_id=for_user.id,birthday=date,is_enabled=announcements,birthday_message=message)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} for {for_user.name}.')
|
|
||||||
db.insert('guild_user_birthday',guild_id=guild_id,user_id=for_user.id,birthday=date,is_enabled=announcements)
|
|
||||||
await ctx.send_response(f'{for_user.name}\'s birthday has been set to {date}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} for {for_user.name} but the date format is invalid.')
|
|
||||||
await ctx.send_response('The date format must be DAY/MONTH.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} for {for_user.name} but is not an administrator.')
|
|
||||||
await ctx.send_response('You must be an administrator to run the command with "for_user".',ephemeral=True)
|
|
||||||
else:
|
|
||||||
if re.match(r'^\d{1,2}\/\d{1,2}$', date):
|
|
||||||
user_data = db.select('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {author.id}')
|
|
||||||
if user_data:
|
|
||||||
# Update user data
|
|
||||||
if message:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message}.')
|
|
||||||
db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {author.id}',birthday=date,is_enabled=announcements,birthday_message=message)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements}.')
|
|
||||||
db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {author.id}',birthday=date,is_enabled=announcements)
|
|
||||||
await ctx.send_response(f'Your birthday has been set to {date}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
# Create user data
|
|
||||||
if message:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message}.')
|
|
||||||
db.insert('guild_user_birthday',guild_id=guild_id,user_id=author.id,birthday=date,is_enabled=announcements,birthday_message=message)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements}.')
|
|
||||||
db.insert('guild_user_birthday',guild_id=guild_id,user_id=author.id,birthday=date,is_enabled=announcements)
|
|
||||||
await ctx.send_response(f'Your birthday has been set to {date}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} but the date format is invalid.')
|
|
||||||
await ctx.send_response('The date format must be DAY/MONTH.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} but the birthday feature is not enabled.')
|
|
||||||
await ctx.send_response('The birthday feature is not enabled.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} but the birthday feature is not enabled.')
|
|
||||||
await ctx.send_response('The birthday feature is not enabled.',ephemeral=True)
|
|
||||||
|
|
||||||
@commands.slash_command(description='Configurate birthday announcements.')
|
|
||||||
@option(name='channel',description='The channel to send birthday announcements to.',required=True,type=discord.TextChannel)
|
|
||||||
@option(name='enable',description='Enable or disable birthday announcements.',required=True,type=bool)
|
|
||||||
async def birthday_config(self,ctx:discord.ApplicationContext, channel:discord.TextChannel,enable:bool):
|
|
||||||
"""Configurate birthday announcements."""
|
|
||||||
command_name = 'birthday_config'
|
|
||||||
guild_id = ctx.guild.id
|
|
||||||
author = ctx.author
|
|
||||||
birthday_settings = db.select('birthday_settings',f'guild_id = {guild_id}')
|
|
||||||
if birthday_settings:
|
|
||||||
# Update guild birthday settings
|
|
||||||
if birthday_settings['is_enabled']:
|
|
||||||
if author.guild_permissions.administrator:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.name} {enable}.')
|
|
||||||
db.update('birthday_settings',f'guild_id = {guild_id}',channel_id=channel.id,is_enabled=enable)
|
|
||||||
await ctx.send_response(f'Birthday announcements have been {"enabled" if enable else "disabled"} in {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.name} {enable} but is not an administrator.')
|
|
||||||
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.name} {enable} but the birthday feature is not enabled.')
|
|
||||||
await ctx.send_response('The birthday feature is not enabled.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
# Create guild birthday settings
|
|
||||||
if author.guild_permissions.administrator:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.name} {enable}.')
|
|
||||||
db.insert('birthday_settings',guild_id=guild_id,channel_id=channel.id,is_enabled=enable)
|
|
||||||
await ctx.send_response(f'Birthday announcements have been {"enabled" if enable else "disabled"} in {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.name} {enable} but is not an administrator.')
|
|
||||||
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import discord
|
|
||||||
from datetime import datetime
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
from discord import option
|
|
||||||
from typing import Union, Optional
|
|
||||||
from asyncio import sleep, create_task
|
|
||||||
from ..setup.logger import LOGGER
|
|
||||||
from ..setup import db
|
|
||||||
from ..setup.data import get_guild_data, save_guild_data, is_feature_enabled
|
|
||||||
|
|
||||||
TASKS = {}
|
|
||||||
|
|
||||||
async def send_reminder(cooldown:int,author:discord.Member,channel:discord.TextChannel,reminder_message:str):
|
|
||||||
while True:
|
|
||||||
await sleep(cooldown)
|
|
||||||
LOGGER.debug(f'Sending productivity reminder to {author.name} in {channel.name} of {channel.guild.name}.')
|
|
||||||
message = reminder_message.replace('{user}',author.mention)
|
|
||||||
await channel.send(message)
|
|
||||||
|
|
||||||
class ProductivityCog(commands.Cog):
|
|
||||||
|
|
||||||
def __init__(self,bot:discord.Bot) -> None:
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
@commands.slash_command(description='Configurate productivity reminder.')
|
|
||||||
@option(name='channel',description='The channel to send productivity reminders in.',required=True)
|
|
||||||
@option(name='enable',description='Enable or disable productivity reminders.',required=True)
|
|
||||||
@option(name='days',description='Number of days to wait before sending a reminder if the user has not been active in the channel.',required=True,type=int)
|
|
||||||
@option(name='custom_message',description='Custom message to send with the reminder.',required=False,type=str)
|
|
||||||
@option(name='for_user',description='The user to configurate productivity reminders for.',required=False,type=discord.Member)
|
|
||||||
async def productivity_set(self,ctx:discord.ApplicationContext, channel:Union[discord.TextChannel,discord.Thread], enable:bool, days:int, custom_message=None, for_user:discord.Member=None):
|
|
||||||
"""Configurate productivity reminder."""
|
|
||||||
command_name = 'productivity_set'
|
|
||||||
guild_id = ctx.guild.id
|
|
||||||
author = ctx.author
|
|
||||||
productivity_settings = db.select('productivity_settings',f'guild_id = {guild_id}')
|
|
||||||
if productivity_settings:
|
|
||||||
if productivity_settings['is_enabled']:
|
|
||||||
if for_user:
|
|
||||||
if author.guild_permissions.administrator:
|
|
||||||
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}')
|
|
||||||
cooldown = days*60*60*24
|
|
||||||
next_reminder = int(datetime.now().timestamp() + cooldown)
|
|
||||||
if user_data:
|
|
||||||
# Update user data
|
|
||||||
if custom_message:
|
|
||||||
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message} for {for_user.name}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} for {for_user.name}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
# Create user data
|
|
||||||
if custom_message:
|
|
||||||
db.insert('guild_user_productivity',guild_id=guild_id,user_id=for_user.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message} for {for_user.name}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
db.insert('guild_user_productivity',guild_id=guild_id,user_id=for_user.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} for {for_user.name}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}')
|
|
||||||
if str(for_user.id) in TASKS.keys():
|
|
||||||
TASKS[str(for_user.id)].cancel()
|
|
||||||
TASKS[str(for_user.id)] = create_task(send_reminder(cooldown,for_user,channel,user_data['reminder_message']))
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.mention} {enable} {days} {custom_message} for {for_user.name} but is not an administrator.')
|
|
||||||
await ctx.send_response('You must be an administrator to run the command with "for_user".',ephemeral=True)
|
|
||||||
else:
|
|
||||||
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}')
|
|
||||||
cooldown = days*60*60*24
|
|
||||||
next_reminder = int(datetime.now().timestamp() + cooldown)
|
|
||||||
if user_data:
|
|
||||||
# Update user data
|
|
||||||
if custom_message:
|
|
||||||
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
# Create user data
|
|
||||||
if custom_message:
|
|
||||||
db.insert('guild_user_productivity',guild_id=guild_id,user_id=author.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
db.insert('guild_user_productivity',guild_id=guild_id,user_id=author.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days}.')
|
|
||||||
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
|
|
||||||
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}')
|
|
||||||
if str(author.id) in TASKS.keys():
|
|
||||||
TASKS[str(author.id)].cancel()
|
|
||||||
TASKS[str(author.id)] = create_task(send_reminder(cooldown,author,channel,user_data['reminder_message']))
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.mention} {enable} {days} {custom_message} but the productivity feature is not enabled.')
|
|
||||||
await ctx.send_response('The productivity feature is not enabled.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.mention} {enable} {days} {custom_message} but no productivity settings were found.')
|
|
||||||
await ctx.send_response('No productivity settings were found.',ephemeral=True)
|
|
||||||
|
|
||||||
@commands.slash_command(description='Enable or disable productivity reminders on the server.')
|
|
||||||
@option(name='enable',description='Enable or disable productivity reminders.',required=True)
|
|
||||||
async def productivity_config(self,ctx:discord.ApplicationContext, enable:bool):
|
|
||||||
"""Enable or disable productivity reminders on the server."""
|
|
||||||
command_name = 'productivity_config'
|
|
||||||
guild_id = ctx.guild.id
|
|
||||||
author = ctx.author
|
|
||||||
productivity_settings = db.select('productivity_settings',f'guild_id = {guild_id}')
|
|
||||||
if productivity_settings:
|
|
||||||
# Update guild productivity settings
|
|
||||||
if author.guild_permissions.administrator:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {enable}.')
|
|
||||||
db.update('productivity_settings',f'guild_id = {guild_id}',is_enabled=enable)
|
|
||||||
await ctx.send_response(f'Productivity reminders have been {"enabled" if enable else "disabled"}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {enable} but is not an administrator.')
|
|
||||||
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
# Create guild productivity settings
|
|
||||||
if author.guild_permissions.administrator:
|
|
||||||
LOGGER.debug(f'{author.name} used /{command_name} {enable}.')
|
|
||||||
db.insert('productivity_settings',guild_id=guild_id,is_enabled=enable)
|
|
||||||
await ctx.send_response(f'Productivity reminders have been {"enabled" if enable else "disabled"}.',ephemeral=True)
|
|
||||||
else:
|
|
||||||
LOGGER.debug(f'{author.name} tried to use /{command_name} {enable} but is not an administrator.')
|
|
||||||
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_message(self,message:discord.Message):
|
|
||||||
author = message.author
|
|
||||||
if author.id != self.bot.user.id: # Ignore messages from the bot
|
|
||||||
guild_id = message.guild.id
|
|
||||||
productivity_settings = db.select('productivity_settings',f'guild_id = {guild_id}')
|
|
||||||
if productivity_settings:
|
|
||||||
if productivity_settings['is_enabled']:
|
|
||||||
user_productivity_data = db.select('guild_user_productivity',f'user_id = {author.id} AND guild_id = {guild_id}')
|
|
||||||
if user_productivity_data:
|
|
||||||
if user_productivity_data['is_enabled']:
|
|
||||||
next_reminder = int(datetime.now().timestamp() + user_productivity_data['cooldown'])
|
|
||||||
db.update('guild_user_productivity',f'user_id = {author.id} AND guild_id = {guild_id}',next_reminder=next_reminder)
|
|
||||||
if str(author.id) in TASKS.keys():
|
|
||||||
TASKS[str(author.id)].cancel()
|
|
||||||
TASKS[str(author.id)] = create_task(send_reminder(user_productivity_data['cooldown'],author,message.channel,user_productivity_data['reminder_message']))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import discord
|
|
||||||
from ..cogs.birthday import BirthdayCog
|
|
||||||
from ..cogs.productivity import ProductivityCog
|
|
||||||
from .logger import LOGGER
|
|
||||||
|
|
||||||
bot_intents = discord.Intents.default()
|
|
||||||
bot_intents.message_content = True
|
|
||||||
bot_intents.members = True
|
|
||||||
bot_intents.presences = True
|
|
||||||
bot_intents.guilds = True
|
|
||||||
bot_intents.reactions = True
|
|
||||||
|
|
||||||
bot = discord.Bot(command_prefix='$', intents=bot_intents)
|
|
||||||
|
|
||||||
COGS = [BirthdayCog, ProductivityCog]
|
|
||||||
|
|
||||||
for cog in COGS:
|
|
||||||
bot.add_cog(cog(bot))
|
|
||||||
|
|
||||||
def reload_feature(feature:str):
|
|
||||||
LOGGER.debug(bot.cogs)
|
|
||||||
LOGGER.debug(f'Reloading {feature}...')
|
|
||||||
bot.remove_cog(feature.capitalize()+'Cog')
|
|
||||||
cog = globals()[feature.capitalize()+'Cog']
|
|
||||||
bot.add_cog(cog(bot))
|
|
||||||
LOGGER.debug(f'{feature} reloaded!')
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
import os, psycopg2
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from typing import Optional, Callable, Any, Union
|
|
||||||
from .logger import LOGGER
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
DB_NAME = os.getenv('DB_NAME')
|
|
||||||
DB_USER = os.getenv('DB_USER')
|
|
||||||
DB_PASSWORD = os.getenv('DB_PASSWORD')
|
|
||||||
DB_HOST = os.getenv('DB_HOST')
|
|
||||||
|
|
||||||
def check_conn():
|
|
||||||
"""Tests the database connection."""
|
|
||||||
connection = psycopg2.connect(
|
|
||||||
database=DB_NAME,
|
|
||||||
user=DB_USER,
|
|
||||||
password=DB_PASSWORD,
|
|
||||||
host=DB_HOST,
|
|
||||||
port="5432"
|
|
||||||
)
|
|
||||||
if connection.status:
|
|
||||||
LOGGER.debug('Established connection to database.')
|
|
||||||
LOGGER.debug(f'Connection status: {connection.status}')
|
|
||||||
else:
|
|
||||||
LOGGER.error('Failed to connect to database.')
|
|
||||||
connection.close()
|
|
||||||
LOGGER.debug('Closed connection to database.')
|
|
||||||
|
|
||||||
def insert(table:str,**kwargs):
|
|
||||||
"""
|
|
||||||
Inserts the given data into the given table.
|
|
||||||
|
|
||||||
Global_User
|
|
||||||
user_id: int
|
|
||||||
user_mention: str
|
|
||||||
user_mention: str
|
|
||||||
|
|
||||||
Guild
|
|
||||||
guild_id: int
|
|
||||||
|
|
||||||
Birthday_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
|
|
||||||
Productivity_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Birthday
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
birthday: int
|
|
||||||
birthday_message: str | None
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Productivity
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
cooldown: int
|
|
||||||
next_reminder: int
|
|
||||||
reminder_message: str | None
|
|
||||||
"""
|
|
||||||
connection = psycopg2.connect(
|
|
||||||
database=DB_NAME,
|
|
||||||
user=DB_USER,
|
|
||||||
password=DB_PASSWORD,
|
|
||||||
host=DB_HOST,
|
|
||||||
port="5432",
|
|
||||||
)
|
|
||||||
cursor = connection.cursor()
|
|
||||||
|
|
||||||
query = f"INSERT INTO {table} ("
|
|
||||||
params = []
|
|
||||||
for key,value in kwargs.items():
|
|
||||||
query += f"{key},"
|
|
||||||
params.append(value)
|
|
||||||
query = query.rstrip(",") # Remove trailing comma
|
|
||||||
query += ") VALUES ("
|
|
||||||
for _ in range(len(params)):
|
|
||||||
query += "%s,"
|
|
||||||
query = query.rstrip(",") # Remove trailing comma
|
|
||||||
query += ")"
|
|
||||||
|
|
||||||
query = cursor.mogrify(query, params)
|
|
||||||
cursor.execute(query)
|
|
||||||
connection.commit()
|
|
||||||
cursor.close()
|
|
||||||
connection.close()
|
|
||||||
LOGGER.debug(f'Inserted data into {table}.')
|
|
||||||
|
|
||||||
def update(table:str,where:str,**kwargs):
|
|
||||||
"""
|
|
||||||
Updates the given data in the given table.
|
|
||||||
|
|
||||||
Global_User
|
|
||||||
user_id: int
|
|
||||||
user_mention: str
|
|
||||||
user_mention: str
|
|
||||||
|
|
||||||
Guild
|
|
||||||
guild_id: int
|
|
||||||
|
|
||||||
Birthday_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
|
|
||||||
Productivity_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Birthday
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
birthday: int
|
|
||||||
birthday_message: str | None
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Productivity
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
cooldown: int
|
|
||||||
next_reminder: int
|
|
||||||
reminder_message: str | None
|
|
||||||
"""
|
|
||||||
connection = psycopg2.connect(
|
|
||||||
database=DB_NAME,
|
|
||||||
user=DB_USER,
|
|
||||||
password=DB_PASSWORD,
|
|
||||||
host=DB_HOST,
|
|
||||||
port="5432",
|
|
||||||
)
|
|
||||||
cursor = connection.cursor()
|
|
||||||
|
|
||||||
query = f"UPDATE {table} SET "
|
|
||||||
params = []
|
|
||||||
for key,value in kwargs.items():
|
|
||||||
query += f"{key} = %s,"
|
|
||||||
params.append(value)
|
|
||||||
query = query.rstrip(",") # Remove trailing comma
|
|
||||||
query += f" WHERE {where}"
|
|
||||||
|
|
||||||
query = cursor.mogrify(query, params)
|
|
||||||
cursor.execute(query)
|
|
||||||
connection.commit()
|
|
||||||
cursor.close()
|
|
||||||
connection.close()
|
|
||||||
LOGGER.debug(f'Updated data in {table}.')
|
|
||||||
|
|
||||||
def delete(table:str,where:str):
|
|
||||||
"""
|
|
||||||
Deletes the given data from the given table.
|
|
||||||
|
|
||||||
Global_User
|
|
||||||
user_id: int
|
|
||||||
user_mention: str
|
|
||||||
user_mention: str
|
|
||||||
|
|
||||||
Guild
|
|
||||||
guild_id: int
|
|
||||||
|
|
||||||
Birthday_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
|
|
||||||
Productivity_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Birthday
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
birthday: int
|
|
||||||
birthday_message: str | None
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Productivity
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
cooldown: int
|
|
||||||
next_reminder: int
|
|
||||||
reminder_message: str | None
|
|
||||||
"""
|
|
||||||
connection = psycopg2.connect(
|
|
||||||
database=DB_NAME,
|
|
||||||
user=DB_USER,
|
|
||||||
password=DB_PASSWORD,
|
|
||||||
host=DB_HOST,
|
|
||||||
port="5432",
|
|
||||||
)
|
|
||||||
cursor = connection.cursor()
|
|
||||||
|
|
||||||
query = f"DELETE FROM {table} WHERE {where}"
|
|
||||||
cursor.execute(query)
|
|
||||||
connection.commit()
|
|
||||||
cursor.close()
|
|
||||||
connection.close()
|
|
||||||
LOGGER.debug(f'Deleted data from {table}.')
|
|
||||||
|
|
||||||
def select(table:str,where:str,*columns:str) -> dict[str,Any] | None:
|
|
||||||
"""
|
|
||||||
Selects the given data from the given table, and returns it as a dictionary.
|
|
||||||
|
|
||||||
Global_User
|
|
||||||
user_id: int
|
|
||||||
user_mention: str
|
|
||||||
user_mention: str
|
|
||||||
|
|
||||||
Guild
|
|
||||||
guild_id: int
|
|
||||||
|
|
||||||
Birthday_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
|
|
||||||
Productivity_Settings
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Birthday
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
birthday: int
|
|
||||||
birthday_message: str | None
|
|
||||||
is_enabled: bool
|
|
||||||
|
|
||||||
Guild_User_Productivity
|
|
||||||
user_id: int
|
|
||||||
guild_id: int
|
|
||||||
is_enabled: bool
|
|
||||||
channel_id: int
|
|
||||||
cooldown: int
|
|
||||||
next_reminder: int
|
|
||||||
reminder_message: str | None
|
|
||||||
"""
|
|
||||||
connection = psycopg2.connect(
|
|
||||||
database=DB_NAME,
|
|
||||||
user=DB_USER,
|
|
||||||
password=DB_PASSWORD,
|
|
||||||
host=DB_HOST,
|
|
||||||
port="5432"
|
|
||||||
)
|
|
||||||
connection.readonly = True
|
|
||||||
cursor = connection.cursor()
|
|
||||||
|
|
||||||
query = f"SELECT "
|
|
||||||
if columns:
|
|
||||||
for column in columns:
|
|
||||||
query += f"{column},"
|
|
||||||
query = query.rstrip(",")
|
|
||||||
else:
|
|
||||||
query += "*"
|
|
||||||
query += f" FROM {table} WHERE {where}"
|
|
||||||
cursor.execute(query)
|
|
||||||
record = cursor.fetchone()
|
|
||||||
result = None
|
|
||||||
if record is None:
|
|
||||||
LOGGER.debug(f'No data found in {table} for {where}.')
|
|
||||||
else:
|
|
||||||
result = {}
|
|
||||||
if len(columns) > 0:
|
|
||||||
for i in range(len(columns)):
|
|
||||||
result[columns[i]] = record[i]
|
|
||||||
else:
|
|
||||||
if table.lower() == 'global_user':
|
|
||||||
result['user_id'] = record[0]
|
|
||||||
result['user_name'] = record[1]
|
|
||||||
result['user_mention'] = record[2]
|
|
||||||
elif table.lower() == 'guild':
|
|
||||||
result['guild_id'] = record[0]
|
|
||||||
elif table.lower() == 'birthday_settings':
|
|
||||||
result['guild_id'] = record[0]
|
|
||||||
result['is_enabled'] = record[1]
|
|
||||||
result['channel_id'] = record[2]
|
|
||||||
elif table.lower() == 'productivity_settings':
|
|
||||||
result['guild_id'] = record[0]
|
|
||||||
result['is_enabled'] = record[1]
|
|
||||||
elif table.lower() == 'guild_user_birthday':
|
|
||||||
result['user_id'] = record[0]
|
|
||||||
result['guild_id'] = record[1]
|
|
||||||
result['birthday'] = record[2]
|
|
||||||
result['is_enabled'] = record[3]
|
|
||||||
elif table.lower() == 'guild_user_productivity':
|
|
||||||
result['user_id'] = record[0]
|
|
||||||
result['guild_id'] = record[1]
|
|
||||||
result['is_enabled'] = record[2]
|
|
||||||
result['channel_id'] = record[3]
|
|
||||||
result['cooldown'] = record[4]
|
|
||||||
result['next_reminder'] = record[5]
|
|
||||||
result['reminder_message'] = record[6]
|
|
||||||
else:
|
|
||||||
LOGGER.error(f'Unknown table {table}.')
|
|
||||||
cursor.close()
|
|
||||||
connection.close()
|
|
||||||
return result
|
|
||||||
@@ -1,2 +1,31 @@
|
|||||||
python-dotenv
|
aiohappyeyeballs==2.6.2
|
||||||
py-cord
|
aiohttp==3.14.1
|
||||||
|
aiosignal==1.4.0
|
||||||
|
asarPy==1.0.1
|
||||||
|
attrs==26.1.0
|
||||||
|
beautifulsoup4==4.15.0
|
||||||
|
certifi==2026.5.20
|
||||||
|
charset-normalizer==3.4.7
|
||||||
|
frozenlist==1.8.0
|
||||||
|
h11==0.16.0
|
||||||
|
idna==3.18
|
||||||
|
lxml==6.1.1
|
||||||
|
multidict==6.7.1
|
||||||
|
outcome==1.3.0.post0
|
||||||
|
propcache==0.5.2
|
||||||
|
psycopg2-binary==2.9.12
|
||||||
|
py-cord==2.8.0
|
||||||
|
PySocks==1.7.1
|
||||||
|
python-dotenv==1.2.2
|
||||||
|
requests==2.34.2
|
||||||
|
selenium==4.45.0
|
||||||
|
sniffio==1.3.1
|
||||||
|
sortedcontainers==2.4.0
|
||||||
|
soupsieve==2.8.4
|
||||||
|
trio==0.33.0
|
||||||
|
trio-websocket==0.12.2
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
urllib3==2.7.0
|
||||||
|
websocket-client==1.9.0
|
||||||
|
wsproto==1.3.2
|
||||||
|
yarl==1.24.2
|
||||||
|
|||||||
85
setup/bot.py
Normal file
85
setup/bot.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import discord
|
||||||
|
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 setup.logger import LOGGER
|
||||||
|
|
||||||
|
bot_intents = discord.Intents.default()
|
||||||
|
bot_intents.message_content = True
|
||||||
|
bot_intents.members = True
|
||||||
|
bot_intents.presences = True
|
||||||
|
bot_intents.guilds = True
|
||||||
|
bot_intents.reactions = True
|
||||||
|
|
||||||
|
bot = bridge.Bot(command_prefix='$', intents=bot_intents)
|
||||||
|
|
||||||
|
COG_REGISTRY = {
|
||||||
|
'cs': CSCog,
|
||||||
|
'insult': InsultsCog,
|
||||||
|
'roulette': RouletteCog
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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))
|
||||||
|
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()]
|
||||||
|
|
||||||
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