Chore: some initial draft
This commit is contained in:
parent
50b1feecfe
commit
77af9a9428
8 changed files with 361 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -160,3 +160,4 @@ cython_debug/
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
run.sh
|
8
Dockerfile
Normal file
8
Dockerfile
Normal 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
13
run.sh.example
Normal 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
74
src/ldapHelper.py
Normal 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
79
src/main.py
Normal 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
90
src/matrixHelper.py
Normal 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
|
93
src/objectStorageHelper.py
Normal file
93
src/objectStorageHelper.py
Normal 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
3
src/requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
python-ldap==3.4.3
|
||||||
|
coloredlogs==15.0.1
|
||||||
|
matrix-nio==0.19.0
|
Loading…
Reference in a new issue