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