Chore: some initial draft

main
Dorian Zedler 2022-09-28 20:04:21 +02:00
parent 50b1feecfe
commit 77af9a9428
Signed by: dozedler
GPG Key ID: 989DE36109AFA354
8 changed files with 361 additions and 0 deletions

1
.gitignore vendored
View File

@ -160,3 +160,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
run.sh

8
Dockerfile Normal file
View File

@ -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" ]

13
run.sh.example Normal file
View File

@ -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

74
src/ldapHelper.py Normal file
View File

@ -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

79
src/main.py Normal file
View File

@ -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")

90
src/matrixHelper.py Normal file
View File

@ -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

View File

@ -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

3
src/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
python-ldap==3.4.3
coloredlogs==15.0.1
matrix-nio==0.19.0