From 0f47c6a79f68755ac1931eff5048cda67d5fd8d1 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Fri, 15 Apr 2022 17:32:27 +0200 Subject: [PATCH] Register more signals to detect changes that need a Matrix room sync --- aleksis/apps/matrix/apps.py | 10 +- aleksis/apps/matrix/models.py | 6 + aleksis/apps/matrix/signals.py | 33 +++- aleksis/apps/matrix/tests/test_matrix.py | 202 ++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 246 insertions(+), 6 deletions(-) diff --git a/aleksis/apps/matrix/apps.py b/aleksis/apps/matrix/apps.py index ac659eb..f5cf90d 100644 --- a/aleksis/apps/matrix/apps.py +++ b/aleksis/apps/matrix/apps.py @@ -1,4 +1,4 @@ -from django.db.models.signals import post_save +from django.db.models.signals import m2m_changed, post_save from aleksis.core.util.apps import AppConfig @@ -17,6 +17,12 @@ class DefaultConfig(AppConfig): def ready(self): from aleksis.core.models import Group - from .signals import post_save_matrix_signal + 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 b5c41bf..dfbf4af 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 index cf1296f..10db2f5 100644 --- a/aleksis/apps/matrix/signals.py +++ b/aleksis/apps/matrix/signals.py @@ -1,12 +1,39 @@ -from aleksis.apps.matrix.models import MatrixRoom +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.""" + """Sync Matrix room after changing a group/Matrix room/Matrix profile.""" if created: return - for room in MatrixRoom.objects.filter(group=instance): + 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/tests/test_matrix.py b/aleksis/apps/matrix/tests/test_matrix.py index 14fc9f7..7d15ae6 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 @@ -318,7 +320,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="") @@ -497,3 +498,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 80dad38..f085de8 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.18.1" +pytest-mock = "^3.7.0" [tool.poetry.plugins."aleksis.app"] matrix = "aleksis.apps.matrix.apps:DefaultConfig" -- GitLab