Added help module, added googlecal module(wip), readme enhancements.
This commit is contained in:
parent
d4bfe1f020
commit
5dc8c0b9c4
3
Pipfile
3
Pipfile
|
@ -6,6 +6,9 @@ name = "pypi"
|
|||
[packages]
|
||||
matrix-nio = "*"
|
||||
geopy = "*"
|
||||
google-api-python-client = "*"
|
||||
google-auth-httplib2 = "*"
|
||||
google-auth-oauthlib = "*"
|
||||
|
||||
[dev-packages]
|
||||
pylint = "*"
|
||||
|
|
69
README.md
69
README.md
|
@ -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
|
||||
|
||||
Write !modulename in Matrix room where the bot is to use modules.
|
||||
### Help
|
||||
|
||||
Prints help on existing modules.
|
||||
|
||||
### Echo
|
||||
|
||||
Simple example module that just echoes what user said. Can be used as an
|
||||
example for writing own modules.
|
||||
Simple example module that just echoes what user said.
|
||||
|
||||
## 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
|
||||
* 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.
|
||||
|
||||
## 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
26
bot.py
|
@ -15,6 +15,8 @@ class Bot:
|
|||
client = None
|
||||
join_on_invite = False
|
||||
modules = dict()
|
||||
pollcount = 0
|
||||
poll_task = None
|
||||
|
||||
async def send_text(self, room, body):
|
||||
msg = {
|
||||
|
@ -23,6 +25,14 @@ class Bot:
|
|||
}
|
||||
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):
|
||||
for roomid in self.client.rooms:
|
||||
if self.client.rooms[roomid].named_room_name() == room.named_room_name():
|
||||
|
@ -83,6 +93,17 @@ class Bot:
|
|||
if 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):
|
||||
self.client = AsyncClient(os.environ['MATRIX_SERVER'], os.environ['MATRIX_USER'])
|
||||
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)
|
||||
|
||||
await self.client.sync()
|
||||
self.poll_task = asyncio.get_event_loop().create_task(self.poll_timer())
|
||||
|
||||
if self.client.logged_in:
|
||||
self.client.add_event_callback(self.message_cb, RoomMessageText)
|
||||
|
@ -134,5 +156,7 @@ bot.init()
|
|||
try:
|
||||
asyncio.get_event_loop().run_until_complete(bot.run())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
if bot.poll_task:
|
||||
bot.poll_task.cancel()
|
||||
|
||||
bot.stop()
|
||||
|
|
|
@ -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")
|
|
@ -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')
|
Loading…
Reference in New Issue