Add example commands for reactions and event reply
There is now a 'react' command that the bot will react to when used. When a reaction is made on a message that the bot sent, then it will acknowledge that reaction using a reply.
This commit is contained in:
parent
f2d1967aab
commit
647233cfac
4 changed files with 170 additions and 13 deletions
|
@ -1,4 +1,4 @@
|
||||||
from my_project_name.chat_functions import send_text_to_room
|
from my_project_name.chat_functions import react_to_event, send_text_to_room
|
||||||
|
|
||||||
|
|
||||||
class Command(object):
|
class Command(object):
|
||||||
|
@ -30,6 +30,8 @@ class Command(object):
|
||||||
"""Process the command"""
|
"""Process the command"""
|
||||||
if self.command.startswith("echo"):
|
if self.command.startswith("echo"):
|
||||||
await self._echo()
|
await self._echo()
|
||||||
|
elif self.command.startswith("react"):
|
||||||
|
await self._react()
|
||||||
elif self.command.startswith("help"):
|
elif self.command.startswith("help"):
|
||||||
await self._show_help()
|
await self._show_help()
|
||||||
else:
|
else:
|
||||||
|
@ -40,6 +42,20 @@ class Command(object):
|
||||||
response = " ".join(self.args)
|
response = " ".join(self.args)
|
||||||
await send_text_to_room(self.client, self.room.room_id, response)
|
await send_text_to_room(self.client, self.room.room_id, response)
|
||||||
|
|
||||||
|
async def _react(self):
|
||||||
|
"""Make the bot react to the command message"""
|
||||||
|
# React with a start emoji
|
||||||
|
reaction = "⭐"
|
||||||
|
await react_to_event(
|
||||||
|
self.client, self.room.room_id, self.event.event_id, reaction
|
||||||
|
)
|
||||||
|
|
||||||
|
# React with some generic text
|
||||||
|
reaction = "Some text"
|
||||||
|
await react_to_event(
|
||||||
|
self.client, self.room.room_id, self.event.event_id, reaction
|
||||||
|
)
|
||||||
|
|
||||||
async def _show_help(self):
|
async def _show_help(self):
|
||||||
"""Show the help text"""
|
"""Show the help text"""
|
||||||
if not self.args:
|
if not self.args:
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from nio import JoinError
|
from nio import JoinError, MatrixRoom, RoomGetEventError, UnknownEvent
|
||||||
|
|
||||||
from my_project_name.bot_commands import Command
|
from my_project_name.bot_commands import Command
|
||||||
|
from my_project_name.chat_functions import make_pill, send_text_to_room
|
||||||
from my_project_name.message_responses import Message
|
from my_project_name.message_responses import Message
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -86,3 +87,66 @@ class Callbacks(object):
|
||||||
|
|
||||||
# Successfully joined room
|
# Successfully joined room
|
||||||
logger.info(f"Joined {room.room_id}")
|
logger.info(f"Joined {room.room_id}")
|
||||||
|
|
||||||
|
async def _reaction(
|
||||||
|
self, room: MatrixRoom, event: UnknownEvent, reacted_to_id: str
|
||||||
|
):
|
||||||
|
"""A reaction was sent to one of our messages. Let's send a reply acknowledging it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room: The room the reaction was sent in.
|
||||||
|
|
||||||
|
event: The reaction event.
|
||||||
|
|
||||||
|
reacted_to_id: The event ID that the reaction points to.
|
||||||
|
"""
|
||||||
|
logger.debug(f"Got reaction to {room.room_id} from {event.sender}.")
|
||||||
|
|
||||||
|
# Get the original event that was reacted to
|
||||||
|
event_response = await self.client.room_get_event(room.room_id, reacted_to_id)
|
||||||
|
if isinstance(event_response, RoomGetEventError):
|
||||||
|
logger.warning(
|
||||||
|
"Error getting event that was reacted to (%s)", reacted_to_id
|
||||||
|
)
|
||||||
|
return
|
||||||
|
reacted_to_event = event_response.event
|
||||||
|
|
||||||
|
# Only acknowledge reactions to events that we sent
|
||||||
|
if reacted_to_event.sender != self.config.user_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Send a message acknowledging the reaction
|
||||||
|
reaction_sender_pill = make_pill(event.sender)
|
||||||
|
reaction_content = (
|
||||||
|
event.source.get("content", {}).get("m.relates_to", {}).get("key")
|
||||||
|
)
|
||||||
|
message = (
|
||||||
|
f"{reaction_sender_pill} reacted to this event with `{reaction_content}`!"
|
||||||
|
)
|
||||||
|
await send_text_to_room(
|
||||||
|
self.client,
|
||||||
|
room.room_id,
|
||||||
|
message,
|
||||||
|
reply_to_event_id=reacted_to_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def unknown(self, room: MatrixRoom, event: UnknownEvent):
|
||||||
|
"""Callback for when an event with a type that is unknown to matrix-nio is received.
|
||||||
|
Currently this is used for reaction events, which are not specced.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room: The room the reaction was sent in.
|
||||||
|
|
||||||
|
event: The reaction event.
|
||||||
|
"""
|
||||||
|
if event.type == "m.reaction":
|
||||||
|
# Get the ID of the event this was a reaction to
|
||||||
|
relation_dict = event.source.get("content", {}).get("m.relates_to", {})
|
||||||
|
|
||||||
|
reacted_to = relation_dict.get("event_id")
|
||||||
|
if reacted_to and relation_dict.get("rel_type") == "m.annotation":
|
||||||
|
await self._reaction(room, event, reacted_to)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Got unknown event with type to {event.type} from {event.sender} in {room.room_id}."
|
||||||
|
)
|
||||||
|
|
|
@ -1,28 +1,37 @@
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
from nio import SendRetryError
|
from nio import AsyncClient, ErrorResponse, Response, SendRetryError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def send_text_to_room(
|
async def send_text_to_room(
|
||||||
client, room_id, message, notice=True, markdown_convert=True
|
client: AsyncClient,
|
||||||
|
room_id: str,
|
||||||
|
message: str,
|
||||||
|
notice: bool = True,
|
||||||
|
markdown_convert: bool = True,
|
||||||
|
reply_to_event_id: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Send text to a matrix room
|
"""Send text to a matrix room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client (nio.AsyncClient): The client to communicate to matrix with
|
client: The client to communicate to matrix with.
|
||||||
|
|
||||||
room_id (str): The ID of the room to send the message to
|
room_id: The ID of the room to send the message to.
|
||||||
|
|
||||||
message (str): The message content
|
message: The message content.
|
||||||
|
|
||||||
notice (bool): Whether the message should be sent with an "m.notice" message type
|
notice: Whether the message should be sent with an "m.notice" message type
|
||||||
(will not ping users)
|
(will not ping users).
|
||||||
|
|
||||||
markdown_convert (bool): Whether to convert the message content to markdown.
|
markdown_convert: Whether to convert the message content to markdown.
|
||||||
Defaults to true.
|
Defaults to true.
|
||||||
|
|
||||||
|
reply_to_event_id: Whether this message is a reply to another event. The event
|
||||||
|
ID this is message is a reply to.
|
||||||
"""
|
"""
|
||||||
# Determine whether to ping room members or not
|
# Determine whether to ping room members or not
|
||||||
msgtype = "m.notice" if notice else "m.text"
|
msgtype = "m.notice" if notice else "m.text"
|
||||||
|
@ -36,9 +45,74 @@ async def send_text_to_room(
|
||||||
if markdown_convert:
|
if markdown_convert:
|
||||||
content["formatted_body"] = markdown(message)
|
content["formatted_body"] = markdown(message)
|
||||||
|
|
||||||
|
if reply_to_event_id:
|
||||||
|
content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_event_id}}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await client.room_send(
|
await client.room_send(
|
||||||
room_id, "m.room.message", content, ignore_unverified_devices=True,
|
room_id,
|
||||||
|
"m.room.message",
|
||||||
|
content,
|
||||||
|
ignore_unverified_devices=True,
|
||||||
)
|
)
|
||||||
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}")
|
||||||
|
|
||||||
|
|
||||||
|
def make_pill(user_id: str, displayname: str = None) -> str:
|
||||||
|
"""Convert a user ID (and optionally a display name) to a formatted user 'pill'
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The MXID of the user.
|
||||||
|
|
||||||
|
displayname: An optional displayname. Clients like Element will figure out the
|
||||||
|
correct display name no matter what, but other clients may not. If not
|
||||||
|
provided, the MXID will be used instead.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The formatted user pill.
|
||||||
|
"""
|
||||||
|
if not displayname:
|
||||||
|
# Use the user ID as the displayname if not provided
|
||||||
|
displayname = user_id
|
||||||
|
|
||||||
|
return f'<a href="https://matrix.to/#/{user_id}">{displayname}</a>'
|
||||||
|
|
||||||
|
|
||||||
|
async def react_to_event(
|
||||||
|
client: AsyncClient,
|
||||||
|
room_id: str,
|
||||||
|
event_id: str,
|
||||||
|
reaction_text: str,
|
||||||
|
) -> Union[Response, ErrorResponse]:
|
||||||
|
"""Reacts to a given event in a room with the given reaction text
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: The client to communicate to matrix with.
|
||||||
|
|
||||||
|
room_id: The ID of the room to send the message to.
|
||||||
|
|
||||||
|
event_id: The ID of the event to react to.
|
||||||
|
|
||||||
|
reaction_text: The string to react with. Can also be (one or more) emoji characters.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A nio.Response or nio.ErrorResponse if an error occurred.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SendRetryError: If the reaction was unable to be sent.
|
||||||
|
"""
|
||||||
|
content = {
|
||||||
|
"m.relates_to": {
|
||||||
|
"rel_type": "m.annotation",
|
||||||
|
"event_id": event_id,
|
||||||
|
"key": reaction_text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await client.room_send(
|
||||||
|
room_id,
|
||||||
|
"m.reaction",
|
||||||
|
content,
|
||||||
|
ignore_unverified_devices=True,
|
||||||
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ from nio import (
|
||||||
LocalProtocolError,
|
LocalProtocolError,
|
||||||
LoginError,
|
LoginError,
|
||||||
RoomMessageText,
|
RoomMessageText,
|
||||||
|
UnknownEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from my_project_name.callbacks import Callbacks
|
from my_project_name.callbacks import Callbacks
|
||||||
|
@ -59,6 +60,7 @@ async def main():
|
||||||
callbacks = Callbacks(client, store, config)
|
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, (InviteMemberEvent,))
|
client.add_event_callback(callbacks.invite, (InviteMemberEvent,))
|
||||||
|
client.add_event_callback(callbacks.unknown, (UnknownEvent,))
|
||||||
|
|
||||||
# Keep trying to reconnect on failure (with some time in-between)
|
# Keep trying to reconnect on failure (with some time in-between)
|
||||||
while True:
|
while True:
|
||||||
|
@ -74,7 +76,8 @@ async def main():
|
||||||
# Try to login with the configured username/password
|
# Try to login with the configured username/password
|
||||||
try:
|
try:
|
||||||
login_response = await client.login(
|
login_response = await client.login(
|
||||||
password=config.user_password, device_name=config.device_name,
|
password=config.user_password,
|
||||||
|
device_name=config.device_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if login failed
|
# Check if login failed
|
||||||
|
|
Loading…
Reference in a new issue