Added help module, added googlecal module(wip), readme enhancements.

This commit is contained in:
Ville Ranki 2019-12-09 22:30:06 +02:00
parent d4bfe1f020
commit 5dc8c0b9c4
5 changed files with 280 additions and 7 deletions

View File

@ -6,6 +6,9 @@ name = "pypi"
[packages] [packages]
matrix-nio = "*" matrix-nio = "*"
geopy = "*" geopy = "*"
google-api-python-client = "*"
google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
[dev-packages] [dev-packages]
pylint = "*" pylint = "*"

View File

@ -1,15 +1,59 @@
# Hemppa - generic Matrix bot # Hemppa - generic modular Matrix bot
This bot is meant to be super easy platform to code Matrix bot functionality
with Python. It uses matrix-nio library https://github.com/poljar/matrix-nio/ .
Type !help in room with this bot running to list active modules.
## Module list ## Module list
Write !modulename in Matrix room where the bot is to use modules. ### Help
Prints help on existing modules.
### Echo ### Echo
Simple example module that just echoes what user said. Can be used as an Simple example module that just echoes what user said.
example for writing own modules.
## First ### Metar
Aviation weather metar service access.
### TAF
Aviation weather TAF service access.
### Uptime
Prints bot uptime.
### Google Calendar (WIP)
Displays changes and daily report of a google calendar to a room. This is a bit pain to set up, sorry.
To set up, you'll need to generate credentials.json file - see https://console.developers.google.com/apis/credentials
When credentials.json is present, you must authenticate the bot to access calendar. There will be a link in console like this:
``` text
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=907....
```
Open the link and authenticate as needed. A new file token.pickle will be created in the directory and bot will read it in future.
Now the bot should be usable.
Use !googlecal add [calendar id] to add new calendar to a room. The bot lists availble calendar ID's on startup and you can find them
in google calendar.
Commands:
* !googlecal - Show next 10 events in calendar
* !googlecal today - Show today's events
* !googlecal add [calendar id] - Add new calendar to room
* !googlecal calendars - List calendars in this room
## Bot setup
* Create a Matrix user * Create a Matrix user
* Get user's access token - In Riot Web see Settings / Help & about * Get user's access token - In Riot Web see Settings / Help & about
@ -51,4 +95,17 @@ join invites automatically.
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 access token. Normally you can use Riot to get it.
## Testing ## 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.
Functions:
* matrix_start - Called once on startup
* async matrix_message - Called when a message is sent to room starting with !module_name
* matrix_stop - Called once before exit
* async matrix_poll - Called every 10 seconds
* help - Return one-liner help text
You only need to implement the ones you need. See existing bots for examples

26
bot.py
View File

@ -15,6 +15,8 @@ class Bot:
client = None client = None
join_on_invite = False join_on_invite = False
modules = dict() modules = dict()
pollcount = 0
poll_task = None
async def send_text(self, room, body): async def send_text(self, room, body):
msg = { msg = {
@ -23,6 +25,14 @@ class Bot:
} }
await self.client.room_send(self.get_room_id(room), 'm.room.message', msg) await self.client.room_send(self.get_room_id(room), 'm.room.message', msg)
async def send_html(self, room, html):
msg = {
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": html
}
await self.client.room_send(self.get_room_id(room), 'm.room.message', msg)
def get_room_id(self, room): def get_room_id(self, room):
for roomid in self.client.rooms: for roomid in self.client.rooms:
if self.client.rooms[roomid].named_room_name() == room.named_room_name(): if self.client.rooms[roomid].named_room_name() == room.named_room_name():
@ -83,6 +93,17 @@ class Bot:
if moduleobject: if moduleobject:
self.modules[modulename] = moduleobject self.modules[modulename] = moduleobject
async def poll_timer(self):
while True:
self.pollcount = self.pollcount + 1
for modulename, moduleobject in self.modules.items():
if "matrix_poll" in dir(moduleobject):
try:
await moduleobject.matrix_poll(bot, self.pollcount)
except:
traceback.print_exc(file=sys.stderr)
await asyncio.sleep(10)
def init(self): def init(self):
self.client = AsyncClient(os.environ['MATRIX_SERVER'], os.environ['MATRIX_USER']) self.client = AsyncClient(os.environ['MATRIX_SERVER'], os.environ['MATRIX_USER'])
self.client.access_token = os.getenv('MATRIX_ACCESS_TOKEN') self.client.access_token = os.getenv('MATRIX_ACCESS_TOKEN')
@ -116,6 +137,7 @@ class Bot:
print("Logged in with password, access token:", self.client.access_token) print("Logged in with password, access token:", self.client.access_token)
await self.client.sync() await self.client.sync()
self.poll_task = asyncio.get_event_loop().create_task(self.poll_timer())
if self.client.logged_in: if self.client.logged_in:
self.client.add_event_callback(self.message_cb, RoomMessageText) self.client.add_event_callback(self.message_cb, RoomMessageText)
@ -134,5 +156,7 @@ bot.init()
try: try:
asyncio.get_event_loop().run_until_complete(bot.run()) asyncio.get_event_loop().run_until_complete(bot.run())
except KeyboardInterrupt: except KeyboardInterrupt:
pass if bot.poll_task:
bot.poll_task.cancel()
bot.stop() bot.stop()

173
modules/googlecal.py Normal file
View File

@ -0,0 +1,173 @@
from __future__ import print_function
from datetime import datetime
import datetime
import pickle
import os.path
import time
import os
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
#
# Google calendar notifications
#
# Note: Provide a token.pickle file for the service.
# It's created on first run (run from console!) and
# can be copied to another computer.
#
# ENV variables:
#
# Google calendar creds file: (defaults to this)
# GCAL_CREDENTIALS="credentials.json"
#
class MatrixModule:
def matrix_start(self, bot):
self.bot = bot
self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
self.credentials_file = "credentials.json"
if os.getenv("GCAL_CREDENTIALS"):
self.credentials_file = os.getenv("GCAL_CREDENTIALS")
self.service = None
self.report_time = 8
self.last_report_date = None
self.calendar_rooms = dict() # Contains rooms -> [calid, calid] ..
creds = None
if not os.path.exists(self.credentials_file):
return # No-op if not set up
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
self.credentials_file, self.SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
self.service = build('calendar', 'v3', credentials=creds)
calendar_list = self.service.calendarList().list().execute()['items']
print(f'Google calendar set up successfully with access to {len(calendar_list)} calendars:\n')
for calendar in calendar_list:
print(calendar['summary'] + ' - ' + calendar['id'])
async def matrix_message(self, bot, room, event):
if not self.service:
await bot.send_text(room, 'Google calendar not set up for this bot.')
return
args = event.body.split()
events = []
calendars = self.calendar_rooms.get(room) or []
if len(args) == 2:
if args[1] == 'today':
for calid in calendars:
print('Listing events in cal', calid)
events = events + self.list_today(calid)
if args[1] == 'calendars':
await bot.send_text(room, 'Calendars in this room: ' + str(self.calendar_rooms.get(room)))
elif len(args) == 3:
if args[1] == 'add':
calid = args[2]
print(f'Adding calendar {calid} to room {room}')
if self.calendar_rooms.get(room):
self.calendar_rooms[room].append(calid)
else:
self.calendar_rooms[room] = [calid]
print(f'Calendars now for this room {self.calendar_rooms[room]}')
await bot.send_text(room, 'Added new google calendar to this room')
else:
for calid in calendars:
print('Listing events in cal', calid)
events = events + self.list_upcoming(calid)
if len(events) == 0:
await bot.send_text(room, 'No events found.')
else:
print(f'Found {len(events)} events')
await self.send_events(bot, events, room)
async def send_events(self, bot, events, room):
for event in events:
start = event['start'].get('dateTime', event['start'].get('date'))
await bot.send_text(room, f"{self.parseDate(start)} {event['summary']}")
# await bot.send_text(room, f"{self.parseDate(start)} {event['summary']} {event['htmlLink']}")
# await bot.send_html(room, self.parseDate(start) + " <a href=\"" + event['htmlLink'] + "\">" + event['summary'] + "</a>")
def list_upcoming(self, calid):
startTime = datetime.datetime.utcnow()
now = startTime.isoformat() + 'Z'
events_result = self.service.events().list(calendarId=calid, timeMin=now,
maxResults=10, singleEvents=True,
orderBy='startTime').execute()
events = events_result.get('items', [])
return events
def list_today(self, calid):
startTime = datetime.datetime.utcnow()
startTime = startTime - datetime.timedelta(hours=startTime.hour, minutes=startTime.minute)
endTime = startTime + datetime.timedelta(hours=24)
now = startTime.isoformat() + 'Z'
end = endTime.isoformat() + 'Z'
events_result = self.service.events().list(calendarId=calid, timeMin=now,
timeMax=end, maxResults=10, singleEvents=True,
orderBy='startTime').execute()
events = events_result.get('items', [])
return events
async def matrix_poll(self, bot, pollcount):
if not self.service:
return
if pollcount % (6 * 5) == 0: # Poll every 5 min
pass # Not implemented yet
needs_send = False
today = datetime.datetime.now()
since_last = 999
# Bot's been started
if self.last_report_date:
since_last = (today - self.last_report_date).total_seconds() / 60 / 60
if since_last > 20 and today.hour >= self.report_time:
needs_send = True
if needs_send:
self.last_report_date = today
for room in self.calendar_rooms:
events = []
for calid in self.calendar_rooms.get(room):
events = events + self.list_today(calid)
await self.send_events(bot, events, room)
def help(self):
return('Google calendar. Lists 10 next events by default. today = list today\'s events.')
def parseDate(self, start):
try:
dt = datetime.datetime.strptime(start, '%Y-%m-%dT%H:%M:%S%z')
return dt.strftime("%d.%m %H:%M")
except ValueError:
dt = datetime.datetime.strptime(start, '%Y-%m-%d')
return dt.strftime("%d.%m")

16
modules/help.py Normal file
View File

@ -0,0 +1,16 @@
class MatrixModule:
async def matrix_message(self, bot, room, event):
msg = 'This is Hemppa, a generic Matrix bot. Known commands:\n\n'
for modulename, moduleobject in bot.modules.items():
msg = msg + '!' + modulename
try:
msg = msg + ' - ' + moduleobject.help() + '\n'
except AttributeError:
pass
msg + msg + '\n'
msg = msg + "\nAdd your own commands at https://github.com/vranki/hemppa"
await bot.send_text(room, msg)
def help(self):
return('Prints help on commands')