Feat: first working version :)

This commit is contained in:
Dorian Zedler 2022-09-29 20:19:35 +02:00
parent 77af9a9428
commit fdc2fb9b30
Signed by: dozedler
GPG key ID: 989DE36109AFA354
5 changed files with 294 additions and 58 deletions

View file

@ -1,3 +1,8 @@
# matrix-bot # matrix-bot
Unser Matrix-Bot, der Gruppen, Gruppenmitgliedschaften und Rechte verwaltet. Unser Matrix-Bot, der Gruppen, Gruppenmitgliedschaften und Rechte verwaltet.
# How it works
1. load all users from matrix
2. add all users to the default channels

View file

@ -10,4 +10,8 @@ export MATRIX_BOT_MATRIX_SPACE_ID="!bajjed:matrix.org"
export MATRIX_BOT_MATRIX_USERNAME="synapse" export MATRIX_BOT_MATRIX_USERNAME="synapse"
export MATRIX_BOT_MATRIX_PASSWORD="SomeSuperSafePassword" export MATRIX_BOT_MATRIX_PASSWORD="SomeSuperSafePassword"
export MATRIX_BOT_DEFAULT_ROOMS="info,talk,show"
export MATRIX_BOT_ADMIN_GROUP="p_admin"
export MATRIX_BOT_MODERATOR_GROUP="p_moderator"
python3 src/main.py python3 src/main.py

View file

@ -9,6 +9,9 @@ class LdapHelper:
self._bindPassword = ldapBindPassword self._bindPassword = ldapBindPassword
self._baseDn = ldapBaseDn self._baseDn = ldapBaseDn
def __del__(self):
self.unbind()
def bind(self): def bind(self):
try: try:
self._ldapConnection = ldap.initialize(f"{self._uri}") self._ldapConnection = ldap.initialize(f"{self._uri}")

View file

@ -1,53 +1,211 @@
import sys, coloredlogs, logging, os, time, asyncio from email.policy import default
import sys
import coloredlogs
import logging
import os
import time
import asyncio
from tokenize import group
from urllib import response from urllib import response
from ldapHelper import LdapHelper from ldapHelper import LdapHelper
from matrixHelper import MatrixHelper from matrixHelper import MatrixHelper
coloredlogs.install(level='INFO', fmt='%(asctime)s - [%(levelname)s] %(message)s') coloredlogs.install(
level='INFO', fmt='%(asctime)s - [%(levelname)s] %(message)s')
class MlmMatrixBot: class MlmMatrixBot:
def __init__(self) -> None: def __init__(self) -> None:
self._config = self._readConfig() self._config = self._readConfig()
self._matrixUsername = f"@{self._config['MATRIX_USERNAME']}:{self._config['MATRIX_DOMAIN']}"
self._ldapHelper = LdapHelper(self._config['LDAP_URI'], self._config['LDAP_BIND_DN'], self._config['LDAP_BIND_DN_PASSWORD'], self._config['LDAP_BASE_DN']) self._ldapHelper = LdapHelper(self._config['LDAP_URI'], self._config['LDAP_BIND_DN'],
self._matrixHelper = MatrixHelper(self._config['MATRIX_SERVER'], f"@{self._config['MATRIX_USERNAME']}:{self._config['MATRIX_DOMAIN']}", self._config['MATRIX_SPACE_ID']) self._config['LDAP_BIND_DN_PASSWORD'], self._config['LDAP_BASE_DN'])
self._matrixHelper = MatrixHelper(
self._config['MATRIX_SERVER'], self._matrixUsername, self._config['MATRIX_SPACE_ID'], self._matrixUsername)
def __del__(self): async def init(self):
self._ldapHelper.unbind()
asyncio.run(self._matrixHelper.logout())
async def run(self):
if not self._ldapHelper.bind(): if not self._ldapHelper.bind():
return False return False
print(self._ldapHelper.search("sophomorixType=project"))
if not await self._matrixHelper.login(self._config['MATRIX_PASSWORD']): if not await self._matrixHelper.login(self._config['MATRIX_PASSWORD']):
return False return False
return True
room = await self._matrixHelper.createRoom("test11", "test11")
response = await self._matrixHelper._client.joined_rooms()
print(response.rooms)
print(await self._matrixHelper._client.room_invite(room.room_id, "@dozedler:makerchat.net")) async def run(self):
await self._matrixHelper._client.sync_forever()
pass logging.info("==> Sync started <==")
await self._create_default_rooms()
current_users, all_projects = await self._load_users_and_groups()
await self._create_project_rooms(all_projects)
logging.info("= (4/5) Loading current rooms =")
matrix_rooms = await self._get_managed_rooms(with_power_levels=True)
matrix_room_ids = list(map(lambda room: room['id'], matrix_rooms))
room_name_id_map = {}
room_id_map = {
self._config['MATRIX_SPACE_ID']: {
'name': "default space",
}
}
for room in matrix_rooms:
room_name_id_map[room['name']] = room['id']
room_id_map[room['id']] = room
logging.info("= (5/5) Syncing user memberships =")
for user in current_users:
if not "mlm" in user['groups']:
continue
# default groups and space
rooms_to_join = self._get_default_rooms() + [self._config["MATRIX_SPACE_ID"]]
# projects
rooms_to_join += list(map(lambda group: group.replace('p_', ''), filter(lambda project: project.startswith("p_"), user['groups'])))
# resolve names to ids
rooms_to_join = list(map(
lambda room: room if room.startswith("!") else room_name_id_map[room],
rooms_to_join))
logging.info(f" \t* Syncing user {user['username']}")
for room_id in rooms_to_join:
# set power level
if room_id in user['rooms']:
if ( user['matrix_username'] in room_id_map[room_id]['power_levels'] and room_id_map[room_id]['power_levels'][user['matrix_username']] != user['power_level']) or (user['matrix_username'] not in room_id_map[room_id]['power_levels'] and user['power_level'] != 0):
logging.info(f"\t\t* Setting power level in {room_id_map[room_id]['name']} to {user['power_level']}")
await self._matrixHelper.set_user_power_level_in_room(user['matrix_username'], room_id, user['power_level'])
continue
logging.info(f"\t* joining {room_id_map[room_id]['name']}")
await self._matrixHelper.add_user_to_room(user['matrix_username'], room_id)
if user['power_level'] != 0:
logging.info(f"\t\t* Setting power level in {room_id_map[room_id]['name']} to {user['power_level']}")
await self._matrixHelper.set_user_power_level_in_room(user['matrix_username'], room_id, user['power_level'])
# remove from rooms that are not in the groups
rooms_to_be_left = list(filter(lambda room: room in matrix_room_ids and not room in rooms_to_join, user['rooms']))
for room_id in rooms_to_be_left:
logging.info(f"\t* leaving {room_id_map[room_id]['name']}")
await self._matrixHelper.remove_user_from_room(
user['matrix_username'], room_id)
logging.info("==> Sync finished <==")
async def _create_default_rooms(self):
# create default rooms
logging.info("= (1/5) Creating default rooms =")
await self._create_rooms(self._get_default_rooms())
async def _load_users_and_groups(self):
"""
loads users and their groups from ldap and matrix and merges them
returns:
([
{
'username': 'dozedler',
'groups': ['mlm', 'p_3d-printing'],
'rooms': ['!room1:matrix.org', '!room2:matrix.org'],
'matrix_username': '@dozedler:matrix.org',
'is_admin': True
}
],
[
'mlm',
'p_3d-printing'
])
"""
logging.info("= (2/5) Loading users and their groups =")
matrix_users = await self._matrixHelper.get_users()
current_users = []
all_projects = []
for user in matrix_users:
username = user['name'].split(':')[0].replace('@', '')
rc, ldap_user = self._ldapHelper.search(
f"(sAMAccountName={username})", ['distinguishedName'])
if not rc:
continue
rc, raw_groups = self._ldapHelper.search(f"(&(member:1.2.840.113556.1.4.1941:={ldap_user[0]['distinguishedName']})(|(sophomorixType=adminclass)(sophomorixType=project)))", [
'sAMAccountName', 'sophomorixType', 'sophomorixMailList'])
if not rc:
continue
all_groups = list(map(lambda group: group['sAMAccountName'], raw_groups))
power_level = 100 if self._config['ADMIN_GROUP'] in all_groups else 50 if self._config['MODERATOR_GROUP'] in all_groups else 0
groups = list(filter(lambda group: group['sophomorixMailList'] == 'TRUE' or group['sophomorixType'] == "adminclass", raw_groups))
all_projects.extend(list(map(lambda group: group['sAMAccountName'], filter(
lambda group: 'sophomorixType' in group and group['sophomorixType'] == 'project', groups))))
rooms = await self._matrixHelper.get_rooms_of_user(user['name'])
groups = list(map(lambda group: group['sAMAccountName'], groups))
current_users.append({
'username': username,
'matrix_username': user['name'],
'groups': groups,
'rooms': rooms,
'power_level': power_level
})
all_projects = list(set(all_projects))
return current_users, all_projects
async def _create_project_rooms(self, projects):
logging.info("= (3/5) Creating project rooms =")
projects = list(map(lambda project: project.replace('p_', ''), projects))
await self._create_rooms(projects)
async def _create_rooms(self, rooms):
matrix_room_names = list(map(lambda room: room['name'], await self._get_managed_rooms()))
for room in rooms:
if room not in matrix_room_names:
logging.info(f"* Creating room {room}")
await self._matrixHelper.create_room(room, room)
async def _get_managed_rooms(self, with_power_levels=False):
matrix_rooms = await self._matrixHelper.get_rooms()
matrix_rooms = filter(
lambda room: room['name'] != None and room['creator'] == self._matrixUsername, matrix_rooms)
matrix_rooms = map(
lambda room: {'id': room['room_id'], 'name': room['name']}, matrix_rooms)
matrix_rooms = list(matrix_rooms)
if with_power_levels:
for i in range(len(matrix_rooms)):
logging.info(f"\t* Getting power levels of room {matrix_rooms[i]['name']}")
matrix_rooms[i]['power_levels'] = await self._matrixHelper.get_room_power_levels(matrix_rooms[i]['id'])
return matrix_rooms
def _get_default_rooms(self):
return self._config['DEFAULT_ROOMS'].split(',')
def _readConfig(self): def _readConfig(self):
requiredConfigKeys = [ requiredConfigKeys = [
'MATRIX_BOT_LDAP_URI', 'MATRIX_BOT_LDAP_URI',
'MATRIX_BOT_LDAP_BASE_DN', 'MATRIX_BOT_LDAP_BASE_DN',
'MATRIX_BOT_LDAP_BIND_DN', 'MATRIX_BOT_LDAP_BIND_DN',
'MATRIX_BOT_LDAP_BIND_DN_PASSWORD', 'MATRIX_BOT_LDAP_BIND_DN_PASSWORD',
'MATRIX_BOT_MATRIX_DOMAIN', 'MATRIX_BOT_MATRIX_DOMAIN',
'MATRIX_BOT_MATRIX_SPACE_ID', 'MATRIX_BOT_MATRIX_SPACE_ID',
'MATRIX_BOT_MATRIX_SERVER', 'MATRIX_BOT_MATRIX_SERVER',
'MATRIX_BOT_MATRIX_USERNAME', 'MATRIX_BOT_MATRIX_USERNAME',
'MATRIX_BOT_MATRIX_PASSWORD', 'MATRIX_BOT_MATRIX_PASSWORD',
'MATRIX_BOT_DEFAULT_ROOMS',
'MATRIX_BOT_ADMIN_GROUP',
'MATRIX_BOT_MODERATOR_GROUP'
] ]
allowedConfigKeys = [ allowedConfigKeys = [
@ -58,12 +216,15 @@ class MlmMatrixBot:
for configKey in requiredConfigKeys: for configKey in requiredConfigKeys:
if configKey not in os.environ: if configKey not in os.environ:
sys.exit (f"Required environment value {configKey} is not set") logging.error(f"Required environment value {configKey} is not set")
config[configKey.replace('MATRIX_BOT_', '')] = os.environ[configKey] sys.exit()
config[configKey.replace('MATRIX_BOT_', '')
] = os.environ[configKey]
for configKey in allowedConfigKeys: for configKey in allowedConfigKeys:
if configKey in os.environ: if configKey in os.environ:
config[configKey.replace('MATRIX_BOT_', '')] = os.environ[configKey] config[configKey.replace(
'MATRIX_BOT_', '')] = os.environ[configKey]
logging.info("CONFIG:") logging.info("CONFIG:")
for key, value in config.items(): for key, value in config.items():
@ -71,9 +232,17 @@ class MlmMatrixBot:
return config return config
if __name__ == "__main__":
async def main():
bot = MlmMatrixBot() bot = MlmMatrixBot()
await bot.init()
while True:
await bot.run()
time.sleep(60)
if __name__ == "__main__":
try: try:
asyncio.run(bot.run()) asyncio.run(main())
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Bye") logging.info("Bye")

View file

@ -1,40 +1,35 @@
from asyncio import current_task, protocols
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Any, Optional, Sequence from typing import Dict, Any, Optional, Sequence
import logging import logging
import nio import nio
import synapse_admin
import asyncio
class Schemas: class MatrixAdminClient:
room_state = { def __init__(self, server: str, access_token: str) -> None:
"type": "object", protocol = server.split("://")[0] + "://"
"properties": {"event_id": {"type": "string"}}, server = server.split("://")[1]
"required": ["event_id"],
}
self.user = synapse_admin.User(server, 443, access_token, protocol)
@dataclass self.room = synapse_admin.Room(server, 443, access_token, protocol)
class RoomStateResponse(nio.responses.Response): self.media = synapse_admin.Media(server, 443, access_token, protocol)
event_id: str = field() self.management = synapse_admin.Management(
server, 443, access_token, protocol)
@staticmethod
def create_error(parsed_dict):
return nio.responses.ErrorResponse.from_dict(parsed_dict)
@classmethod
def from_dict(cls, parsed_dict: Dict[Any, Any]):
try:
nio.validate_json(parsed_dict, Schemas.room_state)
except (nio.SchemaError, nio.ValidationError):
return cls.create_error(parsed_dict)
return cls(parsed_dict["event_id"])
class MatrixHelper: class MatrixHelper:
def __init__(self, server, username, space_id: str) -> None: def __init__(self, server: str, username, space_id: str, matrix_username: str) -> None:
self._client = nio.AsyncClient(server, username) self._client = nio.AsyncClient(server, username)
self._loggedIn = False self._loggedIn = False
self._space_id = space_id self._space_id = space_id
self._server = server
self._username = username
self._matrix_username = matrix_username
def __del__(self):
asyncio.run(self.logout())
async def login(self, password): async def login(self, password):
try: try:
@ -46,15 +41,23 @@ class MatrixHelper:
logging.error("The bot user is not in the space!") logging.error("The bot user is not in the space!")
return False return False
# admin login
self._admin_client = MatrixAdminClient(
self._server, self._client.access_token)
user = self._admin_client.user.query(self._username)
logging.info(f"Logged into Synapse-admin as {user['name']}")
return True return True
except Exception as e: except Exception as e:
print(e)
logging.error(f"Error while logging into matrix server!") logging.error(f"Error while logging into matrix server!")
return False return False
async def logout(self): async def logout(self):
await self._client.close() await self._client.close()
async def createRoom(self, name, alias): async def create_room(self, name, alias, joinable_for_space_members=False):
# we not only need to create the room but also have to add it to the space # we not only need to create the room but also have to add it to the space
initial_state = [ initial_state = [
{ {
@ -79,12 +82,64 @@ class MatrixHelper:
}, },
{ {
"type": "m.room.history_visibility", "type": "m.room.history_visibility",
"content": {"history_visibility": "invited"} "content": {"history_visibility": "shared"}
},
{
"type": "m.room.power_levels",
"content": {
"users": {
self._matrix_username: 200,
}
}
} }
] ]
if joinable_for_space_members:
initial_state.append({
"type": "m.room.guest_access",
"state_key": "",
"content": {"guest_access": "can_join"}
})
room = await self._client.room_create(visibility=nio.RoomVisibility.private, name=name, alias=alias, federate=False, initial_state=initial_state) room = await self._client.room_create(visibility=nio.RoomVisibility.private, name=name, alias=alias, federate=False, initial_state=initial_state)
print(room) assert room.room_id is not None
logging.info(
f"Created room {room.room_id} with alias {alias} and added it to space {self._space_id}")
return room return room
async def get_rooms(self):
return self._admin_client.room.lists()
async def get_users(self):
return self._admin_client.user.lists()
async def get_rooms_of_user(self, username):
return self._admin_client.user.joined_room(username)
async def add_user_to_room(self, user, room):
return self._admin_client.user.join_room(user, room)
async def remove_user_from_room(self, user, room):
# TODO: Ban users?
return await self._client.room_kick(room, user, "You have been removed automatically since you are no longer a member of the group associated with this room. If you think, this is an error, please contact us.")
async def get_room_power_levels(self, room):
current_state = await self._client.room_get_state_event(room, "m.room.power_levels")
return current_state.content['users']
async def set_user_power_level_in_room(self, user, room, power_level):
if user == self._matrix_username:
return False
if power_level != 100 and power_level != 50 and power_level != 0:
return False
users = await self.get_room_power_levels(room)
users[user] = power_level
if not self._matrix_username in users:
users[self._matrix_username] = 200
return await self._client.room_put_state(room, "m.room.power_levels", {
"users": users
})