Merge branch 'upstream-master'

This commit is contained in:
Frank Becker 2020-02-17 21:44:14 +01:00
commit f37651e1bd
9 changed files with 107 additions and 43 deletions

View File

@ -15,6 +15,7 @@ requests = "*"
igramscraper = "*"
twitterscraper = "*"
httpx = "*"
pyyaml = "==5.3"
[dev-packages]
pylint = "*"

View File

@ -215,6 +215,7 @@ MATRIX_ACCESS_TOKEN=MDAxOGxvYlotofcharacters53CgYAYFgo
MATRIX_SERVER=https://matrix.org
JOIN_ON_INVITE=True
BOT_OWNERS=@user1:matrix.org,@user2:matrix.org
DEBUG=False
```
Note: without quotes!
@ -227,21 +228,48 @@ docker-compose up
## Env variables
User, access token and server should be self-explanatory. 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).
`MATRIX_USER`, `MATRIX_ACCESS_TOKEN` and `MATRIX_SERVER` should be self-explanatory.
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
sender to be bot owner. Typically set your own id into it. 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.
`BOT_OWNERS` is a comma-separated list of matrix id's for the owners of the bot.
Some commands require sender to be bot owner.
Typically set your own id into it.
__*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
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.
*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:
* 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.
Logging:
Use `self.logger` in your module to print information to the console.
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.

78
bot.py
View File

@ -1,11 +1,13 @@
#!/usr/bin/env python3
import asyncio
import functools
import glob
import importlib
import json
import yaml
import os
import re
import signal
import sys
import traceback
import urllib.parse
@ -40,24 +42,28 @@ class Bot:
self.pollcount = 0
self.poll_task = None
self.owners = []
self.debug = os.getenv("DEBUG")
self.debug = os.getenv("DEBUG", "false").lower() == "true"
self.logger = None
self.initializeLogger()
self.logger = logging.getLogger("hemppa")
self.logger.debug("Initialized")
self.initialize_logger()
def initializeLogger(self):
def initialize_logger(self):
if os.path.exists('config/logging.config'):
logging.config.fileConfig('config/logging.config')
if os.path.exists('config/logging.yml'):
with open('config/logging.yml') as f:
config = yaml.load(f, Loader=yaml.Loader)
logging.config.dictConfig(config)
else:
log_format = '%(levelname)s - %(name)s - %(message)s'
logging.basicConfig(format=log_format)
self.logger = logging.getLogger("hemppa")
if self.debug:
logging.root.setLevel(logging.DEBUG)
else:
logging.root.setLevel(logging.INFO)
self.logger.info("enabled debugging")
self.logger.debug("Logger initialized")
async def send_text(self, room, body):
msg = {
@ -76,10 +82,10 @@ class Bot:
await self.client.room_send(room.room_id, 'm.room.message', msg)
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:
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):
return self.client.rooms[room_id]
@ -146,18 +152,16 @@ class Bot:
if moduleobject is not None:
if moduleobject.enabled:
try:
await moduleobject.matrix_message(bot, room, event)
await moduleobject.matrix_message(self, room, event)
except CommandRequiresAdmin:
await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.')
except CommandRequiresOwner:
await self.send_text(room, f'Sorry, only bot owner can run that command.')
except Exception:
await self.send_text(room,
f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
traceback.print_exc(file=sys.stderr)
else:
self.logger.error(f"Unknown command: {command}")
# TODO Make this configurable
# await self.send_text(room,
# 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():
if moduleobject.enabled:
try:
await moduleobject.matrix_poll(bot, self.pollcount)
await moduleobject.matrix_poll(self, self.pollcount)
except Exception:
traceback.print_exc(file=sys.stderr)
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")
sys.exit(1)
def start(self):
self.load_settings(self.get_account_data())
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():
if moduleobject.enabled:
try:
moduleobject.matrix_start(bot)
moduleobject.matrix_start(self)
except Exception:
traceback.print_exc(file=sys.stderr)
@ -296,7 +299,7 @@ class Bot:
self.logger.info(f'Stopping {len(self.modules)} modules..')
for modulename, moduleobject in self.modules.items():
try:
moduleobject.matrix_stop(bot)
moduleobject.matrix_stop(self)
except Exception:
traceback.print_exc(file=sys.stderr)
@ -354,15 +357,30 @@ class Bot:
else:
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:
asyncio.get_event_loop().run_until_complete(bot.run())
except KeyboardInterrupt:
if bot.poll_task:
bot.poll_task.cancel()
bot.bot_task.cancel()
bot.stop()
asyncio.get_event_loop().run_until_complete(bot.shutdown())
asyncio.run(main())
except Exception as e:
print(e)

15
config/logging.yml Normal file
View File

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

View File

@ -95,7 +95,6 @@ class MatrixModule(BotModule):
async def enable_module(self, bot, room, event, module_name):
bot.must_be_admin(room, event)
self.logger.info(f"asked to enable {module_name}")
if bot.modules.get(module_name):
module = bot.modules.get(module_name)
module.enable()
@ -108,7 +107,6 @@ class MatrixModule(BotModule):
async def disable_module(self, bot, room, event, module_name):
bot.must_be_admin(room, event)
self.logger.info(f"asked to disable {module_name}")
if bot.modules.get(module_name):
module = bot.modules.get(module_name)
if module.can_be_disabled:

View File

@ -32,7 +32,6 @@ class BotModule(ABC):
self.can_be_disabled = True
self.name = name
self.logger = logging.getLogger("module " + self.name)
self.subcommands = dict()
def matrix_start(self, bot):
"""Called once on startup

View File

@ -7,7 +7,8 @@ class MatrixModule(BotModule):
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')
return 'Echoes back what user has said'

View File

@ -1,5 +1,6 @@
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

View File

@ -3,7 +3,6 @@ from datetime import datetime
from pyteamup import Calendar
#
# TeamUp calendar notifications
#