Skip to content
Snippets Groups Projects
Verified Commit f0103251 authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Add support for syncing spaces

parent a9c8b65b
No related branches found
No related tags found
No related merge requests found
Pipeline #50596 canceled
# Generated by Django 3.2.10 on 2022-01-06 15:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0031_auto_20211228_0008'),
('matrix', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='matrixroom',
name='group',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='matrix_room', to='core.group', verbose_name='Group'),
),
]
......@@ -29,17 +29,15 @@ def _use_in_matrix(self):
@Group.property_
def matrix_alias(self) -> Optional[str]:
"""Return the alias of the group's room in Matrix."""
if hasattr(self, "matrix_room"):
return self.matrix_room.alias
return None
rooms = [room for room in self.matrix_rooms.all() if type(room) == MatrixRoom]
return rooms[0].alias if rooms else None
@Group.property_
def matrix_room_id(self) -> Optional[str]:
"""Return the ID of the group's room in Matrix."""
if hasattr(self, "matrix_room"):
return self.matrix_room.room_id
return None
rooms = [room for room in self.matrix_rooms.all() if type(room) == MatrixRoom]
return rooms[0].room_id if rooms else None
Group.add_permission("view_matrixroom", _("Can view matrix room of a group"))
......@@ -55,37 +55,45 @@ class MatrixRoom(ExtensiblePolymorphicModel):
room_id = models.CharField(max_length=255, verbose_name=_("Room ID"), unique=True)
alias = models.CharField(max_length=255, verbose_name=_("Alias"), unique=True, blank=True)
group = models.OneToOneField(
group = models.ForeignKey(
Group,
on_delete=models.CASCADE,
verbose_name=_("Group"),
related_name="matrix_room",
related_name="matrix_rooms",
)
@classmethod
def from_group(self, group: Group):
def get_queryset(cls):
return cls.objects.not_instance_of(MatrixSpace)
@classmethod
def build_alias(cls, group: Group) -> str:
return slugify(group.short_name or group.name)
@classmethod
def from_group(cls, group: Group) -> "MatrixRoom":
"""Create a Matrix room from a group."""
from .matrix import MatrixException, do_matrix_request
try:
room = MatrixRoom.objects.get(group=group)
except MatrixRoom.DoesNotExist:
room = MatrixRoom(group=group)
room = cls.get_queryset().get(group=group)
except cls.DoesNotExist:
room = cls(group=group)
if room.room_id:
# Existing room, check if still accessible
r = do_matrix_request("GET", f"directory/list/room/{room.room_id}")
else:
# Room does not exist, create it
alias = slugify(group.short_name or group.name)
alias = cls.build_alias(group)
profiles_to_invite = list(
self.get_profiles_for_group(group).values_list("matrix_id", flat=True)
cls.get_profiles_for_group(group).values_list("matrix_id", flat=True)
)
alias_found = False
while not alias_found:
try:
r = self._create_room(group.name, alias, profiles_to_invite)
r = cls._create_room(group.name, alias, profiles_to_invite)
alias_found = True
except MatrixException as e:
print(e.args, get_site_preferences()["matrix__disambiguate_room_aliases"])
......@@ -111,10 +119,23 @@ class MatrixRoom(ExtensiblePolymorphicModel):
return room
@classmethod
def _create_room(self, name, alias, invite: List[str]):
def _create_room(
self,
name,
alias,
invite: Optional[List[str]] = None,
creation_content: Optional[dict] = None,
) -> Dict[str, Any]:
from .matrix import do_matrix_request
body = {"preset": "private_chat", "name": name, "room_alias_name": alias, "invite": invite}
body = {"preset": "private_chat", "name": name, "room_alias_name": alias}
if invite:
body["invite"] = invite
if creation_content:
body["creation_content"] = creation_content
r = do_matrix_request("POST", "createRoom", body=body)
return r
......@@ -210,9 +231,18 @@ class MatrixRoom(ExtensiblePolymorphicModel):
user_levels[profile.matrix_id] = 0
self._set_power_levels(user_levels)
def sync_space(self):
if self.group.child_groups.all():
# Do space stuff
space = MatrixSpace.from_group(self.group)
space.sync()
return None
def sync(self):
"""Sync this room."""
self.sync_profiles()
if get_site_preferences()["matrix__use_spaces"]:
self.sync_space()
class Meta:
verbose_name = _("Matrix room")
......@@ -225,6 +255,77 @@ class MatrixSpace(MatrixRoom):
to=MatrixRoom, verbose_name=_("Child rooms/spaces"), blank=True, related_name="parents"
)
@classmethod
def get_queryset(cls):
return cls.objects.instance_of(MatrixSpace)
@classmethod
def build_alias(cls, group: Group) -> str:
"""Build an alias for this space."""
return slugify(group.short_name or group.name) + "-space"
@classmethod
def _create_room(
self,
name,
alias,
invite: Optional[List[str]] = None,
creation_content: Optional[dict] = None,
) -> Dict[str, Any]:
if not creation_content:
creation_content = {}
creation_content["type"] = "m.space"
return super()._create_room(name, alias, invite, creation_content)
@property
def child_spaces(self) -> List[str]:
"""Get all child spaces of this space."""
from aleksis.apps.matrix.matrix import do_matrix_request
r = do_matrix_request("GET", f"rooms/{self.room_id}/state")
return [c["state_key"] for c in r if c["type"] == "m.space.child"]
def _add_child(self, room_id: str):
"""Add a child room to this space."""
r = do_matrix_request(
"PUT",
f"/_matrix/client/v3/rooms/{self.room_id}/state/m.space.child/{room_id}",
body={"via": [get_site_preferences()["matrix__homeserver_ids"]]},
)
return r
def sync_children(self):
"""Sync membership of child spaces and rooms."""
current_children = self.child_spaces
child_spaces = MatrixSpace.get_queryset().filter(
group__in=self.group.child_groups.filter(child_groups__isnull=False)
)
child_rooms = MatrixRoom.get_queryset().filter(
Q(group__in=self.group.child_groups.filter(child_groups__isnull=True))
| Q(group=self.group)
)
child_ids = [m.room_id for m in list(child_spaces) + list(child_rooms)]
missing_ids = set(child_ids).difference(set(current_children))
for missing_id in missing_ids:
self._add_child(missing_id)
def ensure_children(self):
"""Ensure that all child rooms/spaces exist."""
for group in self.group.child_groups.all().prefetch_related("child_groups"):
if group.child_groups.all():
space = MatrixSpace.from_group(group)
space.ensure_children()
else:
group.use_in_matrix(sync=True)
def sync(self):
"""Sync this space."""
self.ensure_children()
self.sync_children()
class Meta:
verbose_name = _("Matrix space")
verbose_name_plural = _("Matrix spaces")
......@@ -63,3 +63,11 @@ class DisambiguateRoomAliases(BooleanPreference):
name = "disambiguate_room_aliases"
verbose_name = _("Disambiguate room aliases")
default = True
@site_preferences_registry.register
class UseSpaces(BooleanPreference):
section = matrix
name = "use_spaces"
verbose_name = _("Use Matrix spaces")
default = True
......@@ -8,7 +8,7 @@ import requests
from celery.result import AsyncResult
from aleksis.apps.matrix.matrix import do_matrix_request
from aleksis.apps.matrix.models import MatrixProfile, MatrixRoom
from aleksis.apps.matrix.models import MatrixProfile, MatrixRoom, MatrixSpace
from aleksis.core.models import Group, Person, SchoolTerm
from aleksis.core.util.core_helpers import get_site_preferences
......@@ -282,3 +282,47 @@ class MatrixCeleryTest(TransactionTestCase):
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")
child_2 = Group.objects.create(name="Test Group 2")
child_3 = Group.objects.create(name="Test Group 3")
parent_group.child_groups.set([child_1, child_2, child_3])
parent_group.use_in_matrix(sync=True)
get_site_preferences()["matrix__use_spaces"] = True
space = MatrixSpace.from_group(parent_group)
r = do_matrix_request("GET", f"rooms/{space.room_id}/state")
events = {x["type"]: x for x in r}
assert events["m.room.create"]["content"]["type"] == "m.space"
space.ensure_children()
rooms = MatrixRoom.get_queryset().values_list("group_id", flat=True)
assert child_1.pk in rooms
assert child_2.pk in rooms
assert child_3.pk in rooms
space.sync_children()
r = do_matrix_request("GET", f"rooms/{space.room_id}/state")
interesting_events = [x["state_key"] for x in r if x["type"] == "m.space.child"]
assert len(interesting_events) == 4
rooms = list(
MatrixRoom.get_queryset()
.filter(group__in=[parent_group, child_1, child_2, child_3])
.values_list("room_id", flat=True)
)
assert len(rooms) == 4
assert set(interesting_events) == set(rooms)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment