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.
|
||||
#.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