diff --git a/aleksis/apps/matrix/apps.py b/aleksis/apps/matrix/apps.py index 6128273ad9f4fe91815b5d708e629c95581d3a00..984ec7237a76e0aa9a0848b266fff48e0a5ee9c9 100644 --- a/aleksis/apps/matrix/apps.py +++ b/aleksis/apps/matrix/apps.py @@ -1,3 +1,5 @@ +from django.db.models.signals import m2m_changed, post_save + from aleksis.core.util.apps import AppConfig @@ -11,3 +13,16 @@ class DefaultConfig(AppConfig): } licence = "EUPL-1.2+" copyright_info = (([2021, 2022], "Jonathan Weth", "dev@jonathanweth.de"),) + + def ready(self): + from aleksis.core.models import Group + + from .models import MatrixProfile, MatrixRoom + from .signals import m2m_changed_matrix_signal, post_save_matrix_signal + + post_save.connect(post_save_matrix_signal, sender=Group) + post_save.connect(post_save_matrix_signal, sender=MatrixProfile) + post_save.connect(post_save_matrix_signal, sender=MatrixRoom) + + m2m_changed.connect(m2m_changed_matrix_signal, sender=Group.members.through) + m2m_changed.connect(m2m_changed_matrix_signal, sender=Group.owners.through) diff --git a/aleksis/apps/matrix/locale/de_DE/LC_MESSAGES/django.po b/aleksis/apps/matrix/locale/de_DE/LC_MESSAGES/django.po index aff79b37efd858e32496ff7b240fd31c58e69f8b..b5e7a2ead3d9eb1ed21dc0fab0c0eaa3693d9653 100644 --- a/aleksis/apps/matrix/locale/de_DE/LC_MESSAGES/django.po +++ b/aleksis/apps/matrix/locale/de_DE/LC_MESSAGES/django.po @@ -3,173 +3,185 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-06-25 11:24+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"Language: \n" +"POT-Creation-Date: 2022-06-04 11:13+0000\n" +"PO-Revision-Date: 2022-06-23 09:03+0000\n" +"Last-Translator: Jonathan Weth <teckids@jonathanweth.de>\n" +"Language-Team: German <https://translate.edugit.org/projects/aleksis/" +"aleksis-app-matrix/de/>\n" +"Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.12.1\n" #: aleksis/apps/matrix/forms.py:12 msgid "Provision in Matrix" -msgstr "" +msgstr "In Matrix bereitstellen" #: aleksis/apps/matrix/menus.py:6 aleksis/apps/matrix/preferences.py:8 msgid "Matrix" -msgstr "" +msgstr "Matrix" #: aleksis/apps/matrix/menus.py:18 msgid "Groups and Rooms" -msgstr "" +msgstr "Gruppen und Räume" #: aleksis/apps/matrix/model_extensions.py:43 msgid "Can view matrix room of a group" -msgstr "" +msgstr "Kann Matrix-Raum einer Gruppe sehen" #: aleksis/apps/matrix/models.py:19 msgid "Matrix ID" -msgstr "" +msgstr "Matrix-ID" #: aleksis/apps/matrix/models.py:23 msgid "Person" -msgstr "" +msgstr "Person" #: aleksis/apps/matrix/models.py:52 msgid "Matrix profile" -msgstr "" +msgstr "Matrix-Profil" #: aleksis/apps/matrix/models.py:53 msgid "Matrix profiles" -msgstr "" +msgstr "Matrix-Profile" #: aleksis/apps/matrix/models.py:59 msgid "Room ID" -msgstr "" +msgstr "Raum-ID" #: aleksis/apps/matrix/models.py:60 msgid "Alias" -msgstr "" +msgstr "Alias" #: aleksis/apps/matrix/models.py:64 msgid "Group" -msgstr "" +msgstr "Gruppe" #: aleksis/apps/matrix/models.py:266 msgid "Matrix room" -msgstr "" +msgstr "Matrix-Raum" #: aleksis/apps/matrix/models.py:267 msgid "Matrix rooms" -msgstr "" +msgstr "Matrix-Räume" #: aleksis/apps/matrix/models.py:273 msgid "Child rooms/spaces" -msgstr "" +msgstr "Kind-Räume/-Spaces" #: aleksis/apps/matrix/models.py:355 msgid "Matrix space" -msgstr "" +msgstr "Matrix-Space" #: aleksis/apps/matrix/models.py:356 msgid "Matrix spaces" -msgstr "" +msgstr "Matrix-Spaces" #: aleksis/apps/matrix/preferences.py:15 msgid "URL of Matrix homeserver" -msgstr "" +msgstr "URL des Matrix-Homeservers" #: aleksis/apps/matrix/preferences.py:18 msgid "URL of the Matrix homeserver on which groups and spaces should be created (e. g. https://matrix.org)" msgstr "" +"URL des Matrix-Homeservers, auf dem Gruppen und Spaces erstellt werden " +"sollen (z. B. https://matrix.org)" #: aleksis/apps/matrix/preferences.py:27 msgid "Name of Matrix homeserver used for auto-generating Matrix IDs" msgstr "" +"Name des Matrix-Homeservers, der für das automatische Generieren von Matrix-" +"IDs genutzt werden soll" #: aleksis/apps/matrix/preferences.py:28 msgid "Leave empty to not create Matrix IDs automatically" -msgstr "" +msgstr "Freilassen, um Matrix-IDs nicht automatisch zu erstellen" #: aleksis/apps/matrix/preferences.py:36 msgid "Access token to access homeserver" -msgstr "" +msgstr "Access-Token, um auf den Homeserver zuzugreifen" #: aleksis/apps/matrix/preferences.py:39 msgid "This has to be the access token of a suitable bot user. It is used for all actions." msgstr "" +"Dies muss das Access-Token für einen passenden Bot-Benutzer sein. Es wird " +"für alle Aktionen verwendet." #: aleksis/apps/matrix/preferences.py:47 msgid "Disambiguate room aliases" -msgstr "" +msgstr "Raum-Aliase eindeutig machen" #: aleksis/apps/matrix/preferences.py:49 msgid "Suffix room aliases with ascending numbers to avoid name clashes" msgstr "" +"Raum-Aliase mit aufsteigenden Nummern ergänzen, um Namenskonflikte zu " +"vermeiden" #: aleksis/apps/matrix/preferences.py:56 msgid "Use Matrix spaces" -msgstr "" +msgstr "Matrix-Spaces nutzen" #: aleksis/apps/matrix/preferences.py:58 msgid "This activates the creation and management of Matrix spaces." -msgstr "" +msgstr "Dies aktiviert die Erstellung und Verwaltung von Matrix-Spaces." #: aleksis/apps/matrix/preferences.py:65 msgid "Reduce existing power levels" -msgstr "" +msgstr "Existierende Power-Levels reduzieren" #: aleksis/apps/matrix/preferences.py:67 msgid "Reduce power levels of existing members to the level suggested by AlekSIS." msgstr "" +"Power-Levels von existierenden Mitgliedern auf das von AlekSIS " +"vorgeschlagene Level reduzieren." #: aleksis/apps/matrix/preferences.py:74 msgid "Power level for owners" -msgstr "" +msgstr "Power-Level für Besitzer" #: aleksis/apps/matrix/preferences.py:77 msgid "This power level will be set for all owners of a group." -msgstr "" +msgstr "Dieses Power-Level wird für alle Gruppenbesitzer gesetzt." #: aleksis/apps/matrix/preferences.py:84 msgid "Power level for members" -msgstr "" +msgstr "Power-Level für Mitglieder" #: aleksis/apps/matrix/preferences.py:87 msgid "This power level will be set for all members of a group." -msgstr "" +msgstr "Dieses Power-Level wird für alle Gruppenmitglieder gesetzt." #: aleksis/apps/matrix/templates/matrix/room/list.html:8 #: aleksis/apps/matrix/templates/matrix/room/list.html:9 msgid "Groups and Matrix Rooms" -msgstr "" +msgstr "Gruppen und Matrix-Räume" #: aleksis/apps/matrix/templates/matrix/room/list.html:14 msgid "Create group" -msgstr "" +msgstr "Gruppe erstellen" #: aleksis/apps/matrix/templates/matrix/room/list.html:21 msgid "Filter groups" -msgstr "" +msgstr "Gruppen filtern" #: aleksis/apps/matrix/templates/matrix/room/list.html:25 msgid "Filter" -msgstr "" +msgstr "Filtern" #: aleksis/apps/matrix/templates/matrix/room/list.html:28 msgid "Clear" -msgstr "" +msgstr "Zurücksetzen" #: aleksis/apps/matrix/templates/matrix/room/list.html:43 msgid "Selected groups" -msgstr "" +msgstr "Ausgewählte Gruppen" #: aleksis/apps/matrix/templates/matrix/room/list.html:53 msgid "Execute" -msgstr "" +msgstr "Ausführen" diff --git a/aleksis/apps/matrix/models.py b/aleksis/apps/matrix/models.py index a2c19c573d16746fa0b9c3e57dddc2642816cc30..7f2a7a331fda8d849817e69db19a3f9ec566cb3f 100644 --- a/aleksis/apps/matrix/models.py +++ b/aleksis/apps/matrix/models.py @@ -6,6 +6,8 @@ from django.db.models import Q, QuerySet from django.template.defaultfilters import slugify from django.utils.translation import gettext_lazy as _ +from model_utils import FieldTracker + from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel from aleksis.core.models import Group, Person from aleksis.core.util.core_helpers import get_site_preferences @@ -26,6 +28,8 @@ class MatrixProfile(ExtensibleModel): related_name="matrix_profile", ) + change_tracker = FieldTracker() + @classmethod def build_matrix_id(cls, username: str, homeserver: Optional[str] = None) -> str: """Build a Matrix ID from a username.""" @@ -65,6 +69,8 @@ class MatrixRoom(ExtensiblePolymorphicModel): related_name="matrix_rooms", ) + change_tracker = FieldTracker(["group_id"]) + @classmethod def get_queryset(cls): """Get a queryset for only Matrix rooms.""" diff --git a/aleksis/apps/matrix/signals.py b/aleksis/apps/matrix/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..0b67a1fa331fdfc963ab0813a208275dcfad6b2c --- /dev/null +++ b/aleksis/apps/matrix/signals.py @@ -0,0 +1,36 @@ +from django.db.models import Q + +from aleksis.apps.matrix.models import MatrixProfile, MatrixRoom +from aleksis.core.models import Group + +from .tasks import sync_room + + +def post_save_matrix_signal(sender, instance, created, **kwargs): + """Sync Matrix room after changing a group/Matrix room/Matrix profile.""" + rooms = [] + if isinstance(instance, Group): + rooms = MatrixRoom.objects.filter(group=instance) + elif isinstance(instance, MatrixRoom) and instance.change_tracker.has_changed("group_id"): + rooms = [instance] + elif isinstance(instance, MatrixProfile) and instance.change_tracker.changed(): + rooms = MatrixRoom.objects.filter( + Q(group__members=instance.person) | Q(group__owners=instance.person) + ).distinct() + + for room in rooms: + sync_room.delay(room.pk) + + +def m2m_changed_matrix_signal(sender, instance, action, reverse, model, pk_set, **kwargs): + """Sync Matrix room after changing group member- and ownerships.""" + if action not in ("post_add", "post_remove", "post_clear"): + return + + if isinstance(instance, Group): + groups = [instance] + else: + groups = Group.objects.filter(Q(members=instance) | Q(owners=instance)).distinct() + + for room in MatrixRoom.objects.filter(group__in=groups): + sync_room.delay(room.pk) diff --git a/aleksis/apps/matrix/tasks.py b/aleksis/apps/matrix/tasks.py index c7346d8c5eb632fd7255e678cd5ca6aeb4c18075..34ccd15eb6cab43b7d43d8959f7abb577b425149 100644 --- a/aleksis/apps/matrix/tasks.py +++ b/aleksis/apps/matrix/tasks.py @@ -1,3 +1,4 @@ +from datetime import timedelta from typing import Sequence from aleksis.apps.matrix.models import MatrixRoom @@ -25,3 +26,11 @@ def provision_group_in_matrix(pk: int): """Provision provided group in Matrix.""" group = Group.objects.get(pk=pk) group._provision_in_matrix() + + +@app.task(run_every=timedelta(days=1)) +def sync_rooms(): + """Synchronise all Matrix rooms.""" + rooms = MatrixRoom.objects.all() + for room in rooms: + sync_room.delay(room.pk) diff --git a/aleksis/apps/matrix/tests/synapse/homeserver.yaml b/aleksis/apps/matrix/tests/synapse/homeserver.yaml index 05da9c0f914e699c1cafdcae0a47f14210443d71..9221c000737b21d86de9e3ea7302e82e53baf7a9 100644 --- a/aleksis/apps/matrix/tests/synapse/homeserver.yaml +++ b/aleksis/apps/matrix/tests/synapse/homeserver.yaml @@ -30,4 +30,6 @@ form_secret: "eYJgrzzEXHsgblxAi3pBmPsNrXrga.OVTKkmb&u64A11V_8axr" signing_key_path: "%path%/synapse/matrix.aleksis.example.org.signing.key" trusted_key_servers: - - server_name: "matrix.org" \ No newline at end of file + - server_name: "matrix.org" + +enable_registration_without_verification: true diff --git a/aleksis/apps/matrix/tests/test_matrix.py b/aleksis/apps/matrix/tests/test_matrix.py index 14fc9f78177f5842600af0884d24b89686fa9387..b1cc47de7e3c77681cce5e219d801818628be2e6 100644 --- a/aleksis/apps/matrix/tests/test_matrix.py +++ b/aleksis/apps/matrix/tests/test_matrix.py @@ -1,7 +1,9 @@ import time from datetime import date +from unittest.mock import call from django.contrib.auth.models import User +from django.db.models import Q import pytest import requests @@ -319,32 +321,6 @@ def test_use_room_sync(matrix_bot_user): from django.test import TransactionTestCase, override_settings -@pytest.mark.usefixtures("celery_worker", "matrix_bot_user") -@override_settings(CELERY_BROKER_URL="memory://localhost//") -@override_settings(HAYSTACK_SIGNAL_PROCESSOR="") -class MatrixCeleryTest(TransactionTestCase): - serialized_rollback = True - - def test_use_room_async(self): - get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org" - - g = Group.objects.create(name="Test Room") - u1 = User.objects.create_user("test1", "test1@example.org", "test1") - - p1 = Person.objects.create(first_name="Test", last_name="Person", user=u1) - - g.members.add(p1) - - r = g.provision_in_matrix(sync=False) - assert isinstance(r, AsyncResult) - - time.sleep(3) - - assert MatrixProfile.objects.all().count() == 1 - assert p1.matrix_profile - assert p1.matrix_profile.matrix_id == "@test1:matrix.aleksis.example.org" - - def test_space_creation(matrix_bot_user): parent_group = Group.objects.create(name="Test Group") child_1 = Group.objects.create(name="Test Group 1") @@ -497,3 +473,202 @@ def test_too_much_invites(matrix_bot_user): room = MatrixRoom.from_group(g) room.sync_profiles() + + +def test_signal_group_changed(matrix_bot_user, mocker): + g = Group.objects.create(name="Test Room") + room = MatrixRoom.from_group(g) + + sync_mock = mocker.patch("aleksis.apps.matrix.tasks.sync_room.delay") + + g.name = "Test Room 2" + g.save() + + sync_mock.assert_called_once_with(room.pk) + + +def test_signal_room_group_changed(matrix_bot_user, mocker): + g = Group.objects.create(name="Test Room") + g2 = Group.objects.create(name="Test Room 2") + room = MatrixRoom.from_group(g) + + sync_mock = mocker.patch("aleksis.apps.matrix.tasks.sync_room.delay") + + room.group = g2 + room.save() + + sync_mock.assert_called_once_with(room.pk) + + +def test_signal_profile_person_changed(matrix_bot_user, mocker): + get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org" + + p = Person.objects.create( + first_name="Test", last_name="Person", user=User.objects.create(username="test") + ) + p2 = Person.objects.create( + first_name="Test 2", last_name="Person 2", user=User.objects.create(username="test2") + ) + p3 = Person.objects.create( + first_name="Test 3", last_name="Person 3", user=User.objects.create(username="test3") + ) + p4 = Person.objects.create( + first_name="Test 4", last_name="Person 4", user=User.objects.create(username="test4") + ) + + g = Group.objects.create(name="Test Room") + g.members.set([p]) + g.owners.set([p3]) + + g2 = Group.objects.create(name="Test Room 2") + g2.members.set([p2]) + g2.owners.set([p4]) + + room = MatrixRoom.from_group(g) + room2 = MatrixRoom.from_group(g2) + + sync_mock = mocker.patch("aleksis.apps.matrix.tasks.sync_room.delay") + + profile = MatrixProfile.from_person(p) + p2.matrix_profile.delete() + profile.person = p2 + profile.save() + + sync_mock.assert_called_with(room2.pk) + + profile2 = MatrixProfile.from_person(p3) + p4.matrix_profile.delete() + profile2.person = p4 + profile2.save() + + sync_mock.assert_called_with(room2.pk) + + +def test_signal_room_members_changed(matrix_bot_user, mocker): + get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org" + + p = Person.objects.create( + first_name="Test", last_name="Person", user=User.objects.create(username="test") + ) + p2 = Person.objects.create( + first_name="Test 2", last_name="Person 2", user=User.objects.create(username="test2") + ) + p3 = Person.objects.create( + first_name="Test 3", last_name="Person 3", user=User.objects.create(username="test3") + ) + p4 = Person.objects.create( + first_name="Test 4", last_name="Person 4", user=User.objects.create(username="test4") + ) + + g = Group.objects.create(name="Test Room") + g.members.set([p]) + + room = MatrixRoom.from_group(g) + + sync_mock = mocker.patch("aleksis.apps.matrix.tasks.sync_room.delay") + + g.members.set([p2, p3]) + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + g.members.add(p4) + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + g.members.remove(p2) + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + g.members.clear() + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + +def test_signal_room_owners_changed(matrix_bot_user, mocker): + get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org" + + p = Person.objects.create( + first_name="Test", last_name="Person", user=User.objects.create(username="test") + ) + p2 = Person.objects.create( + first_name="Test 2", last_name="Person 2", user=User.objects.create(username="test2") + ) + p3 = Person.objects.create( + first_name="Test 3", last_name="Person 3", user=User.objects.create(username="test3") + ) + p4 = Person.objects.create( + first_name="Test 4", last_name="Person 4", user=User.objects.create(username="test4") + ) + + g = Group.objects.create(name="Test Room") + g.owners.set([p]) + + room = MatrixRoom.from_group(g) + + sync_mock = mocker.patch("aleksis.apps.matrix.tasks.sync_room.delay") + + g.owners.set([p2, p3]) + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + g.owners.add(p4) + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + g.owners.remove(p2) + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + g.owners.clear() + + sync_mock.assert_called_with(room.pk) + sync_mock.reset_mock() + + +def test_signal_room_members_changed_reverse(matrix_bot_user, mocker): + get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org" + + p = Person.objects.create( + first_name="Test", last_name="Person", user=User.objects.create(username="test") + ) + + g = Group.objects.create(name="Test Room") + g2 = Group.objects.create(name="Test Room 2") + + room = MatrixRoom.from_group(g) + room2 = MatrixRoom.from_group(g2) + + sync_mock = mocker.patch("aleksis.apps.matrix.tasks.sync_room.delay") + + p.member_of.set([g, g2]) + + sync_mock.assert_has_calls([call(room.pk), call(room2.pk)]) + sync_mock.reset_mock() + + +def test_signal_room_owners_changed_reverse(matrix_bot_user, mocker): + get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org" + + p = Person.objects.create( + first_name="Test", last_name="Person", user=User.objects.create(username="test") + ) + + g = Group.objects.create(name="Test Room") + g2 = Group.objects.create(name="Test Room 2") + + room = MatrixRoom.from_group(g) + room2 = MatrixRoom.from_group(g2) + + sync_mock = mocker.patch("aleksis.apps.matrix.tasks.sync_room.delay") + + p.owner_of.set([g, g2]) + + sync_mock.assert_has_calls([call(room.pk), call(room2.pk)]) + sync_mock.reset_mock() diff --git a/pyproject.toml b/pyproject.toml index ee8788aea2c26328bea39c38990112df25769885..4daf6e42edf922cd0aab8441971305e4bf735678 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ aleksis-core = "^2.7" aleksis-builddeps = "*" matrix-synapse = "^1.49.2" pytest-xprocess = "^0.19.0" +pytest-mock = "^3.7.0" [tool.poetry.plugins."aleksis.app"] matrix = "aleksis.apps.matrix.apps:DefaultConfig"