Implement rules acknowledgment feature in Discord bot

- Added a new module for managing rules acknowledgment, including a persistent button for users to accept the rules.
- Updated the database schema to change the `value` column type in the `Configuration` model to `TEXT` for better handling of longer rule descriptions.
- Enhanced the web application to include configuration options for rules acknowledgment, allowing customization of the rules message and button label.
- Integrated the rules acknowledgment feature into the Discord bot, enabling automatic role assignment upon acceptance and handling of presentation messages for validated roles.
- Updated the configurations template to support new settings related to rules acknowledgment.
This commit is contained in:
2026-05-04 13:17:04 +02:00
parent e72647612d
commit 69d8167581
6 changed files with 353 additions and 5 deletions
+56 -3
View File
@@ -1,10 +1,35 @@
from flask import render_template, request, redirect, url_for
import asyncio
import logging
from flask import render_template, request, redirect, url_for, flash
from webapp import webapp
from webapp.auth import require_page
from database import db
from database.helpers import ConfigurationHelper
from discordbot import bot
RULES_FORM_KEYS = frozenset({
'rules_channel_id',
'rules_arrival_role_id',
'rules_validated_role_id',
'rules_presentation_channel_id',
'rules_embed_title',
'rules_embed_body',
'rules_button_label',
})
SKIP_FORM_KEYS = frozenset({
'moderation_staff_role_ids',
'rules_ack_section_in_form',
'moderation_roles_in_form',
})
def _form_int_str(raw: str | None) -> str:
s = (raw or '').strip()
return s if s.isdigit() else '0'
@webapp.route("/configurations")
@require_page("configurations")
def openConfigurations():
@@ -23,7 +48,8 @@ def updateConfiguration():
'welcome_enable': 'welcome_channel_id',
'leave_enable': 'leave_channel_id',
'auto_rooms_enable': 'auto_rooms_channel_id',
'twitch_commands_enable': 'twitch_channel'
'twitch_commands_enable': 'twitch_channel',
'rules_ack_enable': 'rules_channel_id',
}
# Ne mettre à jour les rôles staff que si la liste a été rendue dans le formulaire.
@@ -35,8 +61,20 @@ def updateConfiguration():
else:
ConfigurationHelper().createOrUpdate('moderation_staff_role_ids', '')
if request.form.get('rules_ack_section_in_form'):
ch = ConfigurationHelper()
ch.createOrUpdate('rules_channel_id', _form_int_str(request.form.get('rules_channel_id')))
ch.createOrUpdate('rules_arrival_role_id', _form_int_str(request.form.get('rules_arrival_role_id')))
ch.createOrUpdate('rules_validated_role_id', _form_int_str(request.form.get('rules_validated_role_id')))
ch.createOrUpdate('rules_presentation_channel_id', _form_int_str(request.form.get('rules_presentation_channel_id')))
ch.createOrUpdate('rules_embed_title', (request.form.get('rules_embed_title') or '').strip())
ch.createOrUpdate('rules_embed_body', request.form.get('rules_embed_body') or '')
ch.createOrUpdate('rules_button_label', (request.form.get('rules_button_label') or '').strip())
for key in request.form:
if key == 'moderation_staff_role_ids':
if key in SKIP_FORM_KEYS:
continue
if request.form.get('rules_ack_section_in_form') and key in RULES_FORM_KEYS:
continue
value = request.form.get(key)
if value and value.strip():
@@ -49,3 +87,18 @@ def updateConfiguration():
db.session.commit()
return redirect(request.referrer)
@webapp.route("/configurations/publish-rules", methods=['POST'])
@require_page("configurations")
def publishRulesMessage():
from discordbot.rules_ack import publish_rules_embed_sync
if not bot.loop or bot.loop.is_closed():
flash("Le bot Discord n'est pas connecté.", "error")
return redirect(url_for("openConfigurations"))
ok, msg = publish_rules_embed_sync(bot)
flash(msg, "success" if ok else "error")
if not ok:
logging.warning("publishRulesMessage: %s", msg)
return redirect(url_for("openConfigurations"))
+92
View File
@@ -73,6 +73,90 @@
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
<h3 class="font-medium text-gray-800 dark:text-gray-200">Règlement (embed + bouton)</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
Publie un message fixe dans un canal avec un embed et un bouton « J'ai lu le règlement ». Au clic, le membre reçoit le rôle d'arrivée. Si un canal « présentation » est configuré, le premier message du membre dans ce canal lui attribue le rôle membre validé et retire le rôle d'arrivée.
</p>
<input type="hidden" name="rules_ack_section_in_form" value="1">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="rules_ack_enable" {% if configuration.getValue('rules_ack_enable') %}checked{% endif %}
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
<span class="text-sm text-gray-700 dark:text-gray-300">Activer le règlement avec bouton</span>
</label>
<div>
<label for="rules_channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal du règlement (message + bouton)</label>
<select name="rules_channel_id" id="rules_channel_id"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
<option value="">— Choisir un canal —</option>
{% for channel in channels %}
<option value="{{ channel.id }}" {% if configuration.getIntValue('rules_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="rules_embed_title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Titre de l'embed</label>
<input name="rules_embed_title" id="rules_embed_title" type="text"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
value="{{ configuration.getValue('rules_embed_title') or '' }}"
placeholder="Bienvenue"/>
</div>
<div>
<label for="rules_embed_body" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Texte du règlement (description de l'embed, markdown Discord)</label>
<textarea name="rules_embed_body" id="rules_embed_body" rows="8"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all resize-y"
placeholder="Lis le règlement puis clique sur le bouton ci-dessous…">{{ configuration.getValue('rules_embed_body') or '' }}</textarea>
</div>
<div>
<label for="rules_button_label" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Libellé du bouton</label>
<input name="rules_button_label" id="rules_button_label" type="text"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
value="{{ configuration.getValue('rules_button_label') or '' }}"
placeholder="J'ai lu le règlement"/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="rules_arrival_role_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rôle d'arrivée (au clic sur le bouton)</label>
<select name="rules_arrival_role_id" id="rules_arrival_role_id"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
<option value="">— Aucun —</option>
{% for guild_data in roles %}
<optgroup label="{{ guild_data.guild_name }}">
{% for role in guild_data.roles %}
<option value="{{ role.id }}" {% if configuration.getIntValue('rules_arrival_role_id') == role.id %}selected{% endif %}>{{ role.name }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</div>
<div>
<label for="rules_validated_role_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rôle membre validé (accès au reste du serveur)</label>
<select name="rules_validated_role_id" id="rules_validated_role_id"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
<option value="">— Aucun —</option>
{% for guild_data in roles %}
<optgroup label="{{ guild_data.guild_name }}">
{% for role in guild_data.roles %}
<option value="{{ role.id }}" {% if configuration.getIntValue('rules_validated_role_id') == role.id %}selected{% endif %}>{{ role.name }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</div>
</div>
<div>
<label for="rules_presentation_channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal présentation (optionnel)</label>
<select name="rules_presentation_channel_id" id="rules_presentation_channel_id"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
<option value="">— Désactivé —</option>
{% for channel in channels %}
<option value="{{ channel.id }}" {% if configuration.getIntValue('rules_presentation_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
{% endfor %}
</select>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Si renseigné : premier message du membre (qui a le rôle d'arrivée) dans ce canal → ajout du rôle validé et retrait du rôle d'arrivée.</p>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
<h3 class="font-medium text-gray-800 dark:text-gray-200">Messages de départ</h3>
@@ -212,6 +296,14 @@
Enregistrer la configuration Discord
</button>
</form>
<form action="{{ url_for('publishRulesMessage') }}" method="POST" class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">Envoie ou remplace le message du règlement sur Discord (utilise la config <strong>enregistrée</strong> ci-dessus).</p>
<button type="submit"
class="px-6 py-2.5 bg-teal-600 hover:bg-teal-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
Publier le message règlement sur Discord
</button>
</form>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">