diff --git a/README.rst b/README.rst
index fddf3e7ceaece907b6e3fb9876db570db31c4450..c2099c66cf1e5948769e12a2cc125effc9734c89 100644
--- a/README.rst
+++ b/README.rst
@@ -16,7 +16,7 @@ Licence
 
 ::
 
-  Copyright © 2021 Dominik George <dominik.george@teckids.org>
+  Copyright © 2021, 2022 Jonathan Weth <dev@jonathanweth.de>
 
   Licenced under the EUPL, version 1.2 or later
 
diff --git a/SETUP.rst b/SETUP.rst
deleted file mode 100644
index a54bc5be6fa8ca91b693d706b830bed4c617020f..0000000000000000000000000000000000000000
--- a/SETUP.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Setup Matrix sync
------------------
-
-* Register a bot user with a Matrix ID and note down returned access token
-  and Matrix ID.
-* Go to `Admin → Configuration → Matrix` and fill in all values.
\ No newline at end of file
diff --git a/aleksis/apps/matrix/__init__.py b/aleksis/apps/matrix/__init__.py
index 7da74f18ff0c0c063970d5f77d81f41841b0e07a..47838dca46c861480633d45fb511523ce8198bfe 100644
--- a/aleksis/apps/matrix/__init__.py
+++ b/aleksis/apps/matrix/__init__.py
@@ -4,5 +4,3 @@ try:
     __version__ = pkg_resources.get_distribution("AlekSIS-App-Matrix").version
 except Exception:
     __version__ = "unknown"
-
-default_app_config = "aleksis.apps.matrix.apps.DefaultConfig"
diff --git a/aleksis/apps/matrix/apps.py b/aleksis/apps/matrix/apps.py
index 64749ecbac295255e4b89599dfad37ae1dc0a84b..1c60d9e509f7e8e9af819f274130b8001a473be1 100644
--- a/aleksis/apps/matrix/apps.py
+++ b/aleksis/apps/matrix/apps.py
@@ -10,4 +10,4 @@ class DefaultConfig(AppConfig):
         "Repository": "https://edugit.org/AlekSIS/onboarding//AlekSIS-App-Matrix",
     }
     licence = "EUPL-1.2+"
-    copyright_info = (([2021], "Dominik George", "dominik.george@teckids.org"),)
+    copyright_info = (([2021, 2022], "Jonathan Weth", "dev@jonathanweth.de"),)
diff --git a/aleksis/apps/matrix/filters.py b/aleksis/apps/matrix/filters.py
index ac7c9184431babd64be2c969e8737a35ca2df0c9..fe145bec6db2c98d03120f959eb8c10de3f19ae4 100644
--- a/aleksis/apps/matrix/filters.py
+++ b/aleksis/apps/matrix/filters.py
@@ -2,4 +2,4 @@ from aleksis.core.filters import GroupFilter
 
 
 class GroupMatrixRoomFilter(GroupFilter):
-    pass
+    """Custom filter for groups on Matrix room overview."""
diff --git a/aleksis/apps/matrix/forms.py b/aleksis/apps/matrix/forms.py
index e194a7433b3c1674dcb72e140b0708c953f4ba21..4cf2f12a27bcd559efb6af7f8dc44f1f35c78009 100644
--- a/aleksis/apps/matrix/forms.py
+++ b/aleksis/apps/matrix/forms.py
@@ -1,17 +1,17 @@
 from django.utils.translation import gettext as _
 
-from aleksis.apps.matrix.tasks import use_groups_in_matrix
+from aleksis.apps.matrix.tasks import provision_groups_in_matrix
 from aleksis.core.forms import ActionForm
 
 
-def use_in_matrix_action(modeladmin, request, queryset):
-    """Use selected groups in Matrix."""
-    use_groups_in_matrix.delay(list(queryset.values_list("pk", flat=True)))
+def provision_in_matrix_action(modeladmin, request, queryset):
+    """Provision selected groups in Matrix."""
+    provision_groups_in_matrix.delay(list(queryset.values_list("pk", flat=True)))
 
 
-use_in_matrix_action.short_description = _("Use in Matrix")
+provision_in_matrix_action.short_description = _("Provision in Matrix")
 
 
 class GroupMatrixRoomActionForm(ActionForm):
     def get_actions(self):
-        return [use_in_matrix_action]
+        return [provision_in_matrix_action]
diff --git a/aleksis/apps/matrix/migrations/0001_initial.py b/aleksis/apps/matrix/migrations/0001_initial.py
index ba8190a05649f9e62ac6bccad1f268be6eac83d1..8d4865efc32d897c22c9d78ba1c1ed5c2fef43b1 100644
--- a/aleksis/apps/matrix/migrations/0001_initial.py
+++ b/aleksis/apps/matrix/migrations/0001_initial.py
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
             options={
                 'verbose_name': 'Matrix room',
                 'verbose_name_plural': 'Matrix rooms',
-                'permissions': (('use_group_in_matrix', 'Can use group in Matrix'),),
+                'permissions': (('provision_group_in_matrix', 'Can provision group in Matrix'),),
             },
             managers=[
                 ('objects', aleksis.core.managers.PolymorphicCurrentSiteManager()),
diff --git a/aleksis/apps/matrix/model_extensions.py b/aleksis/apps/matrix/model_extensions.py
index ab620422e697417717caa69f7d7519761abdfdbb..4e0472b2a75acaea8486a95e10982815ef61619f 100644
--- a/aleksis/apps/matrix/model_extensions.py
+++ b/aleksis/apps/matrix/model_extensions.py
@@ -5,21 +5,21 @@ from django.utils.translation import gettext_lazy as _
 from celery.result import AsyncResult
 
 from aleksis.apps.matrix.models import MatrixRoom
-from aleksis.apps.matrix.tasks import use_group_in_matrix
+from aleksis.apps.matrix.tasks import provision_group_in_matrix
 from aleksis.core.models import Group
 
 
 @Group.method
-def use_in_matrix(self, sync=False) -> Union[MatrixRoom, AsyncResult]:
+def provision_in_matrix(self, sync: bool = False) -> Union[MatrixRoom, AsyncResult]:
     """Create and sync a room for this group in Matrix."""
     if sync:
-        return self._use_in_matrix()
+        return self._provision_in_matrix()
     else:
-        return use_group_in_matrix.delay(self.pk)
+        return provision_group_in_matrix.delay(self.pk)
 
 
 @Group.method
-def _use_in_matrix(self):
+def _provision_in_matrix(self) -> MatrixRoom:
     """Create and sync a room for this group in Matrix."""
     room = MatrixRoom.from_group(self)
     room.sync()
diff --git a/aleksis/apps/matrix/models.py b/aleksis/apps/matrix/models.py
index 9270c6151c570d8345828a470c1f941fe7b25a2e..4ab8df110397c418be57e9ff5bb602b4ceb7fa98 100644
--- a/aleksis/apps/matrix/models.py
+++ b/aleksis/apps/matrix/models.py
@@ -1,16 +1,17 @@
 import re
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Optional, Union
 
 from django.db import models
-from django.db.models import Q
+from django.db.models import Q, QuerySet
 from django.template.defaultfilters import slugify
 from django.utils.translation import gettext_lazy as _
 
-from aleksis.apps.matrix.matrix import do_matrix_request
 from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel
 from aleksis.core.models import Group, Person
 from aleksis.core.util.core_helpers import get_site_preferences
 
+from .util.matrix import MatrixException, do_matrix_request
+
 
 class MatrixProfile(ExtensibleModel):
     """Model for a Matrix profile."""
@@ -26,12 +27,14 @@ class MatrixProfile(ExtensibleModel):
     )
 
     @classmethod
-    def build_matrix_id(cls, username, homeserver: Optional[str] = None):
+    def build_matrix_id(cls, username: str, homeserver: Optional[str] = None) -> str:
+        """Build a Matrix ID from a username."""
         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]:
+        """Get or create a Matrix profile from a person."""
         if hasattr(person, "matrix_profile"):
             return person.matrix_profile
         if not person.user:
@@ -64,17 +67,17 @@ class MatrixRoom(ExtensiblePolymorphicModel):
 
     @classmethod
     def get_queryset(cls):
+        """Get a queryset for only Matrix rooms."""
         return cls.objects.not_instance_of(MatrixSpace)
 
     @classmethod
     def build_alias(cls, group: Group) -> str:
+        """Build a room alias from a group."""
         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
-
+        """Get or create a Matrix room from a group."""
         try:
             room = cls.get_queryset().get(group=group)
         except cls.DoesNotExist:
@@ -96,12 +99,11 @@ class MatrixRoom(ExtensiblePolymorphicModel):
                     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"])
                     if (
                         not get_site_preferences()["matrix__disambiguate_room_aliases"]
                         or e.args[0].get("errcode") != "M_ROOM_IN_USE"
                     ):
-                        raise MatrixException(*e.args)
+                        raise
 
                 match = re.match(r"^(.*)-(\d+)$", alias)
                 if match:
@@ -121,13 +123,12 @@ class MatrixRoom(ExtensiblePolymorphicModel):
     @classmethod
     def _create_room(
         self,
-        name,
-        alias,
-        invite: Optional[List[str]] = None,
+        name: str,
+        alias: str,
+        invite: Optional[list[str]] = None,
         creation_content: Optional[dict] = None,
-    ) -> Dict[str, Any]:
-        from .matrix import do_matrix_request
-
+    ) -> dict[str, Any]:
+        """Create a Matrix room."""
         body = {"preset": "private_chat", "name": name, "room_alias_name": alias}
 
         if invite:
@@ -140,11 +141,8 @@ class MatrixRoom(ExtensiblePolymorphicModel):
 
         return r
 
-    @property
-    def power_levels(self) -> Dict[str, int]:
+    def get_power_levels(self) -> dict[str, int]:
         """Return the power levels for this room."""
-        from aleksis.apps.matrix.matrix import do_matrix_request
-
         r = do_matrix_request("GET", f"rooms/{self.room_id}/state")
 
         event = list(filter(lambda x: x["type"] == "m.room.power_levels", r))
@@ -152,19 +150,16 @@ class MatrixRoom(ExtensiblePolymorphicModel):
 
         return user_levels
 
-    @property
-    def members(self) -> List[str]:
-        from aleksis.apps.matrix.matrix import do_matrix_request
+    def get_members(self) -> list[str]:
+        """Get all members of this room."""
+        r = do_matrix_request("GET", f"rooms/{self.room_id}/members")
+        return [
+            m["state_key"]
+            for m in filter(lambda c: c["content"]["membership"] in ("join", "invite"), r["chunk"])
+        ]
 
-        r = do_matrix_request(
-            "GET", f"rooms/{self.room_id}/members", body={"membership": ["join", "invite"]}
-        )
-        return [m["state_key"] for m in r["chunk"]]
-
-    def _invite(self, profile: MatrixProfile) -> Dict[str, Any]:
+    def _invite(self, profile: MatrixProfile) -> dict[str, Any]:
         """Invite a user to this room."""
-        from aleksis.apps.matrix.matrix import do_matrix_request
-
         r = do_matrix_request(
             "POST",
             f"rooms/{self.room_id}/invite",
@@ -172,7 +167,7 @@ class MatrixRoom(ExtensiblePolymorphicModel):
         )
         return r
 
-    def _set_power_levels(self, power_levels: Dict[str, int]) -> Dict[str, Any]:
+    def _set_power_levels(self, power_levels: dict[str, int]) -> dict[str, Any]:
         """Set the power levels for this room."""
         r = do_matrix_request(
             "PUT",
@@ -182,7 +177,7 @@ class MatrixRoom(ExtensiblePolymorphicModel):
         return r
 
     @classmethod
-    def get_profiles_for_group(cls, group: Group):
+    def get_profiles_for_group(cls, group: Group) -> QuerySet:
         """Get all profile objects for the members/owners of a group."""
         existing_profiles = MatrixProfile.objects.filter(
             Q(person__member_of=group) | Q(person__owner_of=group)
@@ -201,18 +196,18 @@ class MatrixRoom(ExtensiblePolymorphicModel):
 
         all_profiles = MatrixProfile.objects.filter(
             Q(person__in=group.members.all()) | Q(person__in=group.owners.all())
-        )
+        ).distinct()
 
         return all_profiles
 
-    def get_profiles(self):
+    def get_profiles(self) -> QuerySet:
         """Get all profile objects for the members/owners of this group."""
         return self.get_profiles_for_group(self.group)
 
     def sync_profiles(self):
         """Sync profiles for this room."""
         all_profiles = self.get_profiles()
-        members = self.members
+        members = self.get_members()
 
         # Invite all users who are not in the room yet
         for profile in all_profiles:
@@ -221,17 +216,23 @@ class MatrixRoom(ExtensiblePolymorphicModel):
                 self._invite(profile)
 
         # Set power levels for all users
-        # Mod = 50 = Owners
-        # User = 0 = Members
-        user_levels = self.power_levels
+        user_levels = self.get_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
+                power_level = get_site_preferences()["matrix__power_level_for_owners"]
+            else:
+                power_level = get_site_preferences()["matrix__power_level_for_members"]
+
+            if (
+                profile.matrix_id not in user_levels
+                or user_levels[profile.matrix_id] < power_level
+                or get_site_preferences()["matrix__reduce_power_levels"]
+            ):
+                user_levels[profile.matrix_id] = power_level
         self._set_power_levels(user_levels)
 
     def sync_space(self):
+        """Sync the space for this room."""
         if self.group.child_groups.all():
             # Do space stuff
             space = MatrixSpace.from_group(self.group)
@@ -247,7 +248,7 @@ class MatrixRoom(ExtensiblePolymorphicModel):
     class Meta:
         verbose_name = _("Matrix room")
         verbose_name_plural = _("Matrix rooms")
-        permissions = (("use_group_in_matrix", "Can use group in Matrix"),)
+        permissions = (("provision_group_in_matrix", "Can provision group in Matrix"),)
 
 
 class MatrixSpace(MatrixRoom):
@@ -257,6 +258,7 @@ class MatrixSpace(MatrixRoom):
 
     @classmethod
     def get_queryset(cls):
+        """Get a queryset with only Matrix spaces."""
         return cls.objects.instance_of(MatrixSpace)
 
     @classmethod
@@ -267,25 +269,23 @@ class MatrixSpace(MatrixRoom):
     @classmethod
     def _create_room(
         self,
-        name,
-        alias,
-        invite: Optional[List[str]] = None,
+        name: str,
+        alias: str,
+        invite: Optional[list[str]] = None,
         creation_content: Optional[dict] = None,
-    ) -> Dict[str, Any]:
+    ) -> dict[str, Any]:
+        """Create a Matrix space."""
         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
-
+    def get_children(self) -> list[str]:
+        """Get all children (rooms/spaces) of this space."""
         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):
+    def _add_child(self, room_id: str) -> dict[str, Any]:
         """Add a child room to this space."""
         r = do_matrix_request(
             "PUT",
@@ -296,16 +296,22 @@ class MatrixSpace(MatrixRoom):
 
     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)
+        current_children = self.get_children()
+        child_spaces = (
+            MatrixSpace.get_queryset()
+            .filter(group__in=self.group.child_groups.filter(child_groups__isnull=False))
+            .values_list("room_id", flat=True)
         )
-        child_rooms = MatrixRoom.get_queryset().filter(
-            Q(group__in=self.group.child_groups.filter(child_groups__isnull=True))
-            | Q(group=self.group)
+        child_rooms = (
+            MatrixRoom.get_queryset()
+            .filter(
+                Q(group__in=self.group.child_groups.filter(child_groups__isnull=True))
+                | Q(group=self.group)
+            )
+            .values_list("room_id", flat=True)
         )
 
-        child_ids = [m.room_id for m in list(child_spaces) + list(child_rooms)]
+        child_ids = list(child_spaces) + list(child_rooms)
 
         missing_ids = set(child_ids).difference(set(current_children))
 
@@ -315,7 +321,7 @@ class MatrixSpace(MatrixRoom):
     def ensure_children(self):
         """Ensure that all child rooms/spaces exist."""
         for group in self.group.child_groups.all().prefetch_related("child_groups"):
-            group.use_in_matrix(sync=True)
+            group.provision_in_matrix(sync=True)
             if group.child_groups.all():
                 space = MatrixSpace.from_group(group)
                 space.ensure_children()
diff --git a/aleksis/apps/matrix/preferences.py b/aleksis/apps/matrix/preferences.py
index f1d43e52b2115c2f85952410dfa87e2ec9407520..dce980e06509c98ec9d0f2d7f36769fc73509129 100644
--- a/aleksis/apps/matrix/preferences.py
+++ b/aleksis/apps/matrix/preferences.py
@@ -1,7 +1,7 @@
 from django.utils.translation import gettext as _
 
 from dynamic_preferences.preferences import Section
-from dynamic_preferences.types import BooleanPreference, StringPreference
+from dynamic_preferences.types import BooleanPreference, IntegerPreference, StringPreference
 
 from aleksis.core.registries import site_preferences_registry
 
@@ -14,6 +14,10 @@ class Homeserver(StringPreference):
     name = "homeserver"
     verbose_name = _("URL of Matrix homeserver")
     default = ""
+    help_text = _(
+        "URL of the Matrix homeserver on which groups and "
+        "spaces should be created (e. g. https://matrix.org)"
+    )
 
 
 @site_preferences_registry.register
@@ -31,43 +35,53 @@ class AccessToken(StringPreference):
     name = "access_token"
     verbose_name = _("Access token to access homeserver")
     default = ""
+    help_text = _(
+        "This has to be the access token of a suitable bot user. It is used for all actions."
+    )
 
 
 @site_preferences_registry.register
-class User(StringPreference):
+class DisambiguateRoomAliases(BooleanPreference):
     section = matrix
-    name = "user"
-    verbose_name = _("User to access homeserver")
-    default = ""
+    name = "disambiguate_room_aliases"
+    verbose_name = _("Disambiguate room aliases")
+    default = True
+    help_text = _("Suffix room aliases with ascending numbers to avoid name clashes")
 
 
 @site_preferences_registry.register
-class DeviceID(StringPreference):
+class UseSpaces(BooleanPreference):
     section = matrix
-    name = "device_id"
-    verbose_name = _("Device ID")
-    default = ""
+    name = "use_spaces"
+    verbose_name = _("Use Matrix spaces")
+    default = True
+    help_text = _("This activates the creation and management of Matrix spaces.")
 
 
 @site_preferences_registry.register
-class DeviceName(StringPreference):
+class ReducePowerLevels(BooleanPreference):
     section = matrix
-    name = "device_name"
-    verbose_name = _("Device name")
-    default = "AlekSIS"
+    name = "reduce_power_levels"
+    verbose_name = _("Reduce existing power levels")
+    default = False
+    help_text = _("Reduce power levels of existing members to the level suggested by AlekSIS.")
 
 
 @site_preferences_registry.register
-class DisambiguateRoomAliases(BooleanPreference):
+class PowerLevelForOwners(IntegerPreference):
     section = matrix
-    name = "disambiguate_room_aliases"
-    verbose_name = _("Disambiguate room aliases")
-    default = True
+    name = "power_level_for_owners"
+    verbose_name = _("Power level for owners")
+    default = 50
+    required = True
+    help_text = _("This power level will be set for all owners of a group.")
 
 
 @site_preferences_registry.register
-class UseSpaces(BooleanPreference):
+class PowerLevelForMembers(IntegerPreference):
     section = matrix
-    name = "use_spaces"
-    verbose_name = _("Use Matrix spaces")
-    default = True
+    name = "power_level_for_members"
+    verbose_name = _("Power level for members")
+    default = 0
+    required = True
+    help_text = _("This power level will be set for all members of a group.")
diff --git a/aleksis/apps/matrix/rules.py b/aleksis/apps/matrix/rules.py
index 4628e35ad5a32880211163731d14459960336c81..7617732cf5af37f924c493b5f902ca2c572eb679 100644
--- a/aleksis/apps/matrix/rules.py
+++ b/aleksis/apps/matrix/rules.py
@@ -15,10 +15,10 @@ view_matrix_room_predicate = view_group_predicate & (
 )
 rules.add_perm("matrix.view_matrixroom_rule", view_matrix_room_predicate)
 
-use_room_for_matrix_predicate = view_matrix_room_predicate & (
-    has_global_perm("matrix.use_group_in_matrix")
+provision_room_for_matrix_predicate = view_matrix_room_predicate & (
+    has_global_perm("matrix.provision_group_in_matrix")
 )
-rules.add_perm("matrix.use_group_in_matrix_rule", use_room_for_matrix_predicate)
+rules.add_perm("matrix.provision_group_in_matrix_rule", provision_room_for_matrix_predicate)
 
 show_menu_predicate = view_matrix_rooms_predicate
 rules.add_perm("matrix.show_menu_rule", show_menu_predicate)
diff --git a/aleksis/apps/matrix/tasks.py b/aleksis/apps/matrix/tasks.py
index 20f8d5750efd2bb1fdba97b32afd0ec26345b4f6..c7346d8c5eb632fd7255e678cd5ca6aeb4c18075 100644
--- a/aleksis/apps/matrix/tasks.py
+++ b/aleksis/apps/matrix/tasks.py
@@ -7,18 +7,21 @@ from aleksis.core.models import Group
 
 @app.task
 def sync_room(pk: int):
+    """Synchronise a Matrix room."""
     room = MatrixRoom.objects.get(pk=pk)
     room.sync()
 
 
 @app.task
-def use_groups_in_matrix(pks: Sequence[int]):
+def provision_groups_in_matrix(pks: Sequence[int]):
+    """Provision provided groups in Matrix."""
     groups = Group.objects.filter(pk__in=pks)
     for group in groups:
-        group._use_in_matrix()
+        group._provision_in_matrix()
 
 
 @app.task
-def use_group_in_matrix(pk: int):
+def provision_group_in_matrix(pk: int):
+    """Provision provided group in Matrix."""
     group = Group.objects.get(pk=pk)
-    group._use_in_matrix()
+    group._provision_in_matrix()
diff --git a/aleksis/apps/matrix/tests/conftest.py b/aleksis/apps/matrix/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..eadfed813a9ec1b1bfe9b5e4b21374d70cd8992b
--- /dev/null
+++ b/aleksis/apps/matrix/tests/conftest.py
@@ -0,0 +1,55 @@
+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()
diff --git a/synapse/homeserver.yaml b/aleksis/apps/matrix/tests/synapse/homeserver.yaml
similarity index 100%
rename from synapse/homeserver.yaml
rename to aleksis/apps/matrix/tests/synapse/homeserver.yaml
diff --git a/synapse/matrix.aleksis.example.org.log.config b/aleksis/apps/matrix/tests/synapse/matrix.aleksis.example.org.log.config
similarity index 100%
rename from synapse/matrix.aleksis.example.org.log.config
rename to aleksis/apps/matrix/tests/synapse/matrix.aleksis.example.org.log.config
diff --git a/synapse/matrix.aleksis.example.org.signing.key b/aleksis/apps/matrix/tests/synapse/matrix.aleksis.example.org.signing.key
similarity index 100%
rename from synapse/matrix.aleksis.example.org.signing.key
rename to aleksis/apps/matrix/tests/synapse/matrix.aleksis.example.org.signing.key
diff --git a/aleksis/apps/matrix/tests/test_matrix.py b/aleksis/apps/matrix/tests/test_matrix.py
index 5efb3a1f04be829a4fc86a775d42b4caa987ca56..52f9a885760f99e26cad373039fee8bde2acc1a2 100644
--- a/aleksis/apps/matrix/tests/test_matrix.py
+++ b/aleksis/apps/matrix/tests/test_matrix.py
@@ -7,8 +7,13 @@ import pytest
 import requests
 from celery.result import AsyncResult
 
-from aleksis.apps.matrix.matrix import do_matrix_request
 from aleksis.apps.matrix.models import MatrixProfile, MatrixRoom, MatrixSpace
+from aleksis.apps.matrix.util.matrix import (
+    MatrixException,
+    build_url,
+    do_matrix_request,
+    get_headers,
+)
 from aleksis.core.models import Group, Person, SchoolTerm
 from aleksis.core.util.core_helpers import get_site_preferences
 
@@ -27,8 +32,6 @@ def test_connection(synapse):
 @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
@@ -39,8 +42,6 @@ def matrix_bot_user(synapse):
 
     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 user
@@ -52,8 +53,6 @@ def test_matrix_bot_user(matrix_bot_user):
 
 
 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)
@@ -79,8 +78,6 @@ def test_create_room_for_group_short_name(matrix_bot_user):
 
 
 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")
@@ -123,9 +120,10 @@ def test_room_alias_collision_school_term(matrix_bot_user):
 
 
 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"
+    get_site_preferences()["matrix__reduce_power_levels"] = False
+    get_site_preferences()["matrix__power_level_for_owners"] = 50
+    get_site_preferences()["matrix__power_level_for_members"] = 0
 
     g = Group.objects.create(name="Test Room")
     u1 = User.objects.create_user("test1", "test1@example.org", "test1")
@@ -188,6 +186,71 @@ def test_sync_room_members(matrix_bot_user):
         break
 
 
+def test_power_levels(matrix_bot_user):
+    get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org"
+    get_site_preferences()["matrix__power_level_for_owners"] = 55
+    get_site_preferences()["matrix__power_level_for_members"] = 11
+    get_site_preferences()["matrix__reduce_power_levels"] = False
+
+    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")
+
+    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)
+
+    g.members.set([p1])
+    g.owners.set([p2])
+
+    room = MatrixRoom.from_group(g)
+    room.sync_profiles()
+
+    # Get power levels
+    r = do_matrix_request("GET", f"rooms/{room.room_id}/state")
+    for event in r:
+        if not event["type"] == "m.room.power_levels":
+            continue
+        current_power_levels = event["content"]["users"]
+
+        assert current_power_levels[p1.matrix_profile.matrix_id] == 11
+        assert current_power_levels[p2.matrix_profile.matrix_id] == 55
+
+        break
+
+    # Test reducing of power levels
+    g.owners.set([])
+    g.members.set([p1, p2])
+
+    room.sync_profiles()
+
+    # Not reduced here
+    r = do_matrix_request("GET", f"rooms/{room.room_id}/state")
+    for event in r:
+        if not event["type"] == "m.room.power_levels":
+            continue
+        current_power_levels = event["content"]["users"]
+
+        assert current_power_levels[p1.matrix_profile.matrix_id] == 11
+        assert current_power_levels[p2.matrix_profile.matrix_id] == 55
+
+        break
+
+    get_site_preferences()["matrix__reduce_power_levels"] = True
+    room.sync_profiles()
+
+    # Reduced here
+    r = do_matrix_request("GET", f"rooms/{room.room_id}/state")
+    for event in r:
+        if not event["type"] == "m.room.power_levels":
+            continue
+        current_power_levels = event["content"]["users"]
+
+        assert current_power_levels[p1.matrix_profile.matrix_id] == 11
+        assert current_power_levels[p2.matrix_profile.matrix_id] == 11
+
+        break
+
+
 def test_sync_room_members_without_user(matrix_bot_user):
 
     get_site_preferences()["matrix__homeserver_ids"] = "matrix.aleksis.example.org"
@@ -235,8 +298,6 @@ def test_sync_room_members_without_homeserver(matrix_bot_user):
 
 
 def test_use_room_sync(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")
@@ -246,7 +307,7 @@ def test_use_room_sync(matrix_bot_user):
 
     g.members.add(p1)
 
-    r = g.use_in_matrix(sync=True)
+    r = g.provision_in_matrix(sync=True)
 
     assert isinstance(r, MatrixRoom)
 
@@ -274,7 +335,7 @@ class MatrixCeleryTest(TransactionTestCase):
 
         g.members.add(p1)
 
-        r = g.use_in_matrix(sync=False)
+        r = g.provision_in_matrix(sync=False)
         assert isinstance(r, AsyncResult)
 
         time.sleep(3)
@@ -291,7 +352,7 @@ def test_space_creation(matrix_bot_user):
     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)
+    parent_group.provision_in_matrix(sync=True)
 
     get_site_preferences()["matrix__use_spaces"] = True
 
@@ -338,7 +399,7 @@ def test_space_creation_with_child_spaces(matrix_bot_user):
     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)
+    parent_group.provision_in_matrix(sync=True)
 
     get_site_preferences()["matrix__use_spaces"] = True
 
diff --git a/aleksis/apps/matrix/matrix.py b/aleksis/apps/matrix/util/matrix.py
similarity index 51%
rename from aleksis/apps/matrix/matrix.py
rename to aleksis/apps/matrix/util/matrix.py
index 41a7b04cc13b2b1b47063f2b897be7689b4d564a..864a3f6ebbbefc34219dad31b94b0ccdfc8459ae 100644
--- a/aleksis/apps/matrix/matrix.py
+++ b/aleksis/apps/matrix/util/matrix.py
@@ -1,6 +1,6 @@
 import time
 from json import JSONDecodeError
-from typing import Any, Dict, Optional
+from typing import Any, Optional
 from urllib.parse import urljoin
 
 import requests
@@ -12,34 +12,37 @@ class MatrixException(Exception):
     pass
 
 
-def build_url(path):
+def build_url(path: str) -> str:
+    """Build a URL to the Matrix Client Server API."""
     return urljoin(
         urljoin(get_site_preferences()["matrix__homeserver"], "_matrix/client/v3/"), path
     )
 
 
-def get_headers():
+def get_headers() -> dict[str, str]:
+    """Get the headers for a Matrix Client Server API request."""
     return {
         "Authorization": "Bearer " + get_site_preferences()["matrix__access_token"],
     }
 
 
-def do_matrix_request(method: str, url: str, body: Optional[dict] = None) -> Dict[str, Any]:
+def do_matrix_request(method: str, url: str, body: Optional[dict] = None) -> dict[str, Any]:
     """Do a HTTP request to the Matrix Client Server API."""
     while True:
         res = requests.request(method=method, url=build_url(url), headers=get_headers(), json=body)
-        if res.status_code != requests.codes.ok:
-            try:
-                data = res.json()
-            except JSONDecodeError:
-                raise MatrixException(res.text)
-
-            # If rate limit exceeded, wait and retry
-            if data.get("errcode", "") == "M_LIMIT_EXCEEDED":
-                time.sleep(data["retry_after_ms"] / 1000)
-            else:
-                raise MatrixException(data)
-        else:
+
+        try:
+            data = res.json()
+        except JSONDecodeError:
+            raise MatrixException(res.text) from JSONDecodeError
+
+        if res.status_code == requests.codes.ok:
             break
 
-    return res.json()
+        # If rate limit exceeded, wait and retry
+        if data.get("errcode", "") == "M_LIMIT_EXCEEDED":
+            time.sleep(data["retry_after_ms"] / 1000)
+        else:
+            raise MatrixException(data)
+
+    return data
diff --git a/aleksis/apps/matrix/views.py b/aleksis/apps/matrix/views.py
index 92c4aafa2655773646a0e2ae0634e0112edc70fb..f3f9dd9beb7a00337874b19b11606c487890ab7c 100644
--- a/aleksis/apps/matrix/views.py
+++ b/aleksis/apps/matrix/views.py
@@ -10,6 +10,8 @@ from aleksis.core.models import Group
 
 
 class MatrixRoomListView(PermissionRequiredMixin, SingleTableMixin, FilterView):
+    """Overview about groups and their Matrix rooms."""
+
     model = Group
     template_name = "matrix/room/list.html"
     permission_required = "matrix.view_matrixrooms_rule"
@@ -32,6 +34,8 @@ class MatrixRoomListView(PermissionRequiredMixin, SingleTableMixin, FilterView):
 
     def post(self, request, *args, **kwargs):
         r = super().get(request, *args, **kwargs)
-        if self.action_form.is_valid() and request.user.has_perm("matrix.use_group_in_matrix_rule"):
+        if self.action_form.is_valid() and request.user.has_perm(
+            "matrix.provision_group_in_matrix_rule"
+        ):
             self.action_form.execute()
         return r
diff --git a/conftest.py b/conftest.py
index bdd05deeafd0174838d11958e7a028cdb73d80eb..8dc44cfe41f00b97d89a12e90a35372b941aac95 100644
--- a/conftest.py
+++ b/conftest.py
@@ -5,53 +5,3 @@ import yaml
 from xprocess import ProcessStarter
 
 pytest_plugins = ("celery.contrib.pytest",)
-
-
-@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()
diff --git a/aleksis/apps/matrix/example_data.yaml b/fixtures/example_data.yaml
similarity index 100%
rename from aleksis/apps/matrix/example_data.yaml
rename to fixtures/example_data.yaml
diff --git a/pyproject.toml b/pyproject.toml
index 9ff420af8e5c743e7ca25f02cb1f6feba6a79e42..1c9b49dd1860150aad1093105313f4c89052c64e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ readme = "README.rst"
 include = ["CHANGELOG.rst", "LICENCE.rst", "aleksis/**/*.mo"]
 
 description = "AlekSIS (School Information System) — App Matrix (Integration with Matrix/Element)"
-authors = ["Dominik George <dominik.george@teckids.org>"]
+authors = ["Jonathan Weth <dev@jonathanweth.de>"]
 maintainers = [
     "Jonathan Weth <wethjo@katharineum.de>",
     "Dominik George <dominik.george@teckids.org>"
@@ -32,7 +32,7 @@ secondary = true
 
 [tool.poetry.dependencies]
 python = "^3.9"
-aleksis-core = "^2.1.dev0"
+aleksis-core = "^2.7"
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "^6"