Initial commit

This commit is contained in:
AO-IT Net
2026-02-15 13:26:30 +01:00
commit 1a27536e14
8 changed files with 484 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
*.db
*.conf
*.log
*.bak
testing/
# ---> Python
tmp/
*.tmp
.vscode/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
+10
View File
@@ -0,0 +1,10 @@
# Template Bot for Matrix
This template run a basic matrix-bot with dynamic addable skills.
Skill Template: https://git.ao-it.net/templatecity/bot-matrix-skill
## requirements
- python libs see requirements.txt
- matrix user
## limits
- actually this bot is not designed for encryptet channels
+11
View File
@@ -0,0 +1,11 @@
---
general:
botname: Bot Name
domain: example.com
# all without domain
user: "bot"
pass: "botpassword"
status_room: "!roomid"
skills:
# all to loading skills. optional with own configs
BaseSkills:
+192
View File
@@ -0,0 +1,192 @@
import logging
import argparse
import os
import asyncio
import yaml
import importlib
import pathlib
from nio import AsyncClient, LoginResponse
from skills import *
logging.basicConfig(format='[%(asctime)s] %(levelname)s: %(message)s', level=logging.INFO)
class MatrixBot:
__AUTHOR__ = 'anima'
__VERSION__ = '0.6.1'
def __init__(self, config_file: str = 'bot.conf', session_file: str = 'session.tmp') -> None:
"""
Args:
config_file (str, optional): path to config file
session_file (str, optional): path to session file
"""
self.botname = 'Bot'
self.config_file = config_file
self.session_file = session_file
self._config = dict()
self.silence = False
self.skills = list()
self._find_skills()
if self._load_config():
self.client = AsyncClient(self.homeserver, self.username)
self.helper = BotHelper(self.client)
self._activate_skills()
else:
logging.critical('no config file was found')
exit(1)
def _load_config(self) -> bool:
"""load given config file"""
if not os.path.exists(self.config_file):
return False
logging.info(f'load config from file {self.config_file}')
with open(self.config_file, 'r') as f:
self._config = yaml.safe_load(f)
self.botname = self._config['general']['botname']
self.domain = self._config['general']['domain']
self.homeserver = f"https://{self.domain}"
self.username = f"@{self._config['general']['user']}:{self._config['general']['domain']}"
self.__password = self._config['general']['pass']
del self._config['general']['pass']
return True
def _save_session(self) -> bool:
"""save session data as file
Returns:
bool: True if file was saved
"""
session = {
'access_token': self.session.access_token,
'device_id': self.session.device_id,
'user_id': self.user_id,
'next_batch': self.client.next_batch
}
logging.debug('save session file')
with open(self.session_file, 'w') as f:
yaml.dump(session, f)
return True
def _load_session(self) -> bool:
"""load session date from file
Returns:
bool: True if file was found and load
"""
if not os.path.exists(self.session_file):
return False
logging.info(f'restore session from file {self.session_file}')
with open(self.session_file, 'r') as f:
session = yaml.safe_load(f)
self.client.access_token = session['access_token']
self.client.device_id = session['device_id']
self.client.user_id = session["user_id"]
self.client.next_batch = session['next_batch']
return True
async def _login(self) -> bool:
"""restore login with session file or run full login process
Returns:
bool: True when bot has a valid login
"""
if self._load_session():
self.session = await self.client.sync(timeout=3000)
if self.session.transport_response.ok:
logging.debug('restore session successfully')
return True
else:
logging.warning('no session file was found or file is broken')
logging.debug(f'try login with account {self.username}')
self.session = await self.client.login(self.__password)
if isinstance(session, LoginResponse):
logging.info(f'login successfull with account {self.username} on server {self.homeserver}')
self._save_session()
return True
return False
def _find_skills(self):
"""search all modules in skill folder and init these"""
skills_path = pathlib.Path(__file__).parent / "skills"
for module in skills_path.iterdir():
if module.is_dir() and (module / "__init__.py").exists():
importlib.import_module(f"skills.{module.name}")
def _activate_skills(self):
"""activate all skills which defined in config file"""
for skill in self._config['skills'].keys():
self.load_skill(skill_name=skill, config=self._config['skills'][skill])
def load_skill(self, skill_name: str, config: dict = None) -> bool:
"""load skill from skill registry
Args:
skill_name (str): name of skill (same as classname)
config (dict, optional): additional config. Defaults to None.
Returns:
bool: True is skill is loaded
"""
if not skill_name in Skill.registry.keys():
return False
logging.info(f'load skill {skill_name} version {Skill.registry[skill_name].__VERSION__}')
skill = Skill.registry[skill_name](client=self.client, config=config)
skill.load()
self.skills.append(skill)
return True
def unload_skill(self):
pass
async def send_status(self, message: str) -> bool:
"""send a message to a predefined room
Args:
message (str): message content
Returns:
bool: True if message send
"""
if self.silence:
return False
await self.helper.send_message(f"{self._config['general']['status_room']}:{self.domain}", message)
return True
async def run(self):
if await self._login():
await self.send_status(f"{self.botname} Online!")
await self.client.sync_forever(timeout=30000)
def parse_args() -> argparse.Namespace:
"""evaluates given arguments
Returns:
argsparse.Namespace: Namespace Object with all arguments insert (use: args.long_name_of_argument)
"""
argp = argparse.ArgumentParser(description=__doc__)
# Default args
argp.add_argument('-v', '--verbose', action='count', default=0,
help='increase output verbosity (use up to 3 times)')
argp.add_argument('-c', '--config', default='aoc.conf',
help='path to config file')
argp.add_argument('-s', '--session', default='session.tmp',
help='path to session file')
args = argp.parse_args()
return args
async def main():
args = parse_args()
if 1 <= args.verbose <= 3:
logging.getLogger().setLevel(40 - (args.verbose * 10))
bot = MatrixBot(config_file=args.config, session_file=args.session)
bot.silence = True
await bot.run()
if __name__ == '__main__':
asyncio.run(main())
+3
View File
@@ -0,0 +1,3 @@
matrix-nio
requests
pyyaml
+2
View File
@@ -0,0 +1,2 @@
from .helper import BotHelper
from .skill import Skill
+41
View File
@@ -0,0 +1,41 @@
import logging
import time
import asyncio
from nio import RoomMessageText
logging.basicConfig(format='[%(asctime)s] %(levelname)s: %(message)s', level=logging.INFO)
class BotHelper:
"""a helper class for frequently need actions"""
__AUTHOR__ = 'anima'
__VERSION__ = '0.0.2'
def __init__(self, client):
self.client = client
self.starttime = int(time.time() * 1000)
def validate_message(self, event: RoomMessageText):
"""validate incomming messages befor bot start his work"""
if self.client.user == event.sender:
logging.debug(f"no reaction on messgaes from bot it self")
return False
if self.starttime > event.server_timestamp:
logging.debug(f"no reaction on messgaes befor bot start")
return False
return True
async def send_message(self, room: str, message: str):
"""send a basic text message to a room
Args:
room (str): room id without domain part (like !roomid)
message (str): simpl text message
"""
logging.debug(f'send message: {message}')
await self.client.room_send(
room,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": message
}
)
+39
View File
@@ -0,0 +1,39 @@
import logging
from .helper import BotHelper
logging.basicConfig(format='[%(asctime)s] %(levelname)s: %(message)s', level=logging.INFO)
class Skill:
"""abstract class for bot skills"""
__AUTHOR__ = 'anima'
__VERSION__ = '0.1.0'
__NAME__ = 'Example Skill'
registry: dict[str, Skill] = {}
def __init_subclass__(cls, **kwargs):
"""add loaded subclasses to regestry"""
super().__init_subclass__(**kwargs)
if cls.__name__ != "Skill" and issubclass(cls, Skill):
logging.info(f"add skill {cls.__name__} to regestry")
Skill.registry[cls.__name__] = cls
def __init__(self, client: type, config: dict = None):
"""minimal init for a skill class
Args:
client (AsyncClient): bot client handler
config (dict, optional): additional configs from config file. Defaults to None.
"""
self.client = client
self.config = config
self.helper = BotHelper(self.client)
def load(self):
"""if skill activate it will run this class"""
raise NotImplementedError()
if __name__ == '__main__':
skill = Skill(None)
print(f"Load {skill.__NAME__} version {skill.__VERSION__}")
skill.load()