Merge branch 'upstream-master'
This commit is contained in:
commit
f37651e1bd
1
Pipfile
1
Pipfile
|
@ -15,6 +15,7 @@ requests = "*"
|
||||||
igramscraper = "*"
|
igramscraper = "*"
|
||||||
twitterscraper = "*"
|
twitterscraper = "*"
|
||||||
httpx = "*"
|
httpx = "*"
|
||||||
|
pyyaml = "==5.3"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pylint = "*"
|
pylint = "*"
|
||||||
|
|
46
README.md
46
README.md
|
@ -215,6 +215,7 @@ MATRIX_ACCESS_TOKEN=MDAxOGxvYlotofcharacters53CgYAYFgo
|
||||||
MATRIX_SERVER=https://matrix.org
|
MATRIX_SERVER=https://matrix.org
|
||||||
JOIN_ON_INVITE=True
|
JOIN_ON_INVITE=True
|
||||||
BOT_OWNERS=@user1:matrix.org,@user2:matrix.org
|
BOT_OWNERS=@user1:matrix.org,@user2:matrix.org
|
||||||
|
DEBUG=False
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: without quotes!
|
Note: without quotes!
|
||||||
|
@ -227,21 +228,48 @@ docker-compose up
|
||||||
|
|
||||||
## Env variables
|
## Env variables
|
||||||
|
|
||||||
User, access token and server should be self-explanatory. Set JOIN_ON_INVITE to anything if you want the bot to
|
`MATRIX_USER`, `MATRIX_ACCESS_TOKEN` and `MATRIX_SERVER` should be self-explanatory.
|
||||||
join invites automatically (do not set it if you don't want it to join).
|
Set `JOIN_ON_INVITE` to anything if you want the bot to join invites automatically (do not set it if you don't want it to join).
|
||||||
|
|
||||||
You can set MATRIX_PASSWORD if you want to get access token. Normally you can use Riot to get it.
|
You can set `MATRIX_PASSWORD` if you want to get an access token automatically with a login.
|
||||||
|
Normally you can use Riot to get it.
|
||||||
|
|
||||||
BOT_OWNERS is a comma-separated list of matrix id's for the owners of the bot. Some commands require
|
`BOT_OWNERS` is a comma-separated list of matrix id's for the owners of the bot.
|
||||||
sender to be bot owner. Typically set your own id into it. Don't include bot itself in BOT_OWNERS if cron
|
Some commands require sender to be bot owner.
|
||||||
or any other module that can cause bot to send custom commands is used as it could potentially be used to run
|
Typically set your own id into it.
|
||||||
owner commands as the bot itself.
|
|
||||||
|
__*ATTENTION:*__ Don't include bot itself in `BOT_OWNERS` if cron or any other module that can cause bot to send custom commands is used, as it could potentially be used to run owner commands as the bot itself.
|
||||||
|
|
||||||
|
To enable debugging for the root logger set `DEBUG=True`.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Uses [python logging facility](https://docs.python.org/3/library/logging.html) to print information to the console. Customize it to your needs editing `config/logging.yml`.
|
||||||
|
See [logging.config documentation](https://docs.python.org/3/library/logging.config.html) for further information.
|
||||||
|
|
||||||
## Module API
|
## Module API
|
||||||
|
|
||||||
Just write a python file with desired command name and place it in modules. See current modules for
|
Just write a python file with desired command name and place it in modules. See current modules for
|
||||||
examples. No need to register it anywhere else.
|
examples. No need to register it anywhere else.
|
||||||
|
|
||||||
|
*Simple skeleton for a bot module:*
|
||||||
|
```python
|
||||||
|
|
||||||
|
class MatrixModule(BotModule):
|
||||||
|
|
||||||
|
async def matrix_message(self, bot, room, event):
|
||||||
|
args = event.body.split()
|
||||||
|
args.pop(0)
|
||||||
|
|
||||||
|
# Echo what they said back
|
||||||
|
self.logger.debug(f"room: {room.name} sender: {event.sender} wants an echo")
|
||||||
|
await bot.send_text(room, ' '.join(args))
|
||||||
|
|
||||||
|
def help(self):
|
||||||
|
return 'Echoes back what user has said'
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
Functions:
|
Functions:
|
||||||
|
|
||||||
* matrix_start - Called once on startup
|
* matrix_start - Called once on startup
|
||||||
|
@ -254,6 +282,10 @@ Functions:
|
||||||
|
|
||||||
You only need to implement the ones you need. See existing bots for examples.
|
You only need to implement the ones you need. See existing bots for examples.
|
||||||
|
|
||||||
|
Logging:
|
||||||
|
|
||||||
|
Use `self.logger` in your module to print information to the console.
|
||||||
|
|
||||||
Module settings are stored in Matrix account data.
|
Module settings are stored in Matrix account data.
|
||||||
|
|
||||||
If you write a new module, please make a PR if it's something useful for others.
|
If you write a new module, please make a PR if it's something useful for others.
|
||||||
|
|
78
bot.py
78
bot.py
|
@ -1,11 +1,13 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import functools
|
||||||
import glob
|
import glob
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import yaml
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
@ -40,24 +42,28 @@ class Bot:
|
||||||
self.pollcount = 0
|
self.pollcount = 0
|
||||||
self.poll_task = None
|
self.poll_task = None
|
||||||
self.owners = []
|
self.owners = []
|
||||||
self.debug = os.getenv("DEBUG")
|
self.debug = os.getenv("DEBUG", "false").lower() == "true"
|
||||||
|
self.logger = None
|
||||||
|
|
||||||
self.initializeLogger()
|
self.initialize_logger()
|
||||||
self.logger = logging.getLogger("hemppa")
|
|
||||||
self.logger.debug("Initialized")
|
|
||||||
|
|
||||||
def initializeLogger(self):
|
def initialize_logger(self):
|
||||||
|
|
||||||
if os.path.exists('config/logging.config'):
|
if os.path.exists('config/logging.yml'):
|
||||||
logging.config.fileConfig('config/logging.config')
|
with open('config/logging.yml') as f:
|
||||||
|
config = yaml.load(f, Loader=yaml.Loader)
|
||||||
|
logging.config.dictConfig(config)
|
||||||
else:
|
else:
|
||||||
log_format = '%(levelname)s - %(name)s - %(message)s'
|
log_format = '%(levelname)s - %(name)s - %(message)s'
|
||||||
logging.basicConfig(format=log_format)
|
logging.basicConfig(format=log_format)
|
||||||
|
|
||||||
|
self.logger = logging.getLogger("hemppa")
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
logging.root.setLevel(logging.DEBUG)
|
logging.root.setLevel(logging.DEBUG)
|
||||||
else:
|
self.logger.info("enabled debugging")
|
||||||
logging.root.setLevel(logging.INFO)
|
|
||||||
|
self.logger.debug("Logger initialized")
|
||||||
|
|
||||||
async def send_text(self, room, body):
|
async def send_text(self, room, body):
|
||||||
msg = {
|
msg = {
|
||||||
|
@ -76,10 +82,10 @@ class Bot:
|
||||||
await self.client.room_send(room.room_id, 'm.room.message', msg)
|
await self.client.room_send(room.room_id, 'm.room.message', msg)
|
||||||
|
|
||||||
def remove_callback(self, callback):
|
def remove_callback(self, callback):
|
||||||
for cb_object in bot.client.event_callbacks:
|
for cb_object in self.client.event_callbacks:
|
||||||
if cb_object.func == callback:
|
if cb_object.func == callback:
|
||||||
self.logger.info("remove callback")
|
self.logger.info("remove callback")
|
||||||
bot.client.event_callbacks.remove(cb_object)
|
self.client.event_callbacks.remove(cb_object)
|
||||||
|
|
||||||
def get_room_by_id(self, room_id):
|
def get_room_by_id(self, room_id):
|
||||||
return self.client.rooms[room_id]
|
return self.client.rooms[room_id]
|
||||||
|
@ -146,18 +152,16 @@ class Bot:
|
||||||
if moduleobject is not None:
|
if moduleobject is not None:
|
||||||
if moduleobject.enabled:
|
if moduleobject.enabled:
|
||||||
try:
|
try:
|
||||||
await moduleobject.matrix_message(bot, room, event)
|
await moduleobject.matrix_message(self, room, event)
|
||||||
except CommandRequiresAdmin:
|
except CommandRequiresAdmin:
|
||||||
await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.')
|
await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.')
|
||||||
except CommandRequiresOwner:
|
except CommandRequiresOwner:
|
||||||
await self.send_text(room, f'Sorry, only bot owner can run that command.')
|
await self.send_text(room, f'Sorry, only bot owner can run that command.')
|
||||||
except Exception:
|
except Exception:
|
||||||
await self.send_text(room,
|
await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
|
||||||
f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
|
|
||||||
traceback.print_exc(file=sys.stderr)
|
traceback.print_exc(file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"Unknown command: {command}")
|
self.logger.error(f"Unknown command: {command}")
|
||||||
|
|
||||||
# TODO Make this configurable
|
# TODO Make this configurable
|
||||||
# await self.send_text(room,
|
# await self.send_text(room,
|
||||||
# f"Sorry. I don't know what to do. Execute !help to get a list of available commands.")
|
# f"Sorry. I don't know what to do. Execute !help to get a list of available commands.")
|
||||||
|
@ -219,7 +223,7 @@ class Bot:
|
||||||
for modulename, moduleobject in self.modules.items():
|
for modulename, moduleobject in self.modules.items():
|
||||||
if moduleobject.enabled:
|
if moduleobject.enabled:
|
||||||
try:
|
try:
|
||||||
await moduleobject.matrix_poll(bot, self.pollcount)
|
await moduleobject.matrix_poll(self, self.pollcount)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc(file=sys.stderr)
|
traceback.print_exc(file=sys.stderr)
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
|
@ -280,7 +284,6 @@ class Bot:
|
||||||
self.logger.error("The environment variables MATRIX_SERVER, MATRIX_USER and BOT_OWNERS are mandatory")
|
self.logger.error("The environment variables MATRIX_SERVER, MATRIX_USER and BOT_OWNERS are mandatory")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.load_settings(self.get_account_data())
|
self.load_settings(self.get_account_data())
|
||||||
enabled_modules = [module for module_name, module in self.modules.items() if module.enabled]
|
enabled_modules = [module for module_name, module in self.modules.items() if module.enabled]
|
||||||
|
@ -288,7 +291,7 @@ class Bot:
|
||||||
for modulename, moduleobject in self.modules.items():
|
for modulename, moduleobject in self.modules.items():
|
||||||
if moduleobject.enabled:
|
if moduleobject.enabled:
|
||||||
try:
|
try:
|
||||||
moduleobject.matrix_start(bot)
|
moduleobject.matrix_start(self)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc(file=sys.stderr)
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
|
||||||
|
@ -296,7 +299,7 @@ class Bot:
|
||||||
self.logger.info(f'Stopping {len(self.modules)} modules..')
|
self.logger.info(f'Stopping {len(self.modules)} modules..')
|
||||||
for modulename, moduleobject in self.modules.items():
|
for modulename, moduleobject in self.modules.items():
|
||||||
try:
|
try:
|
||||||
moduleobject.matrix_stop(bot)
|
moduleobject.matrix_stop(self)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc(file=sys.stderr)
|
traceback.print_exc(file=sys.stderr)
|
||||||
|
|
||||||
|
@ -354,15 +357,30 @@ class Bot:
|
||||||
else:
|
else:
|
||||||
await self.client.client_session.close()
|
await self.client.client_session.close()
|
||||||
|
|
||||||
|
def handle_exit(self, signame, loop):
|
||||||
|
self.logger.info(f"Received signal {signame}")
|
||||||
|
if self.poll_task:
|
||||||
|
self.poll_task.cancel()
|
||||||
|
self.bot_task.cancel()
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
bot = Bot()
|
||||||
|
bot.init()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
for signame in {'SIGINT', 'SIGTERM'}:
|
||||||
|
loop.add_signal_handler(
|
||||||
|
getattr(signal, signame),
|
||||||
|
functools.partial(bot.handle_exit, signame, loop))
|
||||||
|
|
||||||
|
await bot.run()
|
||||||
|
await bot.shutdown()
|
||||||
|
|
||||||
|
|
||||||
bot = Bot()
|
|
||||||
bot.init()
|
|
||||||
try:
|
try:
|
||||||
asyncio.get_event_loop().run_until_complete(bot.run())
|
asyncio.run(main())
|
||||||
except KeyboardInterrupt:
|
except Exception as e:
|
||||||
if bot.poll_task:
|
print(e)
|
||||||
bot.poll_task.cancel()
|
|
||||||
bot.bot_task.cancel()
|
|
||||||
|
|
||||||
bot.stop()
|
|
||||||
asyncio.get_event_loop().run_until_complete(bot.shutdown())
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
version: 1
|
||||||
|
formatters:
|
||||||
|
hemppa:
|
||||||
|
format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s'
|
||||||
|
handlers:
|
||||||
|
console:
|
||||||
|
class: logging.StreamHandler
|
||||||
|
formatter: hemppa
|
||||||
|
stream: ext://sys.stdout
|
||||||
|
loggers:
|
||||||
|
hemppa:
|
||||||
|
level: NOTSET
|
||||||
|
root:
|
||||||
|
level: INFO
|
||||||
|
handlers: [console]
|
|
@ -95,7 +95,6 @@ class MatrixModule(BotModule):
|
||||||
async def enable_module(self, bot, room, event, module_name):
|
async def enable_module(self, bot, room, event, module_name):
|
||||||
bot.must_be_admin(room, event)
|
bot.must_be_admin(room, event)
|
||||||
self.logger.info(f"asked to enable {module_name}")
|
self.logger.info(f"asked to enable {module_name}")
|
||||||
|
|
||||||
if bot.modules.get(module_name):
|
if bot.modules.get(module_name):
|
||||||
module = bot.modules.get(module_name)
|
module = bot.modules.get(module_name)
|
||||||
module.enable()
|
module.enable()
|
||||||
|
@ -108,7 +107,6 @@ class MatrixModule(BotModule):
|
||||||
async def disable_module(self, bot, room, event, module_name):
|
async def disable_module(self, bot, room, event, module_name):
|
||||||
bot.must_be_admin(room, event)
|
bot.must_be_admin(room, event)
|
||||||
self.logger.info(f"asked to disable {module_name}")
|
self.logger.info(f"asked to disable {module_name}")
|
||||||
|
|
||||||
if bot.modules.get(module_name):
|
if bot.modules.get(module_name):
|
||||||
module = bot.modules.get(module_name)
|
module = bot.modules.get(module_name)
|
||||||
if module.can_be_disabled:
|
if module.can_be_disabled:
|
||||||
|
|
|
@ -32,7 +32,6 @@ class BotModule(ABC):
|
||||||
self.can_be_disabled = True
|
self.can_be_disabled = True
|
||||||
self.name = name
|
self.name = name
|
||||||
self.logger = logging.getLogger("module " + self.name)
|
self.logger = logging.getLogger("module " + self.name)
|
||||||
self.subcommands = dict()
|
|
||||||
|
|
||||||
def matrix_start(self, bot):
|
def matrix_start(self, bot):
|
||||||
"""Called once on startup
|
"""Called once on startup
|
||||||
|
|
|
@ -7,7 +7,8 @@ class MatrixModule(BotModule):
|
||||||
args.pop(0)
|
args.pop(0)
|
||||||
|
|
||||||
# Echo what they said back
|
# Echo what they said back
|
||||||
|
self.logger.debug(f"room: {room.name} sender: {event.sender} wants an echo")
|
||||||
await bot.send_text(room, ' '.join(args))
|
await bot.send_text(room, ' '.join(args))
|
||||||
|
|
||||||
def help(self):
|
def help(self):
|
||||||
return ('Echoes back what user has said')
|
return 'Echoes back what user has said'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from geopy.geocoders import Nominatim
|
from geopy.geocoders import Nominatim
|
||||||
from nio import RoomMessageUnknown, AsyncClient
|
from nio import RoomMessageUnknown
|
||||||
|
|
||||||
from modules.common.module import BotModule
|
from modules.common.module import BotModule
|
||||||
|
|
||||||
from modules.common.module import BotModule
|
from modules.common.module import BotModule
|
||||||
|
|
|
@ -3,7 +3,6 @@ from datetime import datetime
|
||||||
|
|
||||||
from pyteamup import Calendar
|
from pyteamup import Calendar
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# TeamUp calendar notifications
|
# TeamUp calendar notifications
|
||||||
#
|
#
|
||||||
|
|
Loading…
Reference in New Issue