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

Add first attempts for syncing groups to rooms

parent 08022828
No related branches found
No related tags found
No related merge requests found
from urllib.parse import urljoin
from aleksis.core.util.core_helpers import get_site_preferences
class MatrixException(Exception):
pass
def build_url(path):
return urljoin(
urljoin(get_site_preferences()["matrix__homeserver"], "_matrix/client/v3/"), path
)
def get_headers():
return {
"Authorization": "Bearer " + get_site_preferences()["matrix__access_token"],
}
# Generated by Django 3.2.10 on 2021-12-27 23:08
import aleksis.core.managers
import django.contrib.sites.managers
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('core', '0031_auto_20211228_0008'),
('sites', '0002_alter_domain_unique'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='MatrixRoom',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('extended_data', models.JSONField(default=dict, editable=False)),
('room_id', models.CharField(max_length=255, unique=True, verbose_name='Room ID')),
('alias', models.CharField(blank=True, max_length=255, unique=True, verbose_name='Alias')),
('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matrix_spaces', to='core.group', verbose_name='Group')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_matrix.matrixroom_set+', to='contenttypes.contenttype')),
('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
],
options={
'verbose_name': 'Matrix room',
'verbose_name_plural': 'Matrix rooms',
},
managers=[
('objects', aleksis.core.managers.PolymorphicCurrentSiteManager()),
],
),
migrations.CreateModel(
name='MatrixProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('extended_data', models.JSONField(default=dict, editable=False)),
('matrix_id', models.CharField(max_length=255, unique=True, verbose_name='Matrix ID')),
('person', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='matrix_profile', to='core.person', verbose_name='Person')),
('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')),
],
options={
'verbose_name': 'Matrix profile',
'verbose_name_plural': 'Matrix profiles',
},
managers=[
('objects', django.contrib.sites.managers.CurrentSiteManager()),
],
),
migrations.CreateModel(
name='MatrixSpace',
fields=[
('matrixroom_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='matrix.matrixroom')),
('children', models.ManyToManyField(blank=True, related_name='parents', to='matrix.MatrixRoom', verbose_name='Child rooms/spaces')),
],
options={
'verbose_name': 'Matrix space',
'verbose_name_plural': 'Matrix spaces',
},
bases=('matrix.matrixroom',),
managers=[
('objects', aleksis.core.managers.PolymorphicCurrentSiteManager()),
],
),
]
import re
from django.db import models from django.db import models
from django.template.defaultfilters import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import requests
from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel
from aleksis.core.models import Group, Person from aleksis.core.models import Group, Person
...@@ -8,7 +13,7 @@ from aleksis.core.models import Group, Person ...@@ -8,7 +13,7 @@ from aleksis.core.models import Group, Person
class MatrixProfile(ExtensibleModel): class MatrixProfile(ExtensibleModel):
"""Model for a Matrix profile.""" """Model for a Matrix profile."""
matrix_id = models.CharField(verbose_name=_("Matrix ID"), unique=True) matrix_id = models.CharField(max_length=255, verbose_name=_("Matrix ID"), unique=True)
person = models.OneToOneField( person = models.OneToOneField(
Person, Person,
on_delete=models.CASCADE, on_delete=models.CASCADE,
...@@ -26,8 +31,8 @@ class MatrixProfile(ExtensibleModel): ...@@ -26,8 +31,8 @@ class MatrixProfile(ExtensibleModel):
class MatrixRoom(ExtensiblePolymorphicModel): class MatrixRoom(ExtensiblePolymorphicModel):
"""Model for a Matrix room.""" """Model for a Matrix room."""
room_id = models.CharField(verbose_name=_("Room ID"), unique=True) room_id = models.CharField(max_length=255, verbose_name=_("Room ID"), unique=True)
alias = models.CharField(verbose_name=_("Alias"), unique=True, blank=True, null=True) alias = models.CharField(max_length=255, verbose_name=_("Alias"), unique=True, blank=True)
group = models.ForeignKey( group = models.ForeignKey(
Group, Group,
on_delete=models.CASCADE, on_delete=models.CASCADE,
...@@ -37,6 +42,56 @@ class MatrixRoom(ExtensiblePolymorphicModel): ...@@ -37,6 +42,56 @@ class MatrixRoom(ExtensiblePolymorphicModel):
related_name="matrix_spaces", related_name="matrix_spaces",
) )
@classmethod
def from_group(self, group: Group):
"""Create a Matrix room from a group."""
from .matrix import MatrixException, build_url, get_headers
try:
room = MatrixRoom.objects.get(group=group)
except MatrixRoom.DoesNotExist:
room = MatrixRoom(group=group)
if room.room_id:
# Existing room, check if still accessible
r = requests.get(
build_url(f"directory/list/room/{room.room_id}"), headers=get_headers()
)
if not r.status_code == requests.codes.ok:
raise MatrixException()
else:
# Room does not exist, create it
alias = slugify(group.short_name or group.name)
r = self._create_group(group.name, alias)
while r.json().get("errcode") == "M_ROOM_IN_USE":
match = re.match(r"^(.*)-(\d+)$", alias)
if match:
# Counter found, increase
prefix = match.group(1)
counter = int(match.group(2)) + 1
alias = f"{prefix}-{counter}"
else:
# Counter not found, add one
alias = f"{alias}-2"
r = self._create_group(group.name, alias)
if r.status_code == requests.codes.ok:
room.room_id = r.json()["room_id"]
room.alias = r.json()["room_alias"]
room.save()
else:
raise MatrixException(r.text)
return room
@classmethod
def _create_group(self, name, alias):
from .matrix import build_url, get_headers
body = {"preset": "private_chat", "name": name, "room_alias_name": alias}
r = requests.post(build_url("createRoom"), headers=get_headers(), json=body)
return r
class Meta: class Meta:
verbose_name = _("Matrix room") verbose_name = _("Matrix room")
verbose_name_plural = _("Matrix rooms") verbose_name_plural = _("Matrix rooms")
......
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from dynamic_preferences.preferences import Section from dynamic_preferences.preferences import Section
from dynamic_preferences.types import StringPreference from dynamic_preferences.types import BooleanPreference, StringPreference
from aleksis.core.registries import site_preferences_registry from aleksis.core.registries import site_preferences_registry
...@@ -22,3 +22,36 @@ class AccessToken(StringPreference): ...@@ -22,3 +22,36 @@ class AccessToken(StringPreference):
name = "access_token" name = "access_token"
verbose_name = _("Access token to access homeserver") verbose_name = _("Access token to access homeserver")
default = "" default = ""
@site_preferences_registry.register
class User(StringPreference):
section = matrix
name = "user"
verbose_name = _("User to access homeserver")
default = ""
@site_preferences_registry.register
class DeviceID(StringPreference):
section = matrix
name = "device_id"
verbose_name = _("Device ID")
default = ""
field_kwargs = {"editable": False}
@site_preferences_registry.register
class DeviceName(StringPreference):
section = matrix
name = "device_name"
verbose_name = _("Device name")
default = "AlekSIS"
@site_preferences_registry.register
class DisambiguateRoomAliases(BooleanPreference):
section = matrix
name = "disambiguate_room_aliases"
verbose_name = _("Disambiguate room aliases")
default = True
from datetime import date
import pytest
import requests
from aleksis.apps.matrix.models import MatrixRoom
from aleksis.core.models import Group, SchoolTerm
from aleksis.core.util.core_helpers import get_site_preferences
pytestmark = pytest.mark.django_db
SERVER_URL = "http://127.0.0.1:8008"
def test_connection(synapse):
assert synapse["listeners"][0]["port"] == 8008
assert requests.get(SERVER_URL).status_code == requests.codes.ok
@pytest.fixture
def matrix_bot_user(synapse):
from aleksis.apps.matrix.matrix import build_url
body = {"username": "aleksis-bot", "password": "test", "auth": {"type": "m.login.dummy"}}
get_site_preferences()["matrix__homeserver"] = SERVER_URL
r = requests.post(build_url("register"), json=body)
print(r.text, build_url("register"))
assert r.status_code == requests.codes.ok
user = r.json()
get_site_preferences()["matrix__user"] = user["user_id"]
get_site_preferences()["matrix__device_id"] = user["device_id"]
get_site_preferences()["matrix__access_token"] = user["access_token"]
yield r.json()
def test_create_room_for_group(matrix_bot_user):
from aleksis.apps.matrix.matrix import build_url, get_headers
g = Group.objects.create(name="Test Room")
assert not MatrixRoom.objects.all().exists()
room = MatrixRoom.from_group(g)
assert ":matrix.aleksis.example.org" in room.room_id
assert room.alias == "#test-room:matrix.aleksis.example.org"
# On second get, it should be the same matrix room
assert MatrixRoom.from_group(g) == room
r = requests.get(build_url(f"rooms/{room.room_id}/aliases"), headers=get_headers())
aliases = r.json()["aliases"]
assert "#test-room:matrix.aleksis.example.org" in aliases
#
def test_create_room_for_group_short_name(matrix_bot_user):
g = Group.objects.create(name="Test Room", short_name="test")
assert not MatrixRoom.objects.all().exists()
room = MatrixRoom.from_group(g)
assert room.alias == "#test:matrix.aleksis.example.org"
def test_room_alias_collision_same_name(matrix_bot_user):
from aleksis.apps.matrix.matrix import MatrixException
g1 = Group.objects.create(name="Test Room")
g2 = Group.objects.create(name="test-room")
g3 = Group.objects.create(name="Test-Room")
g4 = Group.objects.create(name="test room")
room = MatrixRoom.from_group(g1)
assert room.alias == "#test-room:matrix.aleksis.example.org"
room = MatrixRoom.from_group(g2)
assert room.alias == "#test-room-2:matrix.aleksis.example.org"
room = MatrixRoom.from_group(g3)
assert room.alias == "#test-room-3:matrix.aleksis.example.org"
get_site_preferences()["matrix__disambiguate_room_aliases"] = False
with pytest.raises(MatrixException):
MatrixRoom.from_group(g4)
def test_room_alias_collision_school_term(matrix_bot_user):
school_term_a = SchoolTerm.objects.create(
name="Test Term A", date_start=date(2020, 1, 1), date_end=date(2020, 12, 31)
)
school_term_b = SchoolTerm.objects.create(
name="Test Term B", date_start=date(2021, 1, 1), date_end=date(2021, 12, 31)
)
g1 = Group.objects.create(name="Test Room", school_term=school_term_a)
g2 = Group.objects.create(name="Test Room", school_term=school_term_b)
room = MatrixRoom.from_group(g1)
assert room.alias == "#test-room:matrix.aleksis.example.org"
room = MatrixRoom.from_group(g2)
assert room.alias == "#test-room-2:matrix.aleksis.example.org"
import os
import pytest
import yaml
from xprocess import ProcessStarter
@pytest.fixture
def synapse(xprocess, tmp_path):
path = os.path.dirname(__file__)
new_config_filename = os.path.join(tmp_path, "homeserver.yaml")
files_to_replace = [
"homeserver.yaml",
"matrix.aleksis.example.org.log.config",
]
for filename in files_to_replace:
new_filename = os.path.join(tmp_path, filename)
with open(os.path.join(path, "synapse", filename), "r") as read_file:
content = read_file.read()
content = content.replace("%path%", path)
content = content.replace("%tmp_path%", str(tmp_path))
with open(new_filename, "w") as write_file:
write_file.write(content)
with open(new_config_filename, "r") as f:
config = yaml.safe_load(f)
config["server_url"] = "http://127.0.0.1:8008"
class SynapseStarter(ProcessStarter):
# startup pattern
pattern = "SynapseSite starting on 8008"
# command to start process
args = [
"python",
"-m",
"synapse.app.homeserver",
"--enable-registration",
"-c",
new_config_filename,
]
max_read_lines = 400
timeout = 10
xprocess.ensure("synapse", SynapseStarter)
yield config
xprocess.getinfo("synapse").terminate()
...@@ -36,6 +36,8 @@ aleksis-core = "^2.1.dev0" ...@@ -36,6 +36,8 @@ aleksis-core = "^2.1.dev0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
aleksis-builddeps = "*" aleksis-builddeps = "*"
matrix-synapse = "^1.49.2"
pytest-xprocess = "^0.18.1"
[tool.poetry.plugins."aleksis.app"] [tool.poetry.plugins."aleksis.app"]
matrix = "aleksis.apps.matrix.apps:DefaultConfig" matrix = "aleksis.apps.matrix.apps:DefaultConfig"
...@@ -47,3 +49,4 @@ exclude = "/migrations/" ...@@ -47,3 +49,4 @@ exclude = "/migrations/"
[build-system] [build-system]
requires = ["poetry>=1.0"] requires = ["poetry>=1.0"]
build-backend = "poetry.masonry.api" build-backend = "poetry.masonry.api"
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