ansible-collection-famedly-.../plugins/modules/matrix_member.py
Lars Kaiser 9dbd99a305
feat(matrix_member): support force join for local users
Requires synapse as homeserver.
2023-03-30 20:41:50 +02:00

317 lines
10 KiB
Python

#!/usr/bin/python
# coding: utf-8
# (c) 2018, Jan Christian Grünhage <jan.christian@gruenhage.xyz>
# (c) 2020-2021, Famedly GmbH
# GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
"metadata_version": "1.1",
"status": ["preview"],
"supported_by": "community",
}
DOCUMENTATION = """
---
author: "Johanna Dorothea Reichmann (@transcaffeine)"
module: matrix_member
short_description: Manage matrix room membership
description:
- Manage the membership status of a given set of matrix users. Invitations (`state=member`),
kicks and bans can be issued. With the `exclusive=True` flag, all other members in the room can be auto-kicked.
options:
hs_url:
description:
- URL of the homeserver, where the CS-API is reachable
required: true
type: str
user_id:
description:
- The user id of the user
required: false
type: str
password:
description:
- The password to log in with
required: false
type: str
token:
description:
- Authentication token for the API call
required: false
type: str
room_id:
description:
- ID of the room to edit
required: true
type: str
user_ids:
description:
- List of matrix IDs to set their state
type: list
elements: str
required: true
state:
description:
- In which state all listed members should be member|kicked|banned
choices: ['member', 'kicked', 'banned']
type: str
required: true
exclusive:
description:
- If state=member, the module ensure only the specified user_ids are in the room by
kicking every other user present in the room.
default: false
type: bool
required: false
force_join:
description:
- If state=member, the module force joins the user by calling the
`/_synapse/admin/v1/join/{room-id}` endpoint of synapses admin API.
- Only works for synapse homeservers and if the provided credentials have admin privileges.
- Only works for local users.
default: false
type: bool
required: false
requirements:
- matrix-nio (Python library)
- requests (Python library)
"""
EXAMPLES = """
- name: Invite two users by matrix-ID into a room
matrix_member:
hs_url: "https://matrix.org"
token: "{{ matrix_auth_token }}"
room_id: "{{ matrix_room_id }}"
state: member
user_ids:
- "@user1:matrix.org"
- "@user2:homeserver.tld"
"""
RETURN = """
members:
description: Dictionary of all members in the given room who are either invited or joined
returned: When auth_token is valid
type: dict
"""
import asyncio
import traceback
from ansible.module_utils.basic import missing_required_lib
LIB_IMP_ERR = None
try:
from ansible_collections.famedly.matrix.plugins.module_utils.matrix import (
AnsibleNioModule,
)
from ansible_collections.famedly.matrix.plugins.module_utils.synapse import (
AdminApi,
Exceptions,
)
from nio import (
RoomGetStateError,
RoomBanError,
RoomUnbanError,
RoomKickError,
RoomInviteError,
)
HAS_LIB = True
except ImportError:
LIB_IMP_ERR = traceback.format_exc()
HAS_LIB = False
class NioOperationError(Exception):
def __init__(self, *args):
super().__init__(*args)
async def get_room_members(client, room_id, res):
member_resp = await client.room_get_state(room_id)
if isinstance(member_resp, RoomGetStateError):
res["msg"] = f"Could not get room state for roomId={room_id}"
raise NioOperationError(res["msg"])
else:
return dict(
list(
map(
lambda m: (m["state_key"], m["content"]["membership"]),
filter(
lambda e: e["type"] == "m.room.member"
and e["content"]["membership"]
in ["invite", "join", "leave", "ban"],
member_resp.events,
),
)
)
)
async def ban_from_room(client, room_id, user_id, res):
ban_resp = await client.room_ban(room_id, user_id)
if isinstance(ban_resp, RoomBanError):
res["msg"] = f"Could not ban user={user_id} from roomId={room_id}"
raise NioOperationError(res["msg"])
res["changed"] = True
res["banned"].append(user_id)
async def unban_from_room(client, room_id, user_id, res):
ban_resp = await client.room_unban(room_id, user_id)
if isinstance(ban_resp, RoomUnbanError):
res["msg"] = f"Could not unban user={user_id} from roomId={room_id}"
raise NioOperationError(res["msg"])
res["changed"] = True
res["unbanned"].append(user_id)
async def kick_from_room(client, room_id, user_id, res):
kick_resp = await client.room_kick(room_id, user_id)
if isinstance(kick_resp, RoomKickError):
res["msg"] = f"Could not kick user={user_id} from roomId={room_id}"
raise NioOperationError(res["msg"])
res["changed"] = True
res["kicked"].append(user_id)
async def invite_to_room(client, room_id, user_id, res):
invite_resp = await client.room_invite(room_id, user_id)
if isinstance(invite_resp, RoomInviteError):
res["msg"] = f"Could not invite user={user_id} to roomId={room_id}"
raise NioOperationError(res["msg"])
res["changed"] = True
res["invited"].append(user_id)
async def force_join_into_room(admin_client, room_id, user_id, res):
admin_client.join.join(room_id, user_id)
res["changed"] = True
res["joined"].append(user_id)
async def run_module():
module_args = dict(
state=dict(choices=["member", "kicked", "banned"], required=True),
room_id=dict(type="str", required=True),
user_ids=dict(type="list", required=True, elements="str"),
exclusive=dict(type="bool", required=False, default=False),
force_join=dict(type="bool", required=False, default=False),
)
result = dict(
changed=False,
banned=[],
unbanned=[],
kicked=[],
invited=[],
joined=[],
members=[],
msg="",
)
module = AnsibleNioModule(module_args)
if not HAS_LIB:
await module.fail_json(msg=missing_required_lib("matrix-nio"))
await module.matrix_login()
action = module.params["state"]
room_id = module.params["room_id"]
user_ids = module.params["user_ids"]
force_join = module.params["force_join"]
# Check for valid parameter combination
if module.params["exclusive"] and action != "member":
await module.fail_json(msg="exclusive=True can only be used with state=member")
if module.params["force_join"] and action != "member":
await module.fail_json(msg="force_join=True can only be used with state=member")
# Handle ansible check mode
if module.check_mode:
result["changed"] = True
result["members"] = user_ids
await module.exit_json(**result)
# Create client object
client = module.client
admin_client = AdminApi(
home_server=module.params["hs_url"], access_token=module.params["token"]
)
try:
# Query all room members (invited users count as member, as they _can_ be in the room)
room_members = await get_room_members(client, room_id, result)
present_members = {
m: s for m, s in room_members.items() if s in ["join", "invite"]
}.keys()
banned_members = {m: s for m, s in room_members.items() if s == "ban"}.keys()
except NioOperationError:
await module.fail_json(**result)
if not module.params["exclusive"]:
# Handle non-exclusive invite|kick|ban
try:
for user_id in user_ids:
if action == "member" and user_id not in present_members:
if user_id in banned_members:
await unban_from_room(client, room_id, user_id, result)
if force_join:
await force_join_into_room(
admin_client, room_id, user_id, result
)
else:
await invite_to_room(client, room_id, user_id, result)
elif action == "kicked" and user_id in present_members:
await kick_from_room(client, room_id, user_id, result)
elif action == "kicked" and user_id in banned_members:
await unban_from_room(client, room_id, user_id, result)
elif action == "banned" and user_id not in banned_members:
await ban_from_room(client, room_id, user_id, result)
except NioOperationError:
await module.fail_json(**result)
except (Exceptions.HTTPException, Exceptions.MatrixException) as e:
result["msg"] = str(e)
await module.fail_json(**result)
else:
# Handle exclusive mode: get state and make lists of users to be kicked or invited
to_invite = list(filter(lambda m: m not in present_members, user_ids))
to_kick = list(filter(lambda m: m not in user_ids, present_members))
try:
for user_id in to_invite:
if force_join:
await force_join_into_room(admin_client, room_id, user_id, result)
else:
await invite_to_room(client, room_id, user_id, result)
for user_id in to_kick:
await kick_from_room(client, room_id, user_id, result)
except NioOperationError:
await module.fail_json(**result)
except (Exceptions.HTTPException, Exceptions.MatrixException) as e:
result["msg"] = str(e)
await module.fail_json(**result)
# Get all current members from the room
try:
room_members_after = await get_room_members(client, room_id, result)
result["members"] = {
m: s for m, s in room_members_after.items() if s in ["join", "invite"]
}.keys()
except NioOperationError:
pass
await module.exit_json(**result)
def main():
asyncio.run(run_module())
if __name__ == "__main__":
main()