Merge branch 'master' of github.com:anoadragon453/nio-template

* 'master' of github.com:anoadragon453/nio-template:
  INSERT OR REPLACE SQL on static value
  Commit to the database after we write to it
  Add projects list
  Add config.yaml to .gitignore
  Send m.notice by default
  Add message_responses.py, pass config parameters to callbacks
This commit is contained in:
Andrew Morgan 2019-10-25 23:42:04 +01:00
commit 6f47000988
8 changed files with 97 additions and 19 deletions

3
.gitignore vendored
View file

@ -9,6 +9,9 @@ env3/
# Bot local files # Bot local files
*.db *.db
# Config file
config.yaml
# Python # Python
__pycache__/ __pycache__/

View file

@ -5,6 +5,13 @@ A template for creating bots with
matrix-nio can be found matrix-nio can be found
[here](https://matrix-nio.readthedocs.io/en/latest/nio.html). [here](https://matrix-nio.readthedocs.io/en/latest/nio.html).
## Projects using nio-template
* [anoadragon453/msc-chatbot](https://github.com/anoadragon453/msc-chatbot) - A matrix bot for matrix spec proposals
Want your project listed here? [Edit this
doc!](https://github.com/anoadragon453/nio-template/edit/master/README.md)
## Project structure ## Project structure
### `main.py` ### `main.py`
@ -84,6 +91,18 @@ prefix (defined by the bot's config file), or through a private message
directly to the bot. The `process` command is then called for the bot to act on directly to the bot. The `process` command is then called for the bot to act on
that command. that command.
### `message_responses.py`
Where responses to messages that are posted in a room (but not necessarily
directed at the bot) are specified. `callbacks.py` will listen for messages in
rooms the bot is in, and upon receiving one will create a new `Message` object
(which contains the message text, amongst other things) and calls `process()`
on it, which can send a message to the room as it sees fit.
A good example of this would be a Github bot that listens for people mentioning
issue numbers in chat (e.g. "We should fix #123"), and the bot sending messages
to the room immediately afterwards with the issue name and link.
### `chat_functions.py` ### `chat_functions.py`
A separate file to hold helper methods related to messaging. Mostly just for A separate file to hold helper methods related to messaging. Mostly just for

View file

@ -2,7 +2,7 @@ from chat_functions import send_text_to_room
class Command(object): class Command(object):
def __init__(self, client, store, command, room, event): def __init__(self, client, store, config, command, room, event):
"""A command made by a user """A command made by a user
Args: Args:
@ -10,6 +10,8 @@ class Command(object):
store (Storage): Bot storage store (Storage): Bot storage
config (Config): Bot configuration parameters
command (str): The command and arguments command (str): The command and arguments
room (nio.rooms.MatrixRoom): The room the command was sent in room (nio.rooms.MatrixRoom): The room the command was sent in

View file

@ -5,6 +5,7 @@ from bot_commands import Command
from nio import ( from nio import (
JoinError, JoinError,
) )
from message_responses import Message
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -12,18 +13,19 @@ logger = logging.getLogger(__name__)
class Callbacks(object): class Callbacks(object):
def __init__(self, client, store, command_prefix): def __init__(self, client, store, config):
""" """
Args: Args:
client (nio.AsyncClient): nio client used to interact with matrix client (nio.AsyncClient): nio client used to interact with matrix
store (Storage): Bot storage store (Storage): Bot storage
command_prefix (str): The prefix for bot commands config (Config): Bot configuration parameters
""" """
self.client = client self.client = client
self.store = store self.store = store
self.command_prefix = command_prefix self.config = config
self.command_prefix = config.command_prefix
async def message(self, room, event): async def message(self, room, event):
"""Callback for when a message event is received """Callback for when a message event is received
@ -46,16 +48,21 @@ class Callbacks(object):
f"{room.user_name(event.sender)}: {msg}" f"{room.user_name(event.sender)}: {msg}"
) )
# Ignore message if in a public room without command prefix # Process as message if in a public room without command prefix
has_command_prefix = msg.startswith(self.command_prefix) has_command_prefix = msg.startswith(self.command_prefix)
if not has_command_prefix and not room.is_group: if not has_command_prefix and not room.is_group:
# General message listener
message = Message(self.client, self.store, self.config, msg, room, event)
await message.process()
return return
# Otherwise if this is in a 1-1 with the bot or features a command prefix,
# treat it as a command
if has_command_prefix: if has_command_prefix:
# Remove the command prefix # Remove the command prefix
msg = msg[len(self.command_prefix):] msg = msg[len(self.command_prefix):]
command = Command(self.client, self.store, msg, room, event) command = Command(self.client, self.store, self.config, msg, room, event)
await command.process() await command.process()
async def invite(self, room, event): async def invite(self, room, event):

View file

@ -7,7 +7,13 @@ from markdown import markdown
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def send_text_to_room(client, room_id, message, markdown_convert=True): async def send_text_to_room(
client,
room_id,
message,
notice=True,
markdown_convert=True
):
"""Send text to a matrix room """Send text to a matrix room
Args: Args:
@ -17,23 +23,30 @@ async def send_text_to_room(client, room_id, message, markdown_convert=True):
message (str): The message content message (str): The message content
notice (bool): Whether the message should be sent with an "m.notice" message type
(will not ping users)
markdown_convert (bool): Whether to convert the message content to markdown. markdown_convert (bool): Whether to convert the message content to markdown.
Defaults to true. Defaults to true.
""" """
formatted = message # Determine whether to ping room members or not
msgtype = "m.notice" if notice else "m.text"
content = {
"msgtype": msgtype,
"format": "org.matrix.custom.html",
"body": message,
}
if markdown_convert: if markdown_convert:
formatted = markdown(message) content["formatted_body"] = markdown(message)
try: try:
await client.room_send( await client.room_send(
room_id, room_id,
"m.room.message", "m.room.message",
{ content,
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"body": message,
"formatted_body": formatted,
}
) )
except SendRetryError: except SendRetryError:
logger.exception(f"Unable to send message response to {room_id}") logger.exception(f"Unable to send message response to {room_id}")

View file

@ -42,7 +42,7 @@ async def main():
client.access_token = config.access_token client.access_token = config.access_token
# Set up event callbacks # Set up event callbacks
callbacks = Callbacks(client, store, config.command_prefix) callbacks = Callbacks(client, store, config)
client.add_event_callback(callbacks.message, (RoomMessageText,)) client.add_event_callback(callbacks.message, (RoomMessageText,))
client.add_event_callback(callbacks.invite, (InviteEvent,)) client.add_event_callback(callbacks.invite, (InviteEvent,))

33
message_responses.py Normal file
View file

@ -0,0 +1,33 @@
import logging
logger = logging.getLogger(__name__)
class Message(object):
def __init__(self, client, store, config, message_content, room, event):
"""Initialize a new Message
Args:
client (nio.AsyncClient): nio client used to interact with matrix
store (Storage): Bot storage
config (Config): Bot configuration parameters
message_content (str): The body of the message
room (nio.rooms.MatrixRoom): The room the event came from
event (nio.events.room_events.RoomMessageText): The event defining the message
"""
self.client = client
self.store = store
self.config = config
self.message_content = message_content
self.room = room
self.event = event
async def process(self):
"""Process and possibly respond to the message"""
pass

View file

@ -30,12 +30,13 @@ class Storage(object):
logger.info("Performing initial database setup...") logger.info("Performing initial database setup...")
# Initialize a connection to the database # Initialize a connection to the database
conn = sqlite3.connect(self.db_path) self.conn = sqlite3.connect(self.db_path)
self.cursor = conn.cursor() self.cursor = self.conn.cursor()
# Sync token table # Sync token table
self.cursor.execute("CREATE TABLE sync_token (" self.cursor.execute("CREATE TABLE sync_token ("
"token TEXT PRIMARY KEY" "dedupe_id INTEGER PRIMARY KEY, "
"token TEXT NOT NULL"
")") ")")
logger.info("Database setup complete") logger.info("Database setup complete")