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 = "*" igramscraper = "*"
twitterscraper = "*" twitterscraper = "*"
httpx = "*" httpx = "*"
pyyaml = "==5.3"
[dev-packages] [dev-packages]
pylint = "*" pylint = "*"

View File

@ -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
View File

@ -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())

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): 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:

View File

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

View File

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

View File

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

View File

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