Event dispatching, simple parsing and more in v0.15

This release is another one full of under the hood changes. Mostly thanks to @Cadair and the rest of the opsdroid Matrix community. These changes bring some awesome improvements to opsdroid but do include breaking changes, be sure to check the full release notes below for information.

Event dispatching

Opsdroid supports different types of events, which can both be sent and received via connectors, for more information on the different types of events see the events documentation.

Connectors can now implement support for sending different types of events using the opsdroid.connector.register_event decorator. This decorator is used to define a method (coroutine) on the connector class for each different event type the connector supports, for instance to add support for Message events you may define a method like this:

@register_event(Message)
async def send_message(self, message):
await myservice.send(message.text)

This method will be called when Connector.send is called with a Message object as its first argument. Methods such as this can be defined for all event types supported by the connector.

import time
# We recommend you use the official library
# for your chat service and import it here
import chatlibrary
# Import opsdroid dependencies
from opsdroid.connector import Connector, register_event
from opsdroid.events import Message

class MyConnector(Connector):
  def __init__(self, config):
# Init the config for the connector
self.name = "MyConnector" # The name of your connector
self.config = config # The config dictionary to be accessed later
self.default_target = "MyDefaultRoom" # The default room for messages to go
  async def connect(self, opsdroid):
# Create connection object with chat library
self.connection = await chatlibrary.connect()
  async def listen(self, opsdroid):
# Listen for new messages from the chat service
while True:
# Get raw message from chat
raw_message = await self.connection.get_next_message()
      # Convert to opsdroid Message object
#
# Message objects take a pointer to the connector to
# allow the skills to call the respond method
message = Message(raw_message.text, raw_message.user,
raw_message.room, self)
      # Parse the message with opsdroid
await opsdroid.parse(message)
  @register_event(Message)
async def send_message(self, message):
# Send message.text back to the chat service
await self.connection.send(message.text, message.user,
message.target)
  async def disconnect(self, opsdroid):
# Disconnect from the chat service
await self.connection.disconnect()

This change is the beginning of the shift away from specific types of response methods like message.react('😂") towards event driven responses like message.respond(opsdroid.events.Reaction('😂')) .

While this makes our skills a little more verbose it allows us to easily add new types of responses in the future.

Matrix Images and Files

All core connectors have been updated to use the new dispatchers for the Message and Reaction events and backward compatibility for Message.respond('Some text string') and Message.react('😂') . However the Matrix connector is the first to get some exciting new event handling.

The Matrix connector now supports receiving images and files using the new @match_event matcher as well as responding with images and files too.

from opsdroid.skill import Skill
from opsdroid.matchers import match_event
from opsdroid.events import Image, Message

class ShoutyImagesSkill(Skill):
@match_event(Image)
async def loudimage(self, event):
await event.respond(Message("THAT'S A PRETTY PICTURE"))

This example skill simply yells THAT'S A PRETTY PICTURE whenever someone uploads an image in Matrix. Or we could even respond with another image. This causes opsdroid to download the file form the url and then upload it to Matrix as an image attachment. You can also construct File and Image objects from byte arrays which you may have generated or read from disk.

from opsdroid.skill import Skill
from opsdroid.matchers import match_event
from opsdroid.events import Image, Message

class ShoutyImagesSkill(Skill):
@match_event(Image)
async def loudimage(self, event):
await event.respond(Image(url="https://i.imgur.com/1rN7yzk.jpg"))
Image that would be sent back to Matrix

Parse parser

The new parse format parser adds a new basic parser which should be super simple for beginners, even more than the regex parser.

This parser is build on top of the parse library which provides an inverse to Python’s string format method. You match your skill with a string following the Python format convention (mostly using {} to denote a placeholder value) and parse will attempt to extract keywords from a string.

Example

from opsdroid.skill import Skill
from opsdroid.matchers import match_parse

class MySkill(Skill):
@match_parse('my name is {name} and my wife is called {wife} and my son is called {son}')
async def my_name_is(self, message):
name = message.parse_result['name']
wife = message.parse_result['wife']
son = message.parse_result['son']

In this example you can see the format string 'my name is {name} and my wife is called {wife} and my son is called {son}' which you would usually use as a template in a format expression where you want to populate those values. However in this case the parse library is using this template to extract the values from the user input and provides them back in the message.parse_result .

This is much easier to read compared to the regex equivalent.

# Regex
My name is (?<name>w+).
# Parse format
My name is {name}.

Full Release Notes

Enhancements

Update telegram connector to accept first_name and username (#841)
Event dispatch system (#826)
Add matching_condition kwarg to regex parser (#877)
Add Event Matcher and Image/File events in the Matrix Connector (#851)
Add parse_format matcher (#922)

Bug fixes

Handle slack bad token issue (#844)
Handle edited messages in Telegram (#866)
Fixing websocket Message parsing error #867 (#868)
Pin python version to 3.6 due to bug in mkdocs (#870)
Fixing regex parser error and core message handling problem #872 (#873)
Clean exit for Matrix connector (#883)
Fixing rasanlu intents matchers issue : changing intents.md file format to intents.yml (#881)
Add slack connect-timeout option (#885)
Suppress Matrix timeout errors in console (fixes #835) (#887)
Turn config name to lowercase to check if is builtin module (#898)
Fix _constrain_skills method #901 (#902)
Add CORS headers to websocket connector (#911)
Fix(matrix-connector): stop parsing empty messages (#913)
Matrix Connector: Change timeout from 6 hours to 5 minutes. (#915)

Breaking changes ⚠️

The connector property connector.default_room has been renamed to connector.default_target
The signature of events.Message has been changed from Message(user, room, connector, text) to Message(text, user, room, connector) to align with the deprecated message.Message.
Connectors which implement the respond method must now use the new @register_event(Message) decorator

Documentation/code quality

Add pipfile and pipfile.lock to .gitignore (#846)
Fix documentation on basic skill tutorial — configuration should be a list(#879)
Include missing await statements in weather example (#882)
Made the README look nicer (#906)
Add backport docs (#910)
Spelling mistakes and grammar corrections (#914)
Google style docstring update to get_logging_level and welcome_message (#917)
 Alphabetise requirements (#927)

v0.15.1

Enhancements

Add Image sending support for Telegram (#929)

Bug fixes

Fix regex and parse_format matchers (#940)
Upgrade setuptools before install on appveyor (#939)

Documentation

Update crontab.md (#937)

v0.15.2

Enhancements

Make web server open by default (#943)
Added timestamps to crontab logs (#930)

Bug fixes

Fix unable to reply in rocketchat (#936)

v0.15.3

Deployments were failing in the Python 3.5 environment so switched to Python 3.6 (#945)

v0.15.4

Revert previous change as that was not the issue (#947)
Add long description content type in setup.py (#946)