feat: initial project setup — Vision Studio bot
Some checks failed
CI / lint (push) Failing after 2s
CI / test (push) Failing after 1s

- Python 3.12 + discord.py
- Docker + docker-compose
- Forgejo Actions CI (lint + test)
- Cog system with /hello example
- deploy.sh for auto-deployment
This commit is contained in:
Mathis 2026-09-19 12:11:06 +02:00
commit e44349cea9
17 changed files with 396 additions and 0 deletions

16
.dockerignore Normal file
View file

@ -0,0 +1,16 @@
.git
.github
__pycache__
*.pyc
*.pyo
venv/
.venv/
.pytest_cache/
tests/
.mypy_cache/
nohup.out
*.log
data/
logs/
.gitignore
.env

6
.env.example Normal file
View file

@ -0,0 +1,6 @@
DISCORD_TOKEN=your_bot_token_here
APPLICATION_ID=your_application_id_here
DEV=1
FORGEJO_URL=https://your-forgejo-instance.com
FORGEJO_OWNER=your_forgejo_owner
FORGEJO_REPO=your_repo_name

46
.forgejo/workflows/ci.yml Normal file
View file

@ -0,0 +1,46 @@
name: CI
on:
push:
branches: [main, dev]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install -r requirements.txt -r requirements-dev.txt
- name: Ruff lint
run: ruff check .
- name: Ruff format check
run: ruff format --check .
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
pip install -r requirements.txt -r requirements-dev.txt
- name: Syntax check
run: python -m py_compile bot.py && python -m py_compile vision.py && python -m py_compile config.py
- name: Run tests
run: pytest tests/ -v || true

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# --- SÉCURITÉ ---
.env
# --- PYTHON ---
__pycache__/
*.py[cod]
*$py.class
venv/
.venv/
env/
# --- DONNÉES ---
data/*
!data/.gitkeep
*.db
*.db-journal
*.db-wal
*.db-shm
# --- LOGS ---
*.log
logs/
# --- IDE ---
.vscode/
.idea/
.DS_Store
# --- DOCKER ---
docker-compose.override.yml

23
Dockerfile Normal file
View file

@ -0,0 +1,23 @@
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import discord; print('ok')" || exit 1
CMD ["python", "vision.py"]

37
bot.py Normal file
View file

@ -0,0 +1,37 @@
import logging
import discord
from discord.ext import commands
import config
logger = logging.getLogger("vision")
class VisionBot(commands.Bot):
def __init__(self):
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
super().__init__(
command_prefix="!",
intents=intents,
application_id=config.APPLICATION_ID,
)
async def setup_hook(self):
logger.info("Chargement des cogs...")
await self.load_extension("commandes.hello")
logger.info("Sync des slash commands...")
await self.tree.sync()
logger.info("Slash commands synchronisées")
async def on_ready(self):
logger.info(f"Connecté en tant que {self.user} (ID: {self.user.id})")
logger.info(f"Serveurs : {len(self.guilds)}")
await self.change_presence(
activity=discord.Activity(
type=discord.ActivityType.watching,
name="Vision Studio",
)
)

0
commandes/__init__.py Normal file
View file

22
commandes/hello.py Normal file
View file

@ -0,0 +1,22 @@
import discord
from discord import app_commands
from discord.ext import commands
class Hello(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@app_commands.command(name="hello", description="Salut de la part de Vision Studio !")
async def hello(self, interaction: discord.Interaction):
embed = discord.Embed(
title="Vision Studio",
description="Salut ! Je suis **Vision Studio**, ton bot Discord.\n\nUtilise `/help` pour voir les commandes disponibles.",
color=discord.Color.blue(),
)
embed.set_footer(text="Vision Studio v1.0")
await interaction.response.send_message(embed=embed)
async def setup(bot: commands.Bot):
await bot.add_cog(Hello(bot))

21
config.py Normal file
View file

@ -0,0 +1,21 @@
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
LOGS_DIR = BASE_DIR / "logs"
DATA_DIR.mkdir(exist_ok=True)
LOGS_DIR.mkdir(exist_ok=True)
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
APPLICATION_ID = os.getenv("APPLICATION_ID", "")
DEV = os.getenv("DEV", "").strip() in ("1", "true", "yes")
FORGEJO_URL = os.getenv("FORGEJO_URL", "")
FORGEJO_TOKEN = os.getenv("FORGEJO_TOKEN", "")
FORGEJO_OWNER = os.getenv("FORGEJO_OWNER", "")
FORGEJO_REPO = os.getenv("FORGEJO_REPO", "")

0
data/.gitkeep Normal file
View file

33
docker-compose.yml Normal file
View file

@ -0,0 +1,33 @@
services:
vision:
build:
context: .
dockerfile: Dockerfile
container_name: vision-studio
restart: unless-stopped
env_file: .env
environment:
- VISION_INSTANCE=prod
dns:
- 1.1.1.1
- 8.8.8.8
volumes:
- vision-data:/app/data
- vision-logs:/app/logs
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
reservations:
memory: 128M
cpus: "0.25"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
vision-data:
vision-logs:

19
pyproject.toml Normal file
View file

@ -0,0 +1,19 @@
[project]
name = "vision-studio"
version = "1.0.0"
description = "Vision Studio — Bot Discord"
requires-python = ">=3.12"
[tool.ruff]
target-version = "py312"
line-length = 120
[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "SIM"]
ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ALL"]
[tool.ruff.format]
quote-style = "double"

3
requirements-dev.txt Normal file
View file

@ -0,0 +1,3 @@
ruff>=0.5.0
pytest>=8.0.0
pytest-asyncio>=0.24.0

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
discord.py>=2.4.0
python-dotenv>=1.0.0
Pillow>=10.0.0
aiohttp>=3.9.0

95
scripts/deploy.sh Executable file
View file

@ -0,0 +1,95 @@
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# Vision Studio — Script de deploiement automatique
#
# Flow :
# 1. Lock anti-concurrence
# 2. git pull origin <branch> --rebase
# 3. Si requirements.txt change -> docker compose build
# 4. Kill process Python -> Docker restart (restart: unless-stopped)
#
# Usage: deploy.sh [branch]
# ══════════════════════════════════════════════════════════════════════════════
set -uo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BRANCH="${1:-main}"
LOCK_FILE="/tmp/vision_deploy.lock"
DEPLOY_LOG="${ROOT_DIR}/data/deploy.log"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
. "$ROOT_DIR/.env"
set +a
fi
COMPOSE_DIR="${ROOT_DIR}"
log() { echo "[deploy] $(date '+%F %T') $*"; }
acquire_lock() {
if [ -f "$LOCK_FILE" ]; then
local lock_pid
lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
log "ERROR: Deploy déjà en cours (PID $lock_pid)"
exit 1
fi
log "WARN: Lock orphelin, nettoyage"
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
}
release_lock() { rm -f "$LOCK_FILE"; }
trap release_lock EXIT
cd "$ROOT_DIR"
acquire_lock
DEPLOY_START=$(date +%s)
COMMIT_BEFORE=$(git log -1 --format="%h %s" 2>/dev/null || echo "unknown")
log "Deploy démarré — branch=$BRANCH — commit=$COMMIT_BEFORE"
log "Git pull origin $BRANCH --rebase..."
if ! git pull origin "$BRANCH" --rebase 2>&1; then
log "ERROR: git pull failed"
exit 1
fi
COMMIT_AFTER=$(git log -1 --format="%h %s")
CHANGED_FILES=$(git diff --name-only HEAD~1 2>/dev/null || echo "")
REBUILD_NEEDED=false
if echo "$CHANGED_FILES" | grep -q "requirements\.txt"; then
REBUILD_NEEDED=true
log "requirements.txt modifié → rebuild nécessaire"
elif echo "$CHANGED_FILES" | grep -q "Dockerfile"; then
REBUILD_NEEDED=true
log "Dockerfile modifié → rebuild nécessaire"
fi
if [ "$REBUILD_NEEDED" = true ] && command -v docker &>/dev/null; then
cd "$COMPOSE_DIR"
docker compose build --no-cache vision 2>&1 | tail -10
BUILD_EXIT=${PIPESTATUS[0]:-$?}
if [ "$BUILD_EXIT" -ne 0 ]; then
log "ERROR: Docker build échoué (exit $BUILD_EXIT)"
exit 1
fi
docker compose up -d vision 2>&1 | tail -5
DEPLOY_END=$(date +%s)
DURATION=$((DEPLOY_END - DEPLOY_START))
log "Deploy réussi en ${DURATION}s (deps rebuild)"
exit 0
fi
DEPLOY_END=$(date +%s)
DURATION=$((DEPLOY_END - DEPLOY_START))
log "Redémarrage du bot..."
mkdir -p "$(dirname "$DEPLOY_LOG")"
echo "$(date '+%F %T') | $BRANCH | $COMMIT_AFTER | ${DURATION}s | auto" >> "$DEPLOY_LOG"
pkill -f "python.*vision\.py" 2>/dev/null || kill 1 2>/dev/null || true

0
tests/__init__.py Normal file
View file

41
vision.py Normal file
View file

@ -0,0 +1,41 @@
import asyncio
import logging
import sys
from dotenv import load_dotenv
load_dotenv()
import config
from bot import VisionBot
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-8s | %(name)s — %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler(config.LOGS_DIR / "vision.log", encoding="utf-8"),
],
)
logger = logging.getLogger("vision")
async def main():
if not config.DISCORD_TOKEN:
logger.critical("DISCORD_TOKEN manquant dans .env")
sys.exit(1)
logger.info("Lancement de Vision Studio...")
bot = VisionBot()
try:
await bot.start(config.DISCORD_TOKEN)
except KeyboardInterrupt:
logger.info("Arrêt demandé")
except Exception as e:
logger.critical(f"Erreur fatale : {e}", exc_info=True)
if __name__ == "__main__":
asyncio.run(main())