Drop Matrix support

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2024-01-13 16:46:19 +01:00
parent 2f4755ffc7
commit 93a2e2436d
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
12 changed files with 3 additions and 991 deletions

View File

@ -54,7 +54,6 @@ SERVER_EMAIL=contact@tfjm.org # Adresse e-mail expéditrice
SYMPA_URL=lists.example.com # Serveur Sympa à utiliser SYMPA_URL=lists.example.com # Serveur Sympa à utiliser
SYMPA_EMAIL= # Adresse e-mail du compte administrateur de Sympa SYMPA_EMAIL= # Adresse e-mail du compte administrateur de Sympa
SYMPA_PASSWORD= # Mot de passe du compte administrateur de Sympa SYMPA_PASSWORD= # Mot de passe du compte administrateur de Sympa
SYNAPSE_PASSWORD= # Mot de passe du robot Matrix
``` ```
Si le type de base de données sélectionné est SQLite, la variable `DJANGO_DB_HOST` sera utilisée en guise de chemin vers Si le type de base de données sélectionné est SQLite, la variable `DJANGO_DB_HOST` sera utilisée en guise de chemin vers

View File

@ -1,477 +0,0 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import asyncio
import os
from django.core.management import BaseCommand
from django.utils.http import urlencode
from django.utils.translation import activate
from participation.models import Team, Tournament
from registration.models import Registration, VolunteerRegistration
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
class Command(BaseCommand):
def handle(self, *args, **options): # noqa: C901
activate("fr")
async def main():
await Matrix.set_display_name("Bot du TFJM²")
if not os.getenv("SYNAPSE_PASSWORD"):
avatar_uri = "plop"
else: # pragma: no cover
if not os.path.isfile(".matrix_avatar"):
avatar_uri = await Matrix.get_avatar()
if isinstance(avatar_uri, str):
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
else:
stat_file = os.stat("tfjm/static/logo.png")
with open("tfjm/static/logo.png", "rb") as f:
resp = (await Matrix.upload(f, filename="../../../tfjm/static/logo.png", content_type="image/png",
filesize=stat_file.st_size))[0][0]
avatar_uri = resp.content_uri
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
await Matrix.set_avatar(avatar_uri)
with open(".matrix_avatar", "r") as f:
avatar_uri = f.read().rstrip(" \t\r\n")
# Create basic channels
if not await Matrix.resolve_room_alias("#aide-jurys-orgas:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="aide-jurys-orgas",
name="Aide jurys & orgas",
topic="Pour discuter de propblèmes d'organisation",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#annonces:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="annonces",
name="Annonces",
topic="Informations importantes du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bienvenue:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bienvenue",
name="Bienvenue",
topic="Bienvenue au TFJM² 2023 !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bot",
name="Bot",
topic="Vive les r0b0ts",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#cno:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="cno",
name="CNO",
topic="Channel des dieux",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#dev-bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="dev-bot",
name="Bot - développement",
topic="Vive le bot",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#faq:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="faq",
name="FAQ",
topic="Posez toutes vos questions ici !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#flood:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="flood",
name="Flood",
topic="Discutez de tout et de rien !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#je-cherche-une-equipe:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="je-cherche-une-equipe",
name="Je cherche une équipe",
topic="Le Tinder du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
# Setup avatars
await Matrix.set_room_avatar("#aide-jurys-orgas:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#annonces:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bienvenue:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#cno:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#dev-bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#faq:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#flood:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#je-cherche-une-equipe:tfjm.org", avatar_uri)
# Read-only channels
await Matrix.set_room_power_level_event("#annonces:tfjm.org", "events_default", 50)
await Matrix.set_room_power_level_event("#bienvenue:tfjm.org", "events_default", 50)
# Invite everyone to public channels
for r in Registration.objects.all():
await Matrix.invite("#annonces:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bienvenue:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bot:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#faq:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#flood:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#je-cherche-une-equipe:tfjm.org",
f"@{r.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {r} in most common channels...")
# Volunteers have access to the help channel
for volunteer in VolunteerRegistration.objects.all():
await Matrix.invite("#aide-jurys-orgas:tfjm.org", f"@{volunteer.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {volunteer} in #aide-jury-orgas...")
# Admins are admins
for admin in VolunteerRegistration.objects.filter(admin=True).all():
self.stdout.write(f"Invite {admin} in #cno and #dev-bot...")
await Matrix.invite("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Give admin permissions for {admin}...")
await Matrix.set_room_power_level("#aide-jurys-orgas:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#annonces:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bienvenue:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#cno:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#dev-bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#faq:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#flood:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#je-cherche-une-equipe:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Create tournament-specific channels
for tournament in Tournament.objects.all():
self.stdout.write(f"Managing tournament of {tournament.name}.")
name = tournament.name
slug = name.lower().replace(" ", "-")
if not await Matrix.resolve_room_alias(f"#annonces-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"annonces-{slug}",
name=f"{name} - Annonces",
topic=f"Annonces du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#general-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"general-{slug}",
name=f"{name} - Général",
topic=f"Accueil du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#flood-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"flood-{slug}",
name=f"{name} - Flood",
topic=f"Discussion libre du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#jury-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"jury-{slug}",
name=f"{name} - Jury",
topic=f"Discussion entre les orgas et jurys du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#orga-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"orga-{slug}",
name=f"{name} - Organisateurs",
topic=f"Discussion entre les orgas du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#tirage-au-sort-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"tirage-au-sort-{slug}",
name=f"{name} - Tirage au sort",
topic=f"Tirage au sort du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
# Setup avatars
await Matrix.set_room_avatar(f"#annonces-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#flood-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#general-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#jury-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#orga-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#tirage-au-sort-{slug}:tfjm.org", avatar_uri)
# Invite admins and give permissions
for admin in VolunteerRegistration.objects.filter(admin=True).all():
self.stdout.write(f"Invite {admin} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Give permissions to {admin} in all channels of the tournament {name}...")
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
self.stdout.write(f"Invite organizer {orga} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite participants
for participation in tournament.participations.filter(valid=True).all():
for participant in participation.team.participants.all():
self.stdout.write(f"Invite {participant} in public channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create pool-specific channels
for pool in tournament.pools.all():
self.stdout.write(f"Managing {pool}...")
five = pool.participations.count() >= 5
for i in range(2 if five else 1):
# Fix for five teams-pools
suffix = f"-{chr(ord('A') + i)}" if five else ""
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}",
name=f"{name} - Jour {pool.round} - Poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les équipes - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}-jurys",
name=f"{name} - Jour {pool.round}{suffix} - Jurys poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les jurys - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org", avatar_uri)
bbb_url = pool.bbb_url.strip()
if five and ';' in bbb_url:
bbb_url = bbb_url.split(";")[i].strip()
url_params = urlencode(dict(url=bbb_url,
isAudioConf='false', displayName='$matrix_display_name',
avatarUrl='$matrix_avatar_url', userId='$matrix_user_id')) \
.replace("%24", "$")
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"https://scalar.vector.im/api/widgets/bigbluebutton.html?{url_params}",
f"bbb-{slug}-{pool.id}{suffix}", "bigbluebutton", "BigBlueButton", str(pool))
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}:tfjm.org",
f"https://board.tfjm.org/boards/{slug}-{pool.id}", f"board-{slug}-{pool.id}",
"customwidget", "Tableau", str(pool))
# Invite admins and give permissions
for admin in VolunteerRegistration.objects.filter(admin=True).all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite the jury, give good permissions
for jury in pool.juries.all():
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
if not jury.is_admin:
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
# Invite participants to the right pool
for participation in pool.participations.all():
for participant in participation.team.participants.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create private channels for teams
for team in Team.objects.all():
self.stdout.write(f"Create private channel for {team}...")
if not await Matrix.resolve_room_alias(f"#equipe-{team.trigram.lower()}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"equipe-{team.trigram.lower()}",
name=f"Équipe {team.trigram}",
topic=f"Discussion interne de l'équipe {team.name}",
federate=False,
preset=RoomPreset.private_chat,
)
for participant in team.participants.all():
await Matrix.invite(f"#equipe-{team.trigram.lower}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#equipe-{team.trigram.lower()}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org", 50)
"""
# Manage channels to discuss about problems
for i in range(9):
self.stdout.write(f"Create channel for problem {i}...")
if not await Matrix.resolve_room_alias(f"#mec-{i}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"mec-{i}",
name=f"Mise en commun - {'Général' if i == 0 else f'Problème {i}'}",
topic=f"Discussion autour des problèmes",
federate=False,
preset=RoomPreset.public_chat,
invite=[f"@{registration.matrix_username}:tfjm.org"
for registration in Registration.objects.all()],
power_level_override={
f"@{registration.matrix_username}:tfjm.org": (95 if registration.is_admin else 50)
for registration in VolunteerRegistration.objects.all()
},
)
await Matrix.set_room_avatar(f"#mec-{i}:tfjm.org", avatar_uri)
for registration in Registration.objects.all():
await Matrix.invite(f"#mec-{i}:tfjm.org", registration.matrix_username)
for registration in VolunteerRegistration.objects.all():
await Matrix.set_room_power_level(f"#mec-{i}:tfjm.org",
f"@{registration.matrix_username}:tfjm.org",
95 if registration.is_admin else 50)
"""
asyncio.get_event_loop().run_until_complete(main())

View File

@ -16,8 +16,6 @@ from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from registration.models import VolunteerRegistration from registration.models import VolunteerRegistration
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
def get_motivation_letter_filename(instance, filename): def get_motivation_letter_filename(instance, filename):
return f"authorization/motivation_letters/motivation_letter_{instance.trigram}" return f"authorization/motivation_letters/motivation_letter_{instance.trigram}"
@ -101,18 +99,9 @@ class Team(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.access_code: if not self.access_code:
# if the team got created, generate the access code, create the contact mailing list # if the team got created, generate the access code, create the contact mailing list
# and create a dedicated Matrix room.
self.access_code = get_random_string(6) self.access_code = get_random_string(6)
self.create_mailing_list() self.create_mailing_list()
Matrix.create_room(
visibility=RoomVisibility.private,
name=f"#équipe-{self.trigram.lower()}",
alias=f"equipe-{self.trigram.lower()}",
topic=f"Discussion de l'équipe {self.name}",
preset=RoomPreset.private_chat,
)
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
def get_absolute_url(self): def get_absolute_url(self):

View File

@ -19,12 +19,11 @@ def create_team_participation(instance, created, raw, **_):
def update_mailing_list(instance: Team, raw, **_): def update_mailing_list(instance: Team, raw, **_):
""" """
When a team name or trigram got updated, update mailing lists and Matrix rooms When a team name or trigram got updated, update mailing lists
""" """
if instance.pk and not raw: if instance.pk and not raw:
old_team = Team.objects.get(pk=instance.pk) old_team = Team.objects.get(pk=instance.pk)
if old_team.trigram != instance.trigram: if old_team.trigram != instance.trigram:
# TODO Rename Matrix room
# Delete old mailing list, create a new one # Delete old mailing list, create a new one
old_team.delete_mailing_list() old_team.delete_mailing_list()
instance.create_mailing_list() instance.create_mailing_list()

View File

@ -32,7 +32,6 @@ from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
from odf.text import P from odf.text import P
from registration.models import StudentRegistration, VolunteerRegistration from registration.models import StudentRegistration, VolunteerRegistration
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix
from tfjm.views import AdminMixin, VolunteerMixin from tfjm.views import AdminMixin, VolunteerMixin
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \ from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
@ -68,8 +67,7 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
""" """
When a team is about to be created, the user automatically When a team is about to be created, the user automatically
joins the team, a mailing list got created and the user is joins the team, a mailing list got created and the user is
automatically subscribed to this mailing list, and finally automatically subscribed to this mailing list.
a Matrix room is created and the user is invited in this room.
""" """
ret = super().form_valid(form) ret = super().form_valid(form)
# The user joins the team # The user joins the team
@ -82,9 +80,6 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False, get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
f"{user.first_name} {user.last_name}") f"{user.first_name} {user.last_name}")
# Invite the user in the team Matrix room
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
f"@{user.registration.matrix_username}:tfjm.org")
return ret return ret
@ -112,7 +107,7 @@ class JoinTeamView(LoginRequiredMixin, FormView):
def form_valid(self, form): def form_valid(self, form):
""" """
When a user joins a team, the user is automatically subscribed to When a user joins a team, the user is automatically subscribed to
the team mailing list,the user is invited in the team Matrix room. the team mailing list.
""" """
self.object = form.instance self.object = form.instance
ret = super().form_valid(form) ret = super().form_valid(form)
@ -127,9 +122,6 @@ class JoinTeamView(LoginRequiredMixin, FormView):
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False, get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
f"{user.first_name} {user.last_name}") f"{user.first_name} {user.last_name}")
# Invite the user in the team Matrix room
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
f"@{user.registration.matrix_username}:tfjm.org")
return ret return ret
def get_success_url(self): def get_success_url(self):
@ -468,9 +460,6 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
request.user.registration.team = None request.user.registration.team = None
request.user.registration.save() request.user.registration.save()
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False) get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False)
Matrix.kick(f"#equipe-{team.trigram.lower()}:tfjm.org",
f"@{request.user.registration.matrix_username}:tfjm.org",
"Équipe quittée")
if team.students.count() + team.coaches.count() == 0: if team.students.count() + team.coaches.count() == 0:
team.delete() team.delete()
return redirect(reverse_lazy("index")) return redirect(reverse_lazy("index"))

View File

@ -1,17 +0,0 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
"""
Override Django Auth User model to define a custom Matrix username.
"""
def attributs(self):
d = super().attributs()
if self.user:
d["matrix_username"] = self.user.registration.matrix_username
d["display_name"] = str(self.user.registration)
return d

View File

@ -1,26 +0,0 @@
[
{
"model": "cas_server.servicepattern",
"pk": 1,
"fields": {
"pos": 100,
"name": "Plateforme du TFJM²",
"pattern": "^https://tfjm.org(:8448)?/.*$",
"user_field": "matrix_username",
"restrict_users": false,
"proxy": true,
"proxy_callback": true,
"single_log_out": true,
"single_log_out_callback": ""
}
},
{
"model": "cas_server.replaceattributname",
"pk": 1,
"fields": {
"name": "display_name",
"replace": "",
"service_pattern": 1
}
}
]

View File

@ -85,10 +85,6 @@ class Registration(PolymorphicModel):
def is_volunteer(self): def is_volunteer(self):
return isinstance(self, VolunteerRegistration) return isinstance(self, VolunteerRegistration)
@property
def matrix_username(self):
return f"tfjm_{self.user.pk}"
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy("registration:user_detail", args=(self.user_id,)) return reverse_lazy("registration:user_detail", args=(self.user_id,))

View File

@ -2,7 +2,6 @@ channels[daphne]~=4.0.0
channels-redis~=4.0.0 channels-redis~=4.0.0
crispy-bootstrap5~=0.7 crispy-bootstrap5~=0.7
Django>=4.1,<5.0 Django>=4.1,<5.0
django-cas-server~=2.0
django-crispy-forms~=1.14 django-crispy-forms~=1.14
django-extensions~=3.2 django-extensions~=3.2
django-filter~=22.1 django-filter~=22.1
@ -14,7 +13,6 @@ django-tables2~=2.4
djangorestframework~=3.14 djangorestframework~=3.14
django-rest-polymorphic~=0.1 django-rest-polymorphic~=0.1
gunicorn~=20.1 gunicorn~=20.1
matrix-nio~=0.20
odfpy~=1.4.1 odfpy~=1.4.1
phonenumbers~=8.12.57 phonenumbers~=8.12.57
psycopg2-binary~=2.9.5 psycopg2-binary~=2.9.5

View File

@ -10,9 +10,6 @@
# Recreate sympa lists # Recreate sympa lists
*/2 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null */2 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
# Update matrix channels
03 */6 * * * cd /code && python manage.py fix_matrix_channels &> /dev/null
# Check payments from Hello Asso # Check payments from Hello Asso
*/6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null */6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null

View File

@ -1,432 +0,0 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum
import os
class Matrix:
"""
Utility class to manage interaction with the Matrix homeserver.
This log in the @tfjmbot account (must be created before).
The access token is then stored.
All is done with this bot account, that is a server administrator.
Tasks are normally asynchronous, but for compatibility we make
them synchronous.
"""
_token = None
_device_id = None
@classmethod
async def _get_client(cls): # pragma: no cover
"""
Retrieve the bot account.
If not logged, log in and store access token.
"""
if not os.getenv("SYNAPSE_PASSWORD") and not os.getenv("SYNAPSE_TOKEN"):
return FakeMatrixClient()
from nio import AsyncClient
client = AsyncClient("https://tfjm.org", "@tfjmbot:tfjm.org")
client.user_id = "@tfjmbot:tfjm.org"
if os.getenv("SYNAPSE_TOKEN"):
client.access_token = os.getenv("SYNAPSE_TOKEN")
client.device_id = os.getenv("SYNAPSE_DEVICE")
return client
elif os.path.isfile(".matrix_token"):
with open(".matrix_device", "r") as f:
cls._device_id = f.read().rstrip(" \t\r\n")
client.device_id = cls._device_id
with open(".matrix_token", "r") as f:
cls._token = f.read().rstrip(" \t\r\n")
client.access_token = cls._token
return client
await client.login(password=os.getenv("SYNAPSE_PASSWORD"), device_name="Plateforme")
cls._token = client.access_token
cls._device_id = client.device_id
with open(".matrix_token", "w") as f:
f.write(cls._token)
with open(".matrix_device", "w") as f:
f.write(cls._device_id)
return client
@classmethod
async def set_display_name(cls, name: str):
"""
Set the display name of the bot account.
"""
client = await cls._get_client()
return await client.set_displayname(name)
@classmethod
async def set_avatar(cls, avatar_url: str): # pragma: no cover
"""
Set the display avatar of the bot account.
"""
client = await cls._get_client()
return await client.set_avatar(avatar_url)
@classmethod
async def get_avatar(cls): # pragma: no cover
"""
Set the display avatar of the bot account.
"""
client = await cls._get_client()
resp = await client.get_avatar()
return resp.avatar_url if hasattr(resp, "avatar_url") else resp
@classmethod
async def upload(
cls,
data_provider,
content_type: str = "application/octet-stream",
filename: str = None,
encrypt: bool = False,
monitor=None,
filesize: int = None,
): # pragma: no cover
"""
Upload a file to the content repository.
Returns a tuple containing:
- Either a `UploadResponse` if the request was successful, or a
`UploadError` if there was an error with the request
- A dict with file decryption info if encrypt is ``True``,
else ``None``.
Args:
data_provider (Callable, SynchronousFile, AsyncFile): A function
returning the data to upload or a file object. File objects
must be opened in binary mode (``mode="r+b"``). Callables
returning a path string, Path, async iterable or aiofiles
open binary file object allow the file data to be read in an
asynchronous and lazy way (without reading the entire file
into memory). Returning a synchronous iterable or standard
open binary file object will still allow the data to be read
lazily, but not asynchronously.
The function will be called again if the upload fails
due to a server timeout, in which case it must restart
from the beginning.
Callables receive two arguments: the total number of
429 "Too many request" errors that occured, and the total
number of server timeout exceptions that occured, thus
cleanup operations can be performed for retries if necessary.
content_type (str): The content MIME type of the file,
e.g. "image/png".
Defaults to "application/octet-stream", corresponding to a
generic binary file.
Custom values are ignored if encrypt is ``True``.
filename (str, optional): The file's original name.
encrypt (bool): If the file's content should be encrypted,
necessary for files that will be sent to encrypted rooms.
Defaults to ``False``.
monitor (TransferMonitor, optional): If a ``TransferMonitor``
object is passed, it will be updated by this function while
uploading.
From this object, statistics such as currently
transferred bytes or estimated remaining time can be gathered
while the upload is running as a task; it also allows
for pausing and cancelling.
filesize (int, optional): Size in bytes for the file to transfer.
If left as ``None``, some servers might refuse the upload.
"""
client = await cls._get_client()
return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \
if not isinstance(client, FakeMatrixClient) else None, None
@classmethod
async def create_room(
cls,
visibility=None,
alias=None,
name=None,
topic=None,
room_version=None,
federate=True,
is_direct=False,
preset=None,
invite=(),
initial_state=(),
power_level_override=None,
):
"""
Create a new room.
Returns either a `RoomCreateResponse` if the request was successful or
a `RoomCreateError` if there was an error with the request.
Args:
visibility (RoomVisibility): whether to have the room published in
the server's room directory or not.
Defaults to ``RoomVisibility.private``.
alias (str, optional): The desired canonical alias local part.
For example, if set to "foo" and the room is created on the
"example.com" server, the room alias will be
"#foo:example.com".
name (str, optional): A name to set for the room.
topic (str, optional): A topic to set for the room.
room_version (str, optional): The room version to set.
If not specified, the homeserver will use its default setting.
If a version not supported by the homeserver is specified,
a 400 ``M_UNSUPPORTED_ROOM_VERSION`` error will be returned.
federate (bool): Whether to allow users from other homeservers from
joining the room. Defaults to ``True``.
Cannot be changed later.
is_direct (bool): If this should be considered a
direct messaging room.
If ``True``, the server will set the ``is_direct`` flag on
``m.room.member events`` sent to the users in ``invite``.
Defaults to ``False``.
preset (RoomPreset, optional): The selected preset will set various
rules for the room.
If unspecified, the server will choose a preset from the
``visibility``: ``RoomVisibility.public`` equates to
``RoomPreset.public_chat``, and
``RoomVisibility.private`` equates to a
``RoomPreset.private_chat``.
invite (list): A list of user id to invite to the room.
initial_state (list): A list of state event dicts to send when
the room is created.
For example, a room could be made encrypted immediatly by
having a ``m.room.encryption`` event dict.
power_level_override (dict): A ``m.room.power_levels content`` dict
to override the default.
The dict will be applied on top of the generated
``m.room.power_levels`` event before it is sent to the room.
"""
client = await cls._get_client()
return await client.room_create(
visibility, alias, name, topic, room_version, federate, is_direct, preset, invite, initial_state,
power_level_override)
@classmethod
async def resolve_room_alias(cls, room_alias: str):
"""
Resolve a room alias to a room ID.
Return None if the alias does not exist.
"""
client = await cls._get_client()
resp = await client.room_resolve_alias(room_alias)
return resp.room_id if resp and hasattr(resp, "room_id") else None
@classmethod
async def invite(cls, room_id: str, user_id: str):
"""
Invite a user to a room.
Returns either a `RoomInviteResponse` if the request was successful or
a `RoomInviteError` if there was an error with the request.
Args:
room_id (str): The room id of the room that the user will be
invited to.
user_id (str): The user id of the user that should be invited.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_invite(room_id, user_id)
@classmethod
async def send_message(cls, room_id: str, body: str, formatted_body: str = None,
msgtype: str = "m.text", html: bool = True):
"""
Send a message to a room.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
content = {
"msgtype": msgtype,
"body": body,
"formatted_body": formatted_body or body,
}
if html:
content["format"] = "org.matrix.custom.html"
return await client.room_send(
room_id=room_id,
message_type="m.room.message",
content=content,
)
@classmethod
async def add_integration(cls, room_id: str, widget_url: str, state_key: str,
widget_type: str = "customwidget", widget_name: str = "Custom widget",
widget_title: str = ""):
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
content = {
"type": widget_type,
"url": widget_url,
"name": widget_name,
"data": {
"curl": widget_url,
"title": widget_title,
},
"creatorUserId": client.user,
"roomId": room_id,
"id": state_key,
}
return await client.room_put_state(
room_id=room_id,
event_type="im.vector.modular.widgets",
content=content,
state_key=state_key,
)
@classmethod
async def remove_integration(cls, room_id: str, state_key: str):
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_put_state(
room_id=room_id,
event_type="im.vector.modular.widgets",
content={},
state_key=state_key,
)
@classmethod
async def kick(cls, room_id: str, user_id: str, reason: str = None):
"""
Kick a user from a room, or withdraw their invitation.
Kicking a user adjusts their membership to "leave" with an optional
reason.
²
Returns either a `RoomKickResponse` if the request was successful or
a `RoomKickError` if there was an error with the request.
Args:
room_id (str): The room id of the room that the user will be
kicked from.
user_id (str): The user_id of the user that should be kicked.
reason (str, optional): A reason for which the user is kicked.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_kick(room_id, user_id, reason)
@classmethod
async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int): # pragma: no cover
"""
Put a given power level to a user in a certain room.
Returns either a `RoomPutStateResponse` if the request was successful or
a `RoomPutStateError` if there was an error with the request.
Args:
room_id (str): The room id of the room where the power level
of the user should be updated.
user_id (str): The user_id of the user which power level should
be updated.
power_level (int): The target power level to give.
"""
client = await cls._get_client()
if isinstance(client, FakeMatrixClient):
return None
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
resp = await client.room_get_state_event(room_id, "m.room.power_levels")
content = resp.content
content["users"][user_id] = power_level
return await client.room_put_state(room_id, "m.room.power_levels", content=content, state_key=resp.state_key)
@classmethod
async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int): # pragma: no cover
"""
Define the minimal power level to have to send a certain event type
in a given room.
Returns either a `RoomPutStateResponse` if the request was successful or
a `RoomPutStateError` if there was an error with the request.
Args:
room_id (str): The room id of the room where the power level
of the event should be updated.
event (str): The event name which minimal power level should
be updated.
power_level (int): The target power level to give.
"""
client = await cls._get_client()
if isinstance(client, FakeMatrixClient):
return None
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
resp = await client.room_get_state_event(room_id, "m.room.power_levels")
content = resp.content
if event.startswith("m."):
content["events"][event] = power_level
else:
content[event] = power_level
return await client.room_put_state(room_id, "m.room.power_levels", content=content, state_key=resp.state_key)
@classmethod
async def set_room_avatar(cls, room_id: str, avatar_uri: str):
"""
Define the avatar of a room.
Returns either a `RoomPutStateResponse` if the request was successful or
a `RoomPutStateError` if there was an error with the request.
Args:
room_id (str): The room id of the room where the avatar
should be changed.
avatar_uri (str): The internal avatar URI to apply.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_put_state(room_id, "m.room.avatar", content={
"url": avatar_uri
}, state_key="")
if os.getenv("SYNAPSE_PASSWORD"): # pragma: no cover
from nio import RoomVisibility, RoomPreset
RoomVisibility = RoomVisibility
RoomPreset = RoomPreset
else:
# When running tests, faking matrix-nio classes to don't include the module
class RoomVisibility(Enum):
private = 'private'
public = 'public'
class RoomPreset(Enum):
private_chat = "private_chat"
trusted_private_chat = "trusted_private_chat"
public_chat = "public_chat"
class FakeMatrixClient:
"""
Simulate a Matrix client to run tests, if no Matrix homeserver is connected.
"""
def __getattribute__(self, item):
async def func(*_, **_2):
return None
return func

View File

@ -74,7 +74,6 @@ INSTALLED_APPS = [
if "test" not in sys.argv: # pragma: no cover if "test" not in sys.argv: # pragma: no cover
INSTALLED_APPS += [ INSTALLED_APPS += [
'cas_server',
'django_extensions', 'django_extensions',
'mailer', 'mailer',
] ]
@ -147,8 +146,6 @@ PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.BCryptPasswordHasher',
] ]
CAS_AUTH_CLASS = 'registration.auth.CustomAuthUser'
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAdminUser' 'rest_framework.permissions.IsAdminUser'