diff --git a/blog/models.py b/blog/models.py index 387d847..5d0d68c 100644 --- a/blog/models.py +++ b/blog/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.urls import reverse class Post(models.Model): name = models.CharField(max_length=200) @@ -15,4 +16,7 @@ class Post(models.Model): verbose_name_plural = "Articles" def __str__(self): - return self.name \ No newline at end of file + return self.name + + def get_absolute_url(self): + return reverse('blog:post_detail', kwargs={'slug': self.slug}) \ No newline at end of file diff --git a/courses/apps.py b/courses/apps.py index 89f1ba2..9a0a0cd 100644 --- a/courses/apps.py +++ b/courses/apps.py @@ -1,6 +1,5 @@ from django.apps import AppConfig - class CoursesConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'courses' \ No newline at end of file diff --git a/devart/settings.py b/devart/settings.py index db43f75..f2dab3b 100644 --- a/devart/settings.py +++ b/devart/settings.py @@ -55,6 +55,8 @@ INSTALLED_APPS = [ 'users', 'progression', 'blog', + + 'discord_integration.apps.DiscordIntegrationConfig', ] MIDDLEWARE = [ diff --git a/discord_integration/__init__.py b/discord_integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/discord_integration/admin.py b/discord_integration/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/discord_integration/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/discord_integration/apps.py b/discord_integration/apps.py new file mode 100644 index 0000000..56fe46c --- /dev/null +++ b/discord_integration/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class DiscordIntegrationConfig(AppConfig): + name = 'discord_integration' + + def ready(self): + import discord_integration.signals diff --git a/discord_integration/core/__init__.py b/discord_integration/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/discord_integration/core/announces_logic.py b/discord_integration/core/announces_logic.py new file mode 100644 index 0000000..03e8939 --- /dev/null +++ b/discord_integration/core/announces_logic.py @@ -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) diff --git a/discord_integration/core/main_bot.py b/discord_integration/core/main_bot.py new file mode 100644 index 0000000..aae766e --- /dev/null +++ b/discord_integration/core/main_bot.py @@ -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) diff --git a/discord_integration/core/role_logic.py b/discord_integration/core/role_logic.py new file mode 100644 index 0000000..8dd2dbc --- /dev/null +++ b/discord_integration/core/role_logic.py @@ -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}") \ No newline at end of file diff --git a/discord_integration/migrations/0001_initial.py b/discord_integration/migrations/0001_initial.py new file mode 100644 index 0000000..6c269a9 --- /dev/null +++ b/discord_integration/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/discord_integration/migrations/__init__.py b/discord_integration/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/discord_integration/models.py b/discord_integration/models.py new file mode 100644 index 0000000..9906f9b --- /dev/null +++ b/discord_integration/models.py @@ -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 '⏳'})" \ No newline at end of file diff --git a/discord_integration/signals.py b/discord_integration/signals.py new file mode 100644 index 0000000..c69abe4 --- /dev/null +++ b/discord_integration/signals.py @@ -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") \ No newline at end of file diff --git a/discord_integration/tests.py b/discord_integration/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/discord_integration/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/discord_integration/views.py b/discord_integration/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/discord_integration/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/templates/partials/_footer.html b/templates/partials/_footer.html index 934a5e3..42cc419 100644 --- a/templates/partials/_footer.html +++ b/templates/partials/_footer.html @@ -29,6 +29,7 @@ {% if settings.contact_email %}