From 77af9a9428b137138d3ce62ccd1ef68afe27ef16 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Wed, 28 Sep 2022 20:04:21 +0200 Subject: [PATCH] Chore: some initial draft --- .gitignore | 1 + Dockerfile | 8 ++++ run.sh.example | 13 ++++++ src/ldapHelper.py | 74 ++++++++++++++++++++++++++++++ src/main.py | 79 ++++++++++++++++++++++++++++++++ src/matrixHelper.py | 90 ++++++++++++++++++++++++++++++++++++ src/objectStorageHelper.py | 93 ++++++++++++++++++++++++++++++++++++++ src/requirements.txt | 3 ++ 8 files changed, 361 insertions(+) create mode 100644 Dockerfile create mode 100644 run.sh.example create mode 100644 src/ldapHelper.py create mode 100644 src/main.py create mode 100644 src/matrixHelper.py create mode 100644 src/objectStorageHelper.py create mode 100644 src/requirements.txt diff --git a/.gitignore b/.gitignore index 5d381cc..abb6ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +run.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a50f265 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3-alpine + +RUN apk --no-cache add build-base openldap-dev python2-dev python3-dev +RUN pip3 install -r requirements.txt + +COPY src/* ./ + +ENTRYPOINT [ "python3", "main.py" ] \ No newline at end of file diff --git a/run.sh.example b/run.sh.example new file mode 100644 index 0000000..3f9f710 --- /dev/null +++ b/run.sh.example @@ -0,0 +1,13 @@ +#!/bin/bash + +export MATRIX_BOT_LDAP_URI="ldap://10.0.0.1" +export MATRIX_BOT_LDAP_BASE_DN=DC=linuxmuster,DC=lan +export MATRIX_BOT_LDAP_BIND_DN=CN=global-binduser,OU=Management,OU=GLOBAL,DC=linuxmuster,DC=lan +export MATRIX_BOT_LDAP_BIND_DN_PASSWORD=SomeSuperSafePassword +export MATRIX_BOT_MATRIX_SERVER="https://matrix.org" +export MATRIX_BOT_MATRIX_DOMAIN="matrix.org" +export MATRIX_BOT_MATRIX_SPACE_ID="!bajjed:matrix.org" +export MATRIX_BOT_MATRIX_USERNAME="synapse" +export MATRIX_BOT_MATRIX_PASSWORD="SomeSuperSafePassword" + +python3 src/main.py \ No newline at end of file diff --git a/src/ldapHelper.py b/src/ldapHelper.py new file mode 100644 index 0000000..df4ae44 --- /dev/null +++ b/src/ldapHelper.py @@ -0,0 +1,74 @@ +# Copied from https://github.com/linuxmuster/linuxmuster-mailcow/blob/4409c726ebbea794f1ea6481684dd3c54339d0e0/src/ldapHelper.py + +import ldap, logging + +class LdapHelper: + def __init__(self, ldapUri, ldapBindDn, ldapBindPassword, ldapBaseDn): + self._uri = ldapUri + self._bindDn = ldapBindDn + self._bindPassword = ldapBindPassword + self._baseDn = ldapBaseDn + + def bind(self): + try: + self._ldapConnection = ldap.initialize(f"{self._uri}") + self._ldapConnection.set_option(ldap.OPT_REFERRALS, 0) + self._ldapConnection.simple_bind_s(self._bindDn, self._bindPassword) + return True + except Exception as e: + logging.critical("!!! Error binding to ldap! {} !!!".format(e)) + return False + + def unbind(self): + if self._ldapConnection != None: + self._ldapConnection.unbind_s() + self._ldapConnection = None + + def search(self, filter, attrlist=None): + if self._ldapConnection == None: + logging.critical("Cannot talk to LDAP") + return False, None + + try: + rawResults = self._ldapConnection.search_s( + self._baseDn, + ldap.SCOPE_SUBTREE, + filter, + attrlist + ) + except Exception as e: + logging.critical("Error executing LDAP search!") + print(e) + return False, None + + try: + processedResults = [] + + if len(rawResults) <= 0 or rawResults[0][0] == None: + return False, None + + for dn, rawResult in rawResults: + if not dn: + continue + + processedResult = {} + + for attribute, rawValue in rawResult.items(): + try: + if len(rawValue) == 1: + processedResult[attribute] = str(rawValue[0].decode()) + elif len(rawValue) > 0: + processedResult[attribute] = [] + for rawItem in rawValue: + processedResult[attribute].append(str(rawItem.decode())) + + except UnicodeDecodeError: + continue + + processedResults.append(processedResult) + + return True, processedResults + + except Exception as e: + print(e) + return False, None \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..3fe7880 --- /dev/null +++ b/src/main.py @@ -0,0 +1,79 @@ +import sys, coloredlogs, logging, os, time, asyncio +from urllib import response +from ldapHelper import LdapHelper +from matrixHelper import MatrixHelper + +coloredlogs.install(level='INFO', fmt='%(asctime)s - [%(levelname)s] %(message)s') + +class MlmMatrixBot: + def __init__(self) -> None: + self._config = self._readConfig() + + self._ldapHelper = LdapHelper(self._config['LDAP_URI'], self._config['LDAP_BIND_DN'], self._config['LDAP_BIND_DN_PASSWORD'], self._config['LDAP_BASE_DN']) + self._matrixHelper = MatrixHelper(self._config['MATRIX_SERVER'], f"@{self._config['MATRIX_USERNAME']}:{self._config['MATRIX_DOMAIN']}", self._config['MATRIX_SPACE_ID']) + + def __del__(self): + self._ldapHelper.unbind() + asyncio.run(self._matrixHelper.logout()) + + async def run(self): + if not self._ldapHelper.bind(): + return False + + + print(self._ldapHelper.search("sophomorixType=project")) + + if not await self._matrixHelper.login(self._config['MATRIX_PASSWORD']): + return False + + + 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")) + + await self._matrixHelper._client.sync_forever() + + pass + + def _readConfig(self): + requiredConfigKeys = [ + 'MATRIX_BOT_LDAP_URI', + 'MATRIX_BOT_LDAP_BASE_DN', + 'MATRIX_BOT_LDAP_BIND_DN', + 'MATRIX_BOT_LDAP_BIND_DN_PASSWORD', + 'MATRIX_BOT_MATRIX_DOMAIN', + 'MATRIX_BOT_MATRIX_SPACE_ID', + 'MATRIX_BOT_MATRIX_SERVER', + 'MATRIX_BOT_MATRIX_USERNAME', + 'MATRIX_BOT_MATRIX_PASSWORD', + ] + + allowedConfigKeys = [ + ] + + config = { + } + + for configKey in requiredConfigKeys: + if configKey not in os.environ: + sys.exit (f"Required environment value {configKey} is not set") + config[configKey.replace('MATRIX_BOT_', '')] = os.environ[configKey] + + for configKey in allowedConfigKeys: + if configKey in os.environ: + config[configKey.replace('MATRIX_BOT_', '')] = os.environ[configKey] + + logging.info("CONFIG:") + for key, value in config.items(): + logging.info(" * {:25}: {}".format(key, value)) + + return config + +if __name__ == "__main__": + bot = MlmMatrixBot() + try: + asyncio.run(bot.run()) + except KeyboardInterrupt: + logging.info("Bye") \ No newline at end of file diff --git a/src/matrixHelper.py b/src/matrixHelper.py new file mode 100644 index 0000000..1da7e68 --- /dev/null +++ b/src/matrixHelper.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass, field +from typing import Dict, Any, Optional, Sequence +import logging +import nio + + +class Schemas: + room_state = { + "type": "object", + "properties": {"event_id": {"type": "string"}}, + "required": ["event_id"], + } + + +@dataclass +class RoomStateResponse(nio.responses.Response): + event_id: str = field() + + @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: + def __init__(self, server, username, space_id: str) -> None: + self._client = nio.AsyncClient(server, username) + self._loggedIn = False + self._space_id = space_id + + async def login(self, password): + try: + await self._client.login(password) + self._loggedIn = True + logging.info(f"Logged into Matrix as {await self._client.get_displayname()}") + rooms = await self._client.joined_rooms() + if self._space_id not in rooms.rooms: + logging.error("The bot user is not in the space!") + return False + + return True + except Exception as e: + logging.error(f"Error while logging into matrix server!") + return False + + async def logout(self): + await self._client.close() + + async def createRoom(self, name, alias): + # we not only need to create the room but also have to add it to the space + initial_state = [ + { + "type": "m.space.parent", + "state_key": self._space_id, + "content": { + "canonical": True, + "via": [self._space_id.split(":")[1]], + } + }, + { + "type": "m.room.join_rules", + "content": { + "join_rule": "restricted", + "allow": [ + { + "type": "m.room_membership", + "room_id": self._space_id + } + ] + } + }, + { + "type": "m.room.history_visibility", + "content": {"history_visibility": "invited"} + } + ] + room = await self._client.room_create(visibility=nio.RoomVisibility.private, name=name, alias=alias, federate=False, initial_state=initial_state) + + print(room) + logging.info( + f"Created room {room.room_id} with alias {alias} and added it to space {self._space_id}") + return room diff --git a/src/objectStorageHelper.py b/src/objectStorageHelper.py new file mode 100644 index 0000000..118b82e --- /dev/null +++ b/src/objectStorageHelper.py @@ -0,0 +1,93 @@ +import math + +class TemporaryObjectListStorage: + primaryKey = "INVALID" + + def __init__(self): + self._current = {} + self._managed = {} + self._addQueue = {} + self._updateQueue = {} + self._killQueue = {} + + self._primaryKey = self.primaryKey + + def loadRawData(self, rawData): + for element in rawData: + self._current[element[self._primaryKey]] = element + if self._checkElementValidity(element): + self._managed[element[self._primaryKey]] = element + self._killQueue[element[self._primaryKey]] = element + + def addElement(self, element, elementId): + """ + This function safely adds an element: + - If it exists and is unchanged, it removes it from the kill queue + - If it exists and has changed, it adds it to the update queue + - If it is unmanaged, it retruns false + - If it does not exist, it adds it to the add queue + """ + if elementId in self._managed: + if elementId in self._killQueue: + del self._killQueue[elementId] + + if self._checkElementChanges(element, elementId): + self._updateQueue[elementId] = element + + elif elementId in self._current: + return False + + elif elementId not in self._addQueue: + self._addQueue[elementId] = element + + return True + + def getQueueAsList(self, queue): + elementList = [] + for _, value in queue.items(): + elementList.append(value) + return elementList + + def addQueue(self): + return self.getQueueAsList(self._addQueue) + + + def updateQueue(self): + queue = [] + for key, value in self._updateQueue.items(): + queue.append({ + "attr": value, + "items": [key] + }) + return queue + + + def killQueue(self): + return self.getQueueAsList(self._killQueue) + + def queuesAreEmpty(self): + return len(self._killQueue) == 0 and len(self._addQueue) == 0 and len(self._updateQueue) == 0 + + def getQueueCountsString(self, descriptor): + return f"Going to add {len(self._addQueue)} {descriptor}, update {len(self._updateQueue)} {descriptor} and kill {len(self._killQueue)} {descriptor}" + + def _checkElementChanges(self, element, elementId): + """ + Checks if an element has changed + :returns: True if changed, False if not + """ + currentElement = self._managed[elementId] + + for key, value in element.items(): + if self._checkElementValueDelta(key, currentElement, value): + #currentValue = "" if key not in currentElement else currentElement[key] + #print(f"Found delta in {key}. Current: {currentValue} new: {value}") + return True + + return False + + def _checkElementValueDelta(self, key, currentElement, newValue): + return key not in currentElement or currentElement[key] != newValue + + def _checkElementValidity(self, element): + return True \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..cc9cbd1 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,3 @@ +python-ldap==3.4.3 +coloredlogs==15.0.1 +matrix-nio==0.19.0 \ No newline at end of file