diff --git a/aleksis/apps/matrix/models.py b/aleksis/apps/matrix/models.py index c385e715af0fe16f1cd63db194cd2a414a28b9dd..78c9fd99c609c86961688ce879fd33fc99d34f4d 100644 --- a/aleksis/apps/matrix/models.py +++ b/aleksis/apps/matrix/models.py @@ -1,6 +1,8 @@ import re +from typing import Any, Dict, Optional, Union from django.db import models +from django.db.models import Q from django.template.defaultfilters import slugify from django.utils.translation import gettext_lazy as _ @@ -8,6 +10,7 @@ import requests from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel from aleksis.core.models import Group, Person +from aleksis.core.util.core_helpers import get_site_preferences class MatrixProfile(ExtensibleModel): @@ -23,6 +26,26 @@ class MatrixProfile(ExtensibleModel): related_name="matrix_profile", ) + @classmethod + def build_matrix_id(cls, username, homeserver: Optional[str] = None): + homeserver = homeserver or get_site_preferences()["matrix__homeserver_ids"] + return f"@{username}:{homeserver}" + + @classmethod + def from_person(cls, person: Person, commit: bool = False) -> Union["MatrixProfile", None]: + if hasattr(person, "matrix_profile"): + return person.matrix_profile + if not person.user: + raise ValueError("Person must have a user.") + if not get_site_preferences()["matrix__homeserver_ids"]: + return None + new_profile = MatrixProfile( + matrix_id=cls.build_matrix_id(person.user.username), person=person + ) + if commit: + new_profile.save() + return new_profile + class Meta: verbose_name = _("Matrix profile") verbose_name_plural = _("Matrix profiles") @@ -92,6 +115,86 @@ class MatrixRoom(ExtensiblePolymorphicModel): return r + @property + def power_levels(self) -> Dict[str, int]: + """Return the power levels for this room.""" + from aleksis.apps.matrix.matrix import MatrixException, build_url, get_headers + + r = requests.get(build_url(f"rooms/{self.room_id}/state"), headers=get_headers()) + if r.status_code != requests.codes.ok: + raise MatrixException(r.text) + + event = list(filter(lambda x: x["type"] == "m.room.power_levels", r.json())) + user_levels = event[0]["content"]["users"] + + return user_levels + + def _invite(self, profile: MatrixProfile) -> Dict[str, Any]: + """Invite a user to this room.""" + from aleksis.apps.matrix.matrix import MatrixException, build_url, get_headers + + r = requests.post( + build_url(f"rooms/{self.room_id}/invite"), + headers=get_headers(), + json={"user_id": profile.matrix_id}, + ) + if not r.status_code == requests.codes.ok: + raise MatrixException(r.text) + return r.json() + + def _set_power_levels(self, power_levels: Dict[str, int]) -> Dict[str, Any]: + """Set the power levels for this room.""" + from aleksis.apps.matrix.matrix import MatrixException, build_url, get_headers + + r = requests.put( + build_url(f"rooms/{self.room_id}/state/m.room.power_levels/"), + headers=get_headers(), + json={"users": power_levels}, + ) + print(r.text, r.status_code) + if not r.status_code == requests.codes.ok: + raise MatrixException(r.text) + return r.json() + + def sync_profiles(self): + """Sync profiles for this room.""" + existing_profiles = MatrixProfile.objects.filter( + Q(person__member_of=self.group) | Q(person__owner_of=self.group) + ) + profiles_to_create = [] + for person in ( + Person.objects.filter(user__isnull=False) + .filter(Q(member_of=self.group) | Q(owner_of=self.group)) + .exclude(matrix_profile__in=existing_profiles) + .distinct() + ): + new_profile = MatrixProfile.from_person(person) + if new_profile: + profiles_to_create.append(new_profile) + MatrixProfile.objects.bulk_create(profiles_to_create) + + all_profiles = MatrixProfile.objects.filter( + Q(person__in=self.group.members.all()) | Q(person__in=self.group.owners.all()) + ) + user_levels = self.power_levels + + # Invite all users who are not in the room yet + for profile in all_profiles: + if profile.matrix_id not in user_levels: + # Now invite + self._invite(profile) + + # Set power levels for all users + # Mod = 50 = Owners + # User = 0 = Members + user_levels = self.power_levels + for profile in all_profiles: + if profile.person in self.group.owners.all(): + user_levels[profile.matrix_id] = 50 + elif profile.person in self.group.members.all(): + user_levels[profile.matrix_id] = 0 + self._set_power_levels(user_levels) + class Meta: verbose_name = _("Matrix room") verbose_name_plural = _("Matrix rooms") diff --git a/aleksis/apps/matrix/preferences.py b/aleksis/apps/matrix/preferences.py index 5b697bcdb8bc8433fd8e022689aa99c7026eee57..124c42f7ecd97d6e919d4d0b6e213726c49819da 100644 --- a/aleksis/apps/matrix/preferences.py +++ b/aleksis/apps/matrix/preferences.py @@ -16,6 +16,15 @@ class Homeserver(StringPreference): default = "" +@site_preferences_registry.register +class HomeserverForIDs(StringPreference): + section = matrix + name = "homeserver_ids" + verbose_name = _("Name of Matrix homeserver used for auto-generating Matrix IDs") + help_text = _("Leave empty to not create Matrix IDs automatically") + default = "" + + @site_preferences_registry.register class AccessToken(StringPreference): section = matrix diff --git a/aleksis/apps/matrix/tests/test_matrix.py b/aleksis/apps/matrix/tests/test_matrix.py index 23d5e08e30f0b8c6b50eaec366fb4cb23b679973..00a8efdf75c084c0a1ec993a43335917bd9d1056 100644 --- a/aleksis/apps/matrix/tests/test_matrix.py +++ b/aleksis/apps/matrix/tests/test_matrix.py @@ -1,10 +1,12 @@ from datetime import date +from django.contrib.auth.models import User + import pytest import requests -from aleksis.apps.matrix.models import MatrixRoom -from aleksis.core.models import Group, SchoolTerm +from aleksis.apps.matrix.models import MatrixProfile, MatrixRoom +from aleksis.core.models import Group, Person, SchoolTerm from aleksis.core.util.core_helpers import get_site_preferences pytestmark = pytest.mark.django_db @@ -105,3 +107,117 @@ def test_room_alias_collision_school_term(matrix_bot_user): room = MatrixRoom.from_group(g2) assert room.alias == "#test-room-2:matrix.aleksis.example.org" + + +def test_sync_room_members(matrix_bot_user): + from aleksis.apps.matrix.matrix import build_url, get_headers + + 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") + u2 = User.objects.create_user("test2", "test2@example.org", "test2") + u3 = User.objects.create_user("test3", "test3@example.org", "test3") + u4 = User.objects.create_user("test4", "test4@example.org", "test4") + u5 = User.objects.create_user("test5", "test5@example.org", "test5") + + p1 = Person.objects.create(first_name="Test", last_name="Person", user=u1) + p2 = Person.objects.create(first_name="Test 2", last_name="Person", user=u2) + p3 = Person.objects.create(first_name="Test 3", last_name="Person", user=u3) + p4 = Person.objects.create(first_name="Test 4", last_name="Person", user=u4) + p5 = Person.objects.create(first_name="Test 5", last_name="Person", user=u5) + + g.members.set([p1, p2, p3]) + g.owners.set([p4, p5]) + + room = MatrixRoom.from_group(g) + room.sync_profiles() + + assert MatrixProfile.objects.all().count() == 5 + assert p1.matrix_profile + assert p1.matrix_profile.matrix_id == "@test1:matrix.aleksis.example.org" + assert p2.matrix_profile + assert p2.matrix_profile.matrix_id == "@test2:matrix.aleksis.example.org" + assert p3.matrix_profile + assert p3.matrix_profile.matrix_id == "@test3:matrix.aleksis.example.org" + assert p4.matrix_profile + assert p4.matrix_profile.matrix_id == "@test4:matrix.aleksis.example.org" + assert p5.matrix_profile + assert p5.matrix_profile.matrix_id == "@test5:matrix.aleksis.example.org" + + # Check members + r = requests.get( + build_url(f"rooms/{room.room_id}/members"), + headers=get_headers(), + json={"membership": ["join", "invite"]}, + ) + assert r.status_code == requests.codes.ok + + matrix_ids = [x["state_key"] for x in r.json()["chunk"]] + assert p1.matrix_profile.matrix_id in matrix_ids + assert p2.matrix_profile.matrix_id in matrix_ids + assert p3.matrix_profile.matrix_id in matrix_ids + assert p4.matrix_profile.matrix_id in matrix_ids + assert p5.matrix_profile.matrix_id in matrix_ids + + # Get power levels + r = requests.get(build_url(f"rooms/{room.room_id}/state"), headers=get_headers()) + assert r.status_code == requests.codes.ok + for event in r.json(): + if not event["type"] == "m.room.power_levels": + continue + current_power_levels = event["content"]["users"] + + assert current_power_levels[p1.matrix_profile.matrix_id] == 0 + assert current_power_levels[p2.matrix_profile.matrix_id] == 0 + assert current_power_levels[p3.matrix_profile.matrix_id] == 0 + assert current_power_levels[p4.matrix_profile.matrix_id] == 50 + assert current_power_levels[p5.matrix_profile.matrix_id] == 50 + + break + + +def test_sync_room_members_without_user(matrix_bot_user): + + 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) + p2 = Person.objects.create(first_name="Test 2", last_name="Person") + + g.members.set([p1, p2]) + + room = MatrixRoom.from_group(g) + room.sync_profiles() + + assert MatrixProfile.objects.all().count() == 1 + assert p1.matrix_profile + assert p1.matrix_profile.matrix_id == "@test1:matrix.aleksis.example.org" + assert not hasattr(p2, "matrix_profile") + + +# test no homeserver for ids + + +def test_sync_room_members_without_homeserver(matrix_bot_user): + + get_site_preferences()["matrix__homeserver_ids"] = "" + + 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) + p2 = Person.objects.create(first_name="Test 2", last_name="Person") + + MatrixProfile.objects.create(person=p2, matrix_id="@test2:matrix.aleksis.example.org") + g.members.set([p1, p2]) + + room = MatrixRoom.from_group(g) + room.sync_profiles() + + assert MatrixProfile.objects.all().count() == 1 + assert not hasattr(p1, "matrix_profile") + assert p2.matrix_profile + assert p2.matrix_profile.matrix_id == "@test2:matrix.aleksis.example.org"