Skip to content

Commit

Permalink
API integration for calidum-rotae requests (#46)
Browse files Browse the repository at this point in the history
closes #45 

Summary:
This pull request introduces several changes to the existing Discord bot
and its API integration to allow for handling webhooks request coming
from calidum rotae.

- API Bot Endpoint: The Quart API endpoint /webhooks/<uuid> 
  - payload validation and parsing.
  - relay calidum-rotae http requests to discord

- Bot Updates
- New Webhook Commands: Added new Slash command group named webhook
which includes the commands create, list, delete, and update.
- Unique Webhook URL Generation: Implemented a utility function
generate_unique_webhook_url() for generating unique URLs for each
webhook.
- Database Integration: Connected the bot with the database to store and
fetch webhook data.
  • Loading branch information
SonOfLope authored Sep 10, 2023
1 parent 069ea0c commit ff96489
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 15 deletions.
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Un robot logiciel pour les serveurs Discord des clubs étudiants de l'ÉTS
Trëma est un bot Discord développé par le club CEDILLE de l'ÉTS. Le but de ce bot est d'aider à accueillir et à guider les nouveaux membres sur le serveur Discord, ainsi que de fournir des fonctionnalités utiles pour la gestion du serveur.

## Fonctionnalités
### Discord bot
__Commandes de Configuration (/config)__
* /config aide: Fournit des informations sur les différentes commandes de configuration.

Expand All @@ -33,6 +34,12 @@ __Sous-commandes de Rappel (/config rappel)__

* /config rappel delai [délai]: Définit le délai en minutes avant que le message de rappel soit envoyé. Rôle admin requis.

__Commandes de Webhook (/webhook)__
* /webhook create [channel_id] [webhook_name]: Crée un nouveau webhook pour le canal spécifié. Rôle admin requis.
* /webhook list: Affiche une liste des webhooks existants pour le serveur. Rôle admin requis.
* /webhook delete [webhook_name]: Supprime un webhook existant. Rôle admin requis.
* /webhook update [webhook_name] [new_channel_id]: Met à jour le canal associé au webhook. Rôle admin requis.

__Commandes d'Information__
* /ping: Affiche la latence du bot et d'autres statistiques.

Expand All @@ -41,19 +48,41 @@ __Commandes d'Information__
__Commandes annonce__
* /annonce: Permet de planifier une annonce.

### Événements
#### Événements
* on_guild_join: Enregistre le serveur dans la base de données lorsqu'il rejoint un nouveau serveur.

* on_member_join: Envoie un message d'accueil personnalisé dans le canal d'accueil configuré et envoie également des rappels aux membres qui n'ont pas encore choisi de rôle.

Les placeholders comme {member}, {username}, {server}, {&role}, {#channel}, {everyone}, {here} etc. peuvent être utilisés pour personnaliser les messages.

### Comment l'utiliser ?
#### Comment l'utiliser ?
Pour commencer à utiliser le bot, [invitez-le](https://discord.com/api/oauth2/authorize?client_id=1042263080794603630&permissions=28582739967217&scope=bot) sur votre serveur et utilisez la commande /config aide pour obtenir de l'aide sur la configuration.

#### Besoin d'aide ?
##### Besoin d'aide ?
Si vous avez des questions ou des problèmes avec le bot, n'hésitez pas à contacter le club CEDILLE à partir de [discord](https://discord.gg/ywvNV4Se) pour un support technique.


### API Endpoints
__Webhooks__
* POST /webhooks/<uuid>: Cet endpoint est utilisé pour gérer les webhooks entrants.
Lorsqu'une requête POST est effectuée sur cet endpoint avec un uuid valide, il traite le contenu intégré pour créer un message Discord Embed et l'envoie au canal Discord correspondant. Le payload JSON entrant doit inclure un tableau embeds contenant les informations d'intégration, telles que le titre, la description, la couleur, et un pied de page facultatif.

Exemple de Payload :
```
{
"embeds": [
{
"title": "Your Title Here",
"description": "Your Description Here",
"color": "16745728",
"footer": {
"text": "Footer Text"
}
}
]
}
```

## Développement local
### Lancement de Trëma

Expand All @@ -63,6 +92,8 @@ Définissez la variable d'environnement suivante :
- MONGO_PASSWORD : mot de passe de la db
- MONGO_HOST : nom d'hôte de la db
- MONGO_PORT : Port pour la db
- API_ADDRESS : Addresse ip de l'api
- API_PORT : port utilisé par l'api

Installer les dépendances en ligne de commande.

Expand Down
2 changes: 2 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ services:
- .env
networks:
- app_network
ports:
- 6000:6000

networks:
app_network:
Expand Down
9 changes: 5 additions & 4 deletions events.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import asyncio

from discord_util import\
member_roles_are_default,\
send_delayed_dm
Expand Down Expand Up @@ -37,9 +35,9 @@ async def on_member_join(member):
reminder_msg = make_mention(reminder_msg, generate_mention_dict(guild, member))
reminder_delay = trema_db.get_server_reminder_delay(guild_id)
msg_condition = lambda: member_roles_are_default(member)
reminder_task = asyncio.create_task(send_delayed_dm(
reminder_task = loop.create_task(send_delayed_dm(
member, reminder_msg, reminder_delay, msg_condition))
await asyncio.wait([reminder_task])
await loop.wait([reminder_task])

@trema_bot.event
async def on_member_remove(member):
Expand All @@ -51,6 +49,9 @@ async def on_member_remove(member):
leave_msg = make_mention(leave_msg, member)
await welcome_chan.send(leave_msg)




@trema_bot.event
async def on_ready():
print(f"{trema_bot.user} fonctionne.")
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ jsonschema==4.7.2
pymongo==4.1.1
pytest~=7.2.0
discord~=2.1.0
pytz~=2023.3
pytz~=2023.3
quart~=0.18.4
1 change: 1 addition & 0 deletions routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .routes import create_routes
38 changes: 38 additions & 0 deletions routes/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from discord import\
Embed
import re
from quart import request, jsonify

def create_routes(app, database, trema):
create_routes_webhooks(app, database, trema)

def create_routes_webhooks(app, database, trema):
@app.route('/webhooks/<uuid>', methods=['POST'])
async def handle_webhook(uuid):
channelID = database.get_channel_by_webhook(uuid)
if channelID is None:
return jsonify({'error': 'Invalid webhook UUID'}), 400

incoming_data = await request.json
embed_data = incoming_data.get('embeds', [])[0]

embed = Embed(
title=embed_data.get('title', 'N/A'),
color=int(embed_data.get('color', '0'))
)

description = embed_data.get('description', 'N/A')

fields = re.findall(r"\*\*(.+?):\*\* (.+?)(?=\n|$)", description)
for name, value in fields:
embed.add_field(name=name, value=value, inline=False if "Details" in name else True)

footer_data = embed_data.get('footer', {})
footer_text = footer_data.get('text', '')
if footer_text:
embed.set_footer(text=footer_text)

channel = trema.get_channel(int(channelID))
await channel.send(embed=embed)

return jsonify({'status': 'success'}), 200
1 change: 1 addition & 0 deletions schemas/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class Schema(Enum):
# The values must match the schemas' file name.
SERVER = "server"
WELCOME = "welcome"
webhooks = "webhooks"

@staticmethod
def from_str(a_string):
Expand Down
8 changes: 8 additions & 0 deletions schemas/webhooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "object",
"properties": {
"webhookName": {"type": "string"},
"channelID": {"type": "string"},
"unique_url": {"type": "string"}
}
}
81 changes: 80 additions & 1 deletion slash_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@
make_mention,\
generate_mention_dict

from uuid import uuid4

_MEMBER_MENTIONABLE = "[@-]"
_REQUEST_VALUE = "$"
_SPACE = " "

def generate_unique_webhook_url():
return str(uuid4())

def is_authorized(trema_db):
async def predicate(ctx):
admin_role_id = trema_db.get_server_admin_role(ctx.guild.id)
Expand All @@ -61,7 +66,9 @@ def create_slash_cmds(trema_bot, trema_db, start_time):
_create_config_reminder_cmds(trema_db, config)
_create_information_cmds(trema_bot, start_time)
_create_management_cmds(trema_bot, trema_db)
webhook = _create_webhooks_cmds(trema_db)
trema_bot.add_application_command(config)
trema_bot.add_application_command(webhook)

def _create_config_cmds(trema_db):
config = SlashCommandGroup(name="config",
Expand Down Expand Up @@ -449,4 +456,76 @@ async def annonce(ctx):
asyncio.create_task(send_delayed_dm(channel, file, delay, condition, 'file', image_path))
await ctx.author.send('Annonce programmée.')
else:
await ctx.author.send('Annonce annulée.')
await ctx.author.send('Annonce annulée.')

def _create_webhooks_cmds(trema_db):
webhook = SlashCommandGroup(name="webhook",
description="Groupe de commandes pour gérer les webhooks.")

@webhook.command(name="create",
description="Créer un webhook pour le canal spécifié")
@is_authorized(trema_db)
async def create_webhook(ctx,
channel_id: Option(str, "Le canal affilié au webhook qui sera créé"),
webhook_name: Option(str, "Le nom du webhook qui sera créé")):

guild_id = ctx.guild_id
unique_url = generate_unique_webhook_url()

# Save the unique URL and associated channel to your database
trema_db.create_webhook(webhook_name, channel_id, unique_url, guild_id)

embed=Embed(title="Webhook créé", description=f"Le webhook '{webhook_name}' a été créé avec succès.\n```webhook url endpoint : {unique_url}```", color=Color.blurple())
await ctx.respond(embed=embed, ephemeral=True)

@webhook.command(name="list",
description="Liste des webhooks existants")
@is_authorized(trema_db)
async def list_webhooks(ctx):
webhooks = trema_db.get_all_webhooks(ctx.guild.id)
if webhooks == []:
embed=Embed(title="Liste des webhooks", description=f"Aucun webhook n'existe pour ce serveur.", color=Color.blurple())
await ctx.respond(embed=embed, ephemeral=True)
return

webhook_strings = []
for webhook in webhooks:
webhook_str = f"Name: {webhook.get('webhookName', 'Unknown')}, Channel ID: {webhook.get('channelID', 'Unknown')}"
webhook_strings.append(webhook_str)

embed=Embed(title="Liste des webhooks", description=f"Voici la liste des webhooks existants :\n" + f'\n'.join(webhook_strings), color=Color.blurple())
await ctx.respond(embed=embed, ephemeral=True)

@webhook.command(name="delete",
description="Supprime le webhook référencé")
@is_authorized(trema_db)
async def delete_webhook(ctx, webhook_name: Option(str, "Le nom du webhook à supprimer")):
# Your logic here to delete the specified webhook
webhook = trema_db.get_webhook_by_name(webhook_name, ctx.guild.id)
if webhook is None:
embed=Embed(title="Webhook non trouvé", description=f"Le webhook '{webhook_name}' n'existe pas.", color=Color.blurple())
await ctx.respond(embed=embed, ephemeral=True)
return

trema_db.delete_webhook(webhook_name, ctx.guild.id)

embed_title = _make_cmd_full_name(ctx.command) + _SPACE + webhook_name
embed = Embed(title=embed_title, description=f"Webhook '{webhook_name}' supprimé.", color=Color.blurple())
await ctx.respond(embed=embed, ephemeral=True)

@webhook.command(name="update",
description="Met à jour le canal lié au webhook")
@is_authorized(trema_db)
async def update_webhook(ctx,
webhook_name: Option(str, "Le nom du webhook à mettre à jour"),
new_channel_id: Option(str, "Le nouveau canal à associer au webhook")):

new_channel = ctx.guild.get_channel(int(new_channel_id))
if new_channel:
trema_db.update_webhook_channel(webhook_name, new_channel.id, ctx.guild.id)
embed=Embed(title="Webhook mis à jour", description=f"Le webhook '{webhook_name}' a été mis à jour pour le canal {new_channel_id} avec succès.", color=Color.blurple())
await ctx.respond(embed=embed, ephemeral=True)
else:
await ctx.respond(f"Le canal '{new_channel_id}' n'existe pas.", ephemeral=True)

return webhook
35 changes: 30 additions & 5 deletions trema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
import discord
import sys
from datetime import datetime

from quart import Quart
import asyncio
from events import\
create_event_reactions

from slash_commands import\
create_slash_cmds

from trema_database import\
get_trema_database
from routes import\
create_routes

start_time = datetime.now()

Expand All @@ -23,6 +24,18 @@
print('ERROR: Token var is missing: DISCORD_TOKEN')
sys.exit(-1)

api_address = os.getenv('API_ADDRESS')
if not api_address:
print('ERROR: API address var is missing: API_ADDRESS')
sys.exit(-1)

api_port = os.getenv('API_PORT')
if not api_port:
print('ERROR: API port var is missing: API_PORT')
sys.exit(-1)

app = Quart(__name__)

intents = discord.Intents.default()
intents.members = True
trema = discord.Bot(intents=intents)
Expand All @@ -31,5 +44,17 @@

create_event_reactions(trema, database)
create_slash_cmds(trema, database, start_time)

trema.run(bot_token)
create_routes(app, database, trema)

async def main():
loop = asyncio.get_event_loop()

# Start the bot and the API
bot_coro = loop.create_task(trema.start(bot_token))
api_coro = loop.create_task(app.run_task(host=api_address, port=api_port))

await asyncio.gather(bot_coro, api_coro)

if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Loading

0 comments on commit ff96489

Please sign in to comment.