File: rooms.py

package info (click to toggle)
mautrix-python 0.20.7-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 1,812 kB
  • sloc: python: 19,103; makefile: 16
file content (789 lines) | stat: -rw-r--r-- 33,993 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
# Copyright (c) 2022 Tulir Asokan
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import annotations

from typing import Any, Awaitable, Callable
import asyncio

from multidict import CIMultiDict

from mautrix.api import Method, Path
from mautrix.errors import (
    MatrixRequestError,
    MatrixResponseError,
    MNotFound,
    MNotJoined,
    MRoomInUse,
)
from mautrix.types import (
    JSON,
    DirectoryPaginationToken,
    EventID,
    EventType,
    Membership,
    MemberStateEventContent,
    PowerLevelStateEventContent,
    RoomAlias,
    RoomAliasInfo,
    RoomCreatePreset,
    RoomCreateStateEventContent,
    RoomDirectoryResponse,
    RoomDirectoryVisibility,
    RoomID,
    Serializable,
    StateEvent,
    StrippedStateEvent,
    UserID,
)

from .base import BaseClientAPI
from .events import EventMethods


class RoomMethods(EventMethods, BaseClientAPI):
    """
    Methods in section 8 Rooms of the spec. These methods are used for creating rooms, interacting
    with the room directory and using the easy room metadata editing endpoints. Generic state
    setting and sending events are in the :class:`EventMethods` (section 7) module.

    See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#rooms-1>`__
    """

    # region 8.1 Creation
    # API reference: https://spec.matrix.org/v1.1/client-server-api/#creation

    async def create_room(
        self,
        alias_localpart: str | None = None,
        visibility: RoomDirectoryVisibility = RoomDirectoryVisibility.PRIVATE,
        preset: RoomCreatePreset = RoomCreatePreset.PRIVATE,
        name: str | None = None,
        topic: str | None = None,
        is_direct: bool = False,
        invitees: list[UserID] | None = None,
        initial_state: list[StateEvent | StrippedStateEvent | dict[str, JSON]] | None = None,
        room_version: str = None,
        creation_content: RoomCreateStateEventContent | dict[str, JSON] | None = None,
        power_level_override: PowerLevelStateEventContent | dict[str, JSON] | None = None,
        beeper_auto_join_invites: bool = False,
        custom_request_fields: dict[str, Any] | None = None,
    ) -> RoomID:
        """
        Create a new room with various configuration options.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3createroom>`__

        Args:
            alias_localpart: The desired room alias **local part**. If this is included, a room
                alias will be created and mapped to the newly created room. The alias will belong
                on the same homeserver which created the room. For example, if this was set to
                "foo" and sent to the homeserver "example.com" the complete room alias would be
                ``#foo:example.com``.
            visibility: A ``public`` visibility indicates that the room will be shown in the
                published room list. A ``private`` visibility will hide the room from the published
                room list. Defaults to ``private``. **NB:** This should not be confused with
                ``join_rules`` which also uses the word ``public``.
            preset: Convenience parameter for setting various default state events based on a
                preset. Defaults to private (invite-only).
            name: If this is included, an ``m.room.name`` event will be sent into the room to
                indicate the name of the room. See `Room Events`_ for more information on
                ``m.room.name``.
            topic: If this is included, an ``m.room.topic`` event will be sent into the room to
                indicate the topic for the room. See `Room Events`_ for more information on
                ``m.room.topic``.
            is_direct: This flag makes the server set the ``is_direct`` flag on the
                `m.room.member`_ events sent to the users in ``invite`` and ``invite_3pid``. See
                `Direct Messaging`_ for more information.
            invitees: A list of user IDs to invite to the room. This will tell the server to invite
                everyone in the list to the newly created room.
            initial_state: A list of state events to set in the new room. This allows the user to
                override the default state events set in the new room. The expected format of the
                state events are an object with type, state_key and content keys set.

                Takes precedence over events set by ``is_public``, but gets overriden by ``name``
                and ``topic keys``.
            room_version: The room version to set for the room. If not provided, the homeserver
                will use its configured default.
            creation_content: Extra keys, such as ``m.federate``, to be added to the
                ``m.room.create`` event. The server will ignore ``creator`` and ``room_version``.
                Future versions of the specification may allow the server to ignore other keys.
            power_level_override: The power level content to override in the default power level
                event. This object is applied on top of the generated ``m.room.power_levels`` event
                content prior to it being sent to the room. Defaults to overriding nothing.
            beeper_auto_join_invites: A Beeper-specific extension which auto-joins all members in
                the invite array instead of sending invites.
            custom_request_fields: Additional fields to put in the top-level /createRoom content.
                Non-custom fields take precedence over fields here.

        Returns:
            The ID of the newly created room.

        Raises:
            MatrixResponseError: If the response does not contain a ``room_id`` field.

        .. _Room Events: https://spec.matrix.org/v1.1/client-server-api/#room-events
        .. _Direct Messaging: https://spec.matrix.org/v1.1/client-server-api/#direct-messaging
        .. _m.room.create: https://spec.matrix.org/v1.1/client-server-api/#mroomcreate
        .. _m.room.member: https://spec.matrix.org/v1.1/client-server-api/#mroommember
        """
        content = {
            **(custom_request_fields or {}),
            "visibility": visibility.value,
            "is_direct": is_direct,
            "preset": preset.value,
        }
        if alias_localpart:
            content["room_alias_name"] = alias_localpart
        if invitees:
            content["invite"] = invitees
            if beeper_auto_join_invites:
                content["com.beeper.auto_join_invites"] = True
        if name:
            content["name"] = name
        if topic:
            content["topic"] = topic
        if initial_state:
            content["initial_state"] = [
                event.serialize() if isinstance(event, Serializable) else event
                for event in initial_state
            ]
        if room_version:
            content["room_version"] = room_version
        if creation_content:
            content["creation_content"] = (
                creation_content.serialize()
                if isinstance(creation_content, Serializable)
                else creation_content
            )
            # Remove keys that the server will ignore anyway
            content["creation_content"].pop("room_version", None)
        if power_level_override:
            content["power_level_content_override"] = (
                power_level_override.serialize()
                if isinstance(power_level_override, Serializable)
                else power_level_override
            )

        resp = await self.api.request(Method.POST, Path.v3.createRoom, content)
        try:
            return resp["room_id"]
        except KeyError:
            raise MatrixResponseError("`room_id` not in response.")

    # endregion
    # region 8.2 Room aliases
    # API reference: https://spec.matrix.org/v1.1/client-server-api/#room-aliases

    async def add_room_alias(
        self, room_id: RoomID, alias_localpart: str, override: bool = False
    ) -> None:
        """
        Create a new mapping from room alias to room ID.

        See also: `API reference <https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-directory-room-roomalias>`__

        Args:
            room_id: The room ID to set.
            alias_localpart: The localpart of the room alias to set.
            override: Whether or not the alias should be removed and the request retried if the
                server responds with HTTP 409 Conflict
        """
        room_alias = f"#{alias_localpart}:{self.domain}"
        content = {"room_id": room_id}
        try:
            await self.api.request(Method.PUT, Path.v3.directory.room[room_alias], content)
        except MatrixRequestError as e:
            if e.http_status == 409:
                if override:
                    await self.remove_room_alias(alias_localpart)
                    await self.api.request(Method.PUT, Path.v3.directory.room[room_alias], content)
                else:
                    raise MRoomInUse(e.http_status, e.message) from e
            else:
                raise

    async def remove_room_alias(self, alias_localpart: str, raise_404: bool = False) -> None:
        """
        Remove a mapping of room alias to room ID.

        Servers may choose to implement additional access control checks here, for instance that
        room aliases can only be deleted by their creator or server administrator.

        See also: `API reference <https://matrix.org/docs/spec/client_server/r0.4.0.html#delete-matrix-client-r0-directory-room-roomalias>`__

        Args:
            alias_localpart: The room alias to remove.
            raise_404: Whether 404 errors should be raised as exceptions instead of ignored.
        """
        room_alias = f"#{alias_localpart}:{self.domain}"
        try:
            await self.api.request(Method.DELETE, Path.v3.directory.room[room_alias])
        except MNotFound:
            if raise_404:
                raise
            # else: ignore

    async def resolve_room_alias(self, room_alias: RoomAlias) -> RoomAliasInfo:
        """
        Request the server to resolve a room alias to a room ID.

        The server will use the federation API to resolve the alias if the domain part of the alias
        does not correspond to the server's own domain.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3directoryroomroomalias>`__

        Args:
            room_alias: The room alias.

        Returns:
            The room ID and a list of servers that are aware of the room.
        """
        content = await self.api.request(Method.GET, Path.v3.directory.room[room_alias])
        return RoomAliasInfo.deserialize(content)

    # endregion
    # region 8.4 Room membership
    # API reference: https://spec.matrix.org/v1.1/client-server-api/#room-membership

    async def get_joined_rooms(self) -> list[RoomID]:
        """Get the list of rooms the user is in."""
        content = await self.api.request(Method.GET, Path.v3.joined_rooms)
        try:
            return content["joined_rooms"]
        except KeyError:
            raise MatrixResponseError("`joined_rooms` not in response.")

    # region 8.4.1 Joining rooms
    # API reference: https://spec.matrix.org/v1.1/client-server-api/#joining-rooms

    async def join_room_by_id(
        self,
        room_id: RoomID,
        third_party_signed: JSON = None,
        extra_content: dict[str, Any] | None = None,
    ) -> RoomID:
        """
        Start participating in a room, i.e. join it by its ID.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidjoin>`__

        Args:
            room_id: The ID of the room to join.
            third_party_signed: A signature of an ``m.third_party_invite`` token to prove that this
                user owns a third party identity which has been invited to the room.
            extra_content: Additional properties for the join event content.
                If a non-empty dict is passed, the join event will be created using
                the ``PUT /state/m.room.member/...`` endpoint instead of ``POST /join``.

        Returns:
            The ID of the room the user joined.
        """
        if extra_content:
            await self.send_member_event(
                room_id, self.mxid, Membership.JOIN, extra_content=extra_content
            )
            return room_id
        content = await self.api.request(
            Method.POST,
            Path.v3.rooms[room_id].join,
            {"third_party_signed": third_party_signed} if third_party_signed is not None else None,
        )
        try:
            return content["room_id"]
        except KeyError:
            raise MatrixResponseError("`room_id` not in response.")

    async def join_room(
        self,
        room_id_or_alias: RoomID | RoomAlias,
        servers: list[str] | None = None,
        third_party_signed: JSON = None,
        max_retries: int = 4,
    ) -> RoomID:
        """
        Start participating in a room, i.e. join it by its ID or alias, with an optional list of
        servers to ask about the ID from.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3joinroomidoralias>`__

        Args:
            room_id_or_alias: The ID of the room to join, or an alias pointing to the room.
            servers: A list of servers to ask about the room ID to join. Not applicable for aliases,
                as aliases already contain the necessary server information.
            third_party_signed: A signature of an ``m.third_party_invite`` token to prove that this
                user owns a third party identity which has been invited to the room.
            max_retries: The maximum number of retries. Used to circumvent a Synapse bug with
                accepting invites over federation. 0 means only one join call will be attempted.
                See: `matrix-org/synapse#2807 <https://github.com/matrix-org/synapse/issues/2807>`__

        Returns:
            The ID of the room the user joined.
        """
        max_retries = max(0, max_retries)
        tries = 0
        content = (
            {"third_party_signed": third_party_signed} if third_party_signed is not None else None
        )
        query_params = CIMultiDict()
        for server_name in servers or []:
            query_params.add("server_name", server_name)
        while tries <= max_retries:
            try:
                content = await self.api.request(
                    Method.POST,
                    Path.v3.join[room_id_or_alias],
                    content=content,
                    query_params=query_params,
                )
                break
            except MatrixRequestError:
                tries += 1
                if tries <= max_retries:
                    wait = tries * 10
                    self.log.exception(
                        f"Failed to join room {room_id_or_alias}, retrying in {wait} seconds..."
                    )
                    await asyncio.sleep(wait)
                else:
                    raise
        try:
            return content["room_id"]
        except KeyError:
            raise MatrixResponseError("`room_id` not in response.")

    fill_member_event_callback: (
        Callable[
            [RoomID, UserID, MemberStateEventContent], Awaitable[MemberStateEventContent | None]
        ]
        | None
    )

    async def fill_member_event(
        self, room_id: RoomID, user_id: UserID, content: MemberStateEventContent
    ) -> MemberStateEventContent | None:
        """
        Fill a membership event content that is going to be sent in :meth:`send_member_event`.

        This is used to set default fields like the displayname and avatar, which are usually set
        by the server in the sugar membership endpoints like /join and /invite, but are not set
        automatically when sending member events manually.

        This default implementation only calls :attr:`fill_member_event_callback`.

        Args:
            room_id: The room where the member event is going to be sent.
            user_id: The user whose membership is changing.
            content: The new member event content.

        Returns:
            The filled member event content.
        """
        if self.fill_member_event_callback is not None:
            return await self.fill_member_event_callback(room_id, user_id, content)
        return None

    async def send_member_event(
        self,
        room_id: RoomID,
        user_id: UserID,
        membership: Membership,
        extra_content: dict[str, JSON] | None = None,
    ) -> EventID:
        """
        Send a membership event manually.

        Args:
            room_id: The room to send the event to.
            user_id: The user whose membership to change.
            membership: The membership status.
            extra_content: Additional content to put in the member event.

        Returns:
            The event ID of the new member event.
        """
        content = MemberStateEventContent(membership=membership)
        for key, value in extra_content.items():
            content[key] = value
        content = await self.fill_member_event(room_id, user_id, content) or content
        return await self.send_state_event(
            room_id, EventType.ROOM_MEMBER, content=content, state_key=user_id, ensure_joined=False
        )

    async def invite_user(
        self,
        room_id: RoomID,
        user_id: UserID,
        reason: str | None = None,
        extra_content: dict[str, JSON] | None = None,
    ) -> None:
        """
        Invite a user to participate in a particular room. They do not start participating in the
        room until they actually join the room.

        Only users currently in the room can invite other users to join that room.

        If the user was invited to the room, the homeserver will add a `m.room.member`_ event to
        the room.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidinvite>`__

        .. _m.room.member: https://spec.matrix.org/v1.1/client-server-api/#mroommember

        Args:
            room_id: The ID of the room to which to invite the user.
            user_id: The fully qualified user ID of the invitee.
            reason: The reason the user was invited. This will be supplied as the ``reason`` on
                the `m.room.member`_ event.
            extra_content: Additional properties for the invite event content.
                If a non-empty dict is passed, the invite event will be created using
                the ``PUT /state/m.room.member/...`` endpoint instead of ``POST /invite``.
        """
        if extra_content:
            await self.send_member_event(
                room_id, user_id, Membership.INVITE, extra_content=extra_content
            )
        else:
            data = {"user_id": user_id}
            if reason:
                data["reason"] = reason
            await self.api.request(Method.POST, Path.v3.rooms[room_id].invite, content=data)

    # endregion
    # region 8.4.2 Leaving rooms
    # API reference: https://spec.matrix.org/v1.1/client-server-api/#leaving-rooms

    async def leave_room(
        self,
        room_id: RoomID,
        reason: str | None = None,
        extra_content: dict[str, JSON] | None = None,
        raise_not_in_room: bool = False,
    ) -> None:
        """
        Stop participating in a particular room, i.e. leave the room.

        If the user was already in the room, they will no longer be able to see new events in the
        room. If the room requires an invite to join, they will need to be re-invited before they
        can re-join.

        If the user was invited to the room, but had not joined, this call serves to reject the
        invite.

        The user will still be allowed to retrieve history from the room which they were previously
        allowed to see.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidleave>`__

        Args:
            room_id: The ID of the room to leave.
            reason: The reason for leaving the room. This will be supplied as the ``reason`` on
                the updated `m.room.member`_ event.
            extra_content: Additional properties for the leave event content.
                If a non-empty dict is passed, the leave event will be created using
                the ``PUT /state/m.room.member/...`` endpoint instead of ``POST /leave``.
            raise_not_in_room: Should errors about the user not being in the room be raised?
        """
        try:
            if extra_content:
                await self.send_member_event(
                    room_id, self.mxid, Membership.LEAVE, extra_content=extra_content
                )
            else:
                data = {}
                if reason:
                    data["reason"] = reason
                await self.api.request(Method.POST, Path.v3.rooms[room_id].leave, content=data)
        except MNotJoined:
            if raise_not_in_room:
                raise
        except MatrixRequestError as e:
            # TODO remove this once MSC3848 is released and minimum spec version is bumped
            if "not in room" not in e.message or raise_not_in_room:
                raise

    async def knock_room(
        self,
        room_id_or_alias: RoomID | RoomAlias,
        reason: str | None = None,
        servers: list[str] | None = None,
    ) -> RoomID:
        """
        Knock on a room, i.e. request to join it by its ID or alias, with an optional list of
        servers to ask about the ID from.

        See also: `API reference <https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3knockroomidoralias>`__

        Args:
            room_id_or_alias: The ID of the room to knock on, or an alias pointing to the room.
            reason: The reason for knocking on the room. This will be supplied as the ``reason`` on
                the updated `m.room.member`_ event.
            servers: A list of servers to ask about the room ID to knock. Not applicable for aliases,
                as aliases already contain the necessary server information.

        Returns:
            The ID of the room the user knocked on.
        """
        data = {}
        if reason:
            data["reason"] = reason
        query_params = CIMultiDict()
        for server_name in servers or []:
            query_params.add("server_name", server_name)
        content = await self.api.request(
            Method.POST,
            Path.v3.knock[room_id_or_alias],
            content=data,
            query_params=query_params,
        )
        try:
            return content["room_id"]
        except KeyError:
            raise MatrixResponseError("`room_id` not in response.")

    async def forget_room(self, room_id: RoomID) -> None:
        """
        Stop remembering a particular room, i.e. forget it.

        In general, history is a first class citizen in Matrix. After this API is called, however,
        a user will no longer be able to retrieve history for this room. If all users on a
        homeserver forget a room, the room is eligible for deletion from that homeserver.

        If the user is currently joined to the room, they must leave the room before calling this
        API.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidforget>`__

        Args:
            room_id: The ID of the room to forget.
        """
        await self.api.request(Method.POST, Path.v3.rooms[room_id].forget)

    async def kick_user(
        self,
        room_id: RoomID,
        user_id: UserID,
        reason: str = "",
        extra_content: dict[str, JSON] | None = None,
    ) -> None:
        """
        Kick a user from the room.

        The caller must have the required power level in order to perform this operation.

        Kicking a user adjusts the target member's membership state to be ``leave`` with an optional
        ``reason``. Like with other membership changes, a user can directly adjust the target
        member's state by calling :meth:`EventMethods.send_state_event` with
        :attr:`EventType.ROOM_MEMBER` as the event type and the ``user_id`` as the state key.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidkick>`__

        Args:
            room_id: The ID of the room from which the user should be kicked.
            user_id: The fully qualified user ID of the user being kicked.
            reason: The reason the user has been kicked. This will be supplied as the ``reason`` on
                the target's updated `m.room.member`_ event.
            extra_content: Additional properties for the kick event content.
                If a non-empty dict is passed, the kick event will be created using
                the ``PUT /state/m.room.member/...`` endpoint instead of ``POST /kick``.

        .. _m.room.member: https://spec.matrix.org/v1.1/client-server-api/#mroommember
        """
        if extra_content:
            if reason and "reason" not in extra_content:
                extra_content["reason"] = reason
            await self.send_member_event(
                room_id, user_id, Membership.LEAVE, extra_content=extra_content
            )
            return
        await self.api.request(
            Method.POST, Path.v3.rooms[room_id].kick, {"user_id": user_id, "reason": reason}
        )

    # endregion
    # region 8.4.2.1 Banning users in a room
    # API reference: https://spec.matrix.org/v1.1/client-server-api/#banning-users-in-a-room

    async def ban_user(
        self,
        room_id: RoomID,
        user_id: UserID,
        reason: str = "",
        extra_content: dict[str, JSON] | None = None,
    ) -> None:
        """
        Ban a user in the room. If the user is currently in the room, also kick them. When a user is
        banned from a room, they may not join it or be invited to it until they are unbanned. The
        caller must have the required power level in order to perform this operation.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidban>`__

        Args:
            room_id: The ID of the room from which the user should be banned.
            user_id: The fully qualified user ID of the user being banned.
            reason: The reason the user has been kicked. This will be supplied as the ``reason`` on
                the target's updated `m.room.member`_ event.
            extra_content: Additional properties for the ban event content.
                If a non-empty dict is passed, the ban will be created using
                the ``PUT /state/m.room.member/...`` endpoint instead of ``POST /ban``.

        .. _m.room.member: https://spec.matrix.org/v1.1/client-server-api/#mroommember
        """
        if extra_content:
            if reason and "reason" not in extra_content:
                extra_content["reason"] = reason
            await self.send_member_event(
                room_id, user_id, Membership.BAN, extra_content=extra_content
            )
            return
        await self.api.request(
            Method.POST, Path.v3.rooms[room_id].ban, {"user_id": user_id, "reason": reason}
        )

    async def unban_user(
        self,
        room_id: RoomID,
        user_id: UserID,
        reason: str = "",
        extra_content: dict[str, JSON] | None = None,
    ) -> None:
        """
        Unban a user from the room. This allows them to be invited to the room, and join if they
        would otherwise be allowed to join according to its join rules. The caller must have the
        required power level in order to perform this operation.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#post_matrixclientv3roomsroomidunban>`__

        Args:
            room_id: The ID of the room from which the user should be unbanned.
            user_id: The fully qualified user ID of the user being banned.
            reason: The reason the user has been unbanned. This will be supplied as the ``reason`` on
                the target's updated `m.room.member`_ event.
            extra_content: Additional properties for the unban (leave) event content.
                If a non-empty dict is passed, the unban will be created using
                the ``PUT /state/m.room.member/...`` endpoint instead of ``POST /unban``.
        """
        if extra_content:
            if reason and "reason" not in extra_content:
                extra_content["reason"] = reason
            await self.send_member_event(
                room_id, user_id, Membership.LEAVE, extra_content=extra_content
            )
            return
        await self.api.request(
            Method.POST, Path.v3.rooms[room_id].unban, {"user_id": user_id, "reason": reason}
        )

    # endregion

    # endregion
    # region 8.5 Listing rooms
    # API reference: https://spec.matrix.org/v1.1/client-server-api/#listing-rooms

    async def get_room_directory_visibility(self, room_id: RoomID) -> RoomDirectoryVisibility:
        """
        Get the visibility of the room on the server's public room directory.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3directorylistroomroomid>`__

        Args:
            room_id: The ID of the room.

        Returns:
            The visibility of the room in the directory.
        """
        resp = await self.api.request(Method.GET, Path.v3.directory.list.room[room_id])
        try:
            return RoomDirectoryVisibility(resp["visibility"])
        except KeyError:
            raise MatrixResponseError("`visibility` not in response.")
        except ValueError:
            raise MatrixResponseError(
                f"Invalid value for `visibility` in response: {resp['visibility']}"
            )

    async def set_room_directory_visibility(
        self, room_id: RoomID, visibility: RoomDirectoryVisibility
    ) -> None:
        """
        Set the visibility of the room in the server's public room directory.

        Servers may choose to implement additional access control checks here, for instance that
        room visibility can only be changed by the room creator or a server administrator.

        Args:
            room_id: The ID of the room.
            visibility: The new visibility setting for the room.

        .. _API reference: https://spec.matrix.org/v1.1/client-server-api/#put_matrixclientv3directorylistroomroomid
        """
        await self.api.request(
            Method.PUT,
            Path.v3.directory.list.room[room_id],
            {
                "visibility": visibility.value,
            },
        )

    async def get_room_directory(
        self,
        limit: int | None = None,
        server: str | None = None,
        since: DirectoryPaginationToken | None = None,
        search_query: str | None = None,
        include_all_networks: bool | None = None,
        third_party_instance_id: str | None = None,
    ) -> RoomDirectoryResponse:
        """
        Get a list of public rooms from the server's room directory.

        See also: `API reference <https://spec.matrix.org/v1.1/client-server-api/#get_matrixclientv3publicrooms>`__

        Args:
            limit: The maximum number of results to return.
            server: The server to fetch the room directory from. Defaults to the user's server.
            since: A pagination token from a previous request, allowing clients to get the next (or
                previous) batch of rooms. The direction of pagination is specified solely by which
                token is supplied, rather than via an explicit flag.
            search_query: A string to search for in the room metadata, e.g. name, topic, canonical
                alias etc.
            include_all_networks: Whether or not to include rooms from all known networks/protocols
                from application services on the homeserver. Defaults to false.
            third_party_instance_id: The specific third party network/protocol to request from the
                homeserver. Can only be used if ``include_all_networks`` is false.

        Returns:
            The relevant pagination tokens, an estimate of the total number of public rooms and the
            paginated chunk of public rooms.
        """
        method = (
            Method.GET
            if (
                search_query is None
                and include_all_networks is None
                and third_party_instance_id is None
            )
            else Method.POST
        )
        content = {}
        if limit is not None:
            content["limit"] = limit
        if since is not None:
            content["since"] = since
        if search_query is not None:
            content["filter"] = {"generic_search_term": search_query}
        if include_all_networks is not None:
            content["include_all_networks"] = include_all_networks
        if third_party_instance_id is not None:
            content["third_party_instance_id"] = third_party_instance_id
        query_params = {"server": server} if server is not None else None

        content = await self.api.request(
            method, Path.v3.publicRooms, content, query_params=query_params
        )

        return RoomDirectoryResponse.deserialize(content)

    # endregion