Ajout de l'application discord_integration avec modèles, migrations, logique d'annonces et gestion des rôles dans Discord.
This commit is contained in:
parent
1354568495
commit
2ec4a5c065
18 changed files with 239 additions and 6 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
class Post(models.Model):
|
class Post(models.Model):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
|
|
@ -16,3 +17,6 @@ class Post(models.Model):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('blog:post_detail', kwargs={'slug': self.slug})
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class CoursesConfig(AppConfig):
|
class CoursesConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'courses'
|
name = 'courses'
|
||||||
|
|
@ -55,6 +55,8 @@ INSTALLED_APPS = [
|
||||||
'users',
|
'users',
|
||||||
'progression',
|
'progression',
|
||||||
'blog',
|
'blog',
|
||||||
|
|
||||||
|
'discord_integration.apps.DiscordIntegrationConfig',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|
|
||||||
0
discord_integration/__init__.py
Normal file
0
discord_integration/__init__.py
Normal file
3
discord_integration/admin.py
Normal file
3
discord_integration/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
8
discord_integration/apps.py
Normal file
8
discord_integration/apps.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordIntegrationConfig(AppConfig):
|
||||||
|
name = 'discord_integration'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import discord_integration.signals
|
||||||
0
discord_integration/core/__init__.py
Normal file
0
discord_integration/core/__init__.py
Normal file
58
discord_integration/core/announces_logic.py
Normal file
58
discord_integration/core/announces_logic.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
from asgiref.sync import sync_to_async
|
||||||
|
import discord
|
||||||
|
from discord.ext import tasks
|
||||||
|
from discord_integration.models import DiscordNotification
|
||||||
|
|
||||||
|
def get_pending_notifications():
|
||||||
|
return list(DiscordNotification.objects.filter(is_announced=False))
|
||||||
|
|
||||||
|
def mark_as_done(notif):
|
||||||
|
notif.is_announced = True
|
||||||
|
notif.save()
|
||||||
|
|
||||||
|
def process_notifications():
|
||||||
|
# Cette fonction fait tout le travail SQL "interdit" en mode async
|
||||||
|
notifs = list(DiscordNotification.objects.filter(is_announced=False))
|
||||||
|
results = []
|
||||||
|
for n in notifs:
|
||||||
|
# Ici, on peut toucher à content_object car on est en mode "sync" !
|
||||||
|
obj = n.content_object
|
||||||
|
if obj:
|
||||||
|
# 1. On cherche la description, sinon le contenu, sinon rien
|
||||||
|
teaser = getattr(obj, 'description', getattr(obj, 'content', ""))
|
||||||
|
|
||||||
|
# 2. Sécurité : On coupe à 3000 caractères pour éviter les erreurs Discord
|
||||||
|
if len(teaser) > 3000:
|
||||||
|
teaser = teaser[:2997] + "..."
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'notif_model': n,
|
||||||
|
'title': getattr(obj, 'title', getattr(obj, 'name', str(obj))),
|
||||||
|
'url': obj.get_absolute_url() if hasattr(obj, 'get_absolute_url') else "#",
|
||||||
|
'summary': teaser
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@tasks.loop(seconds=5.0)
|
||||||
|
async def check_announcements(client, channel_id):
|
||||||
|
print("Checking announcements...")
|
||||||
|
announcements = await sync_to_async(process_notifications)()
|
||||||
|
|
||||||
|
for data in announcements:
|
||||||
|
title = data['title']
|
||||||
|
link = data['url']
|
||||||
|
notif = data['notif_model']
|
||||||
|
summary = data['summary']
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title = f"📣 Nouveau contenu : {title}",
|
||||||
|
url = f"https://partirdezero.com{link}",
|
||||||
|
description = summary,
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_id = client.get_channel(channel_id)
|
||||||
|
if channel_id:
|
||||||
|
await channel_id.send(embed=embed)
|
||||||
|
await sync_to_async(mark_as_done)(notif)
|
||||||
49
discord_integration/core/main_bot.py
Normal file
49
discord_integration/core/main_bot.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import discord
|
||||||
|
import django
|
||||||
|
import os, sys
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
# On import django pour communiquer avec
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
if BASE_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, BASE_DIR)
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'devart.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
# Import des fonctions
|
||||||
|
from role_logic import check_role_reaction
|
||||||
|
from announces_logic import check_announcements
|
||||||
|
|
||||||
|
# CONFIGURATION
|
||||||
|
TOKEN = 'MTQ1MDkyNzQ5NzQ3Nzc1MDg1NA.GmkYxN.YHWXYUIav51yriV_9EotmtUO-cQqdVFLkkb6Do'
|
||||||
|
MESSAGE_ID = 1450928822156263505 # L'ID du message des règles (clic droit > Copier l'identifiant)
|
||||||
|
ROLE_ID = 1450920002868875435 # L'ID du rôle "Membres"
|
||||||
|
ANNOUNCEMENT_CHANNEL_ID = 1450912559774306346
|
||||||
|
EMOJI_VALIDATION = "✅"
|
||||||
|
|
||||||
|
# LES INTENTS (PERMISSIONS DU BOT)
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.members = True # Important pour pouvoir donner des rôles
|
||||||
|
intents.message_content = True
|
||||||
|
|
||||||
|
client = discord.Client(intents=intents)
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_ready():
|
||||||
|
print(f'✅ Bot connecté : {client.user}')
|
||||||
|
|
||||||
|
if not check_announcements.is_running():
|
||||||
|
check_announcements.start(client, ANNOUNCEMENT_CHANNEL_ID)
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_raw_reaction_add(payload):
|
||||||
|
# On envoie tout le nécessaire à notre fonction dans role_logic.py
|
||||||
|
await check_role_reaction(
|
||||||
|
payload,
|
||||||
|
client,
|
||||||
|
MESSAGE_ID,
|
||||||
|
ROLE_ID,
|
||||||
|
EMOJI_VALIDATION
|
||||||
|
)
|
||||||
|
|
||||||
|
client.run(TOKEN)
|
||||||
39
discord_integration/core/role_logic.py
Normal file
39
discord_integration/core/role_logic.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import discord
|
||||||
|
|
||||||
|
|
||||||
|
async def check_role_reaction(payload, client, target_message_id, target_role_id, target_emoji):
|
||||||
|
# 1. On vérifie si c'est le bon message
|
||||||
|
if payload.message_id != target_message_id:
|
||||||
|
return # On ignore si ce n'est pas le bon message
|
||||||
|
|
||||||
|
# 2. On vérifie si c'est le bon emoji
|
||||||
|
if str(payload.emoji) == target_emoji:
|
||||||
|
guild = client.get_guild(payload.guild_id)
|
||||||
|
if guild is None:
|
||||||
|
print("Erreur: Impossible de trouver le serveur (Guild is None).")
|
||||||
|
return
|
||||||
|
|
||||||
|
member = guild.get_member(payload.user_id)
|
||||||
|
if member is None:
|
||||||
|
print("Erreur: Impossible de trouver le membre (Member is None).")
|
||||||
|
return
|
||||||
|
|
||||||
|
role = guild.get_role(target_role_id)
|
||||||
|
if role is None:
|
||||||
|
print("Erreur : Le role n'existe pas.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await member.add_roles(role)
|
||||||
|
print(f"🎉 SUCCÈS : Rôle donné à {member.name} !")
|
||||||
|
try:
|
||||||
|
await member.send("Bienvenue ! Tu as accès aux salons.")
|
||||||
|
except:
|
||||||
|
print("Note: MP bloqués par l'utilisateur.")
|
||||||
|
except discord.Forbidden:
|
||||||
|
print("⛔ ERREUR PERMISSION : Je n'ai pas le droit de donner ce rôle !")
|
||||||
|
print(
|
||||||
|
"👉 SOLUTION : Va dans Paramètres Serveur > Rôles. Glisse le rôle 'PartirDeZero Bot' AU-DESSUS du rôle 'Membres'.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Erreur inconnue : {e}")
|
||||||
26
discord_integration/migrations/0001_initial.py
Normal file
26
discord_integration/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Generated by Django 6.0 on 2025-12-18 08:23
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DiscordNotification',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('is_announced', models.BooleanField(default=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
discord_integration/migrations/__init__.py
Normal file
0
discord_integration/migrations/__init__.py
Normal file
13
discord_integration/models.py
Normal file
13
discord_integration/models.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
class DiscordNotification(models.Model):
|
||||||
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = GenericForeignKey('content_type', 'object_id')
|
||||||
|
is_announced = models.BooleanField(default=False)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Annonces pour {self.content_object} ({'✅' if self.is_announced else '⏳'})"
|
||||||
26
discord_integration/signals.py
Normal file
26
discord_integration/signals.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from .models import DiscordNotification
|
||||||
|
from blog.models import Post
|
||||||
|
from courses.models import Lesson, Course
|
||||||
|
|
||||||
|
@receiver(post_save, sender="blog.Post")
|
||||||
|
def create_discord_notification_blog(sender, instance, created, **kwargs):
|
||||||
|
print(f"DEBUG : Signal capté ! Nouveau objet : {instance}")
|
||||||
|
if created:
|
||||||
|
DiscordNotification.objects.create(content_object=instance)
|
||||||
|
print("DEBUG : Notification enregistée en base de données")
|
||||||
|
|
||||||
|
@receiver(post_save, sender="courses.Course")
|
||||||
|
def create_discord_notification_course(sender, instance, created, **kwargs):
|
||||||
|
print(f"DEBUG : Signal capté ! Nouveau objet : {instance}")
|
||||||
|
if created:
|
||||||
|
DiscordNotification.objects.create(content_object=instance)
|
||||||
|
print("DEBUG : Notification enregistée en base de données")
|
||||||
|
|
||||||
|
@receiver(post_save, sender="courses.Lesson")
|
||||||
|
def create_discord_notification_lesson(sender, instance, created, **kwargs):
|
||||||
|
print(f"DEBUG : Signal capté ! Nouveau objet : {instance}")
|
||||||
|
if created:
|
||||||
|
DiscordNotification.objects.create(content_object=instance)
|
||||||
|
print("DEBUG : Notification enregistée en base de données")
|
||||||
3
discord_integration/tests.py
Normal file
3
discord_integration/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
discord_integration/views.py
Normal file
3
discord_integration/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
{% if settings.contact_email %}
|
{% if settings.contact_email %}
|
||||||
<li><a href="mailto:contact@exemple.com"><i class="fa-solid fa-envelope"></i> Email</a></li>
|
<li><a href="mailto:contact@exemple.com"><i class="fa-solid fa-envelope"></i> Email</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li><a href="https://discord.gg/bAkuKHWFqU" target="_blank"><i class="fab fa-discord"></i> Communauté</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,10 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li><a href="https://discord.gg/bAkuKHWFqU" target="_blank" class="button"><i class="fab fa-discord"></i> Discord</a></li>
|
||||||
{% if user.is_authenticated and user.is_staff %}
|
{% if user.is_authenticated and user.is_staff %}
|
||||||
<li>
|
<li><a href="{% url 'admin:index' %}" class="button button-danger" title="Administration">Admin</a></li>
|
||||||
<a href="{% url 'admin:index' %}" class="button button-danger" title="Administration">Admin</a>
|
<li><a href="{% url 'home:stats_dashboard' %}" class="button button-warning" title="Stats">Stats</a></li>
|
||||||
<a href="{% url 'home:stats_dashboard' %}" class="button button-warning" title="Stats">Stats</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue