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/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"