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):
|
||||
|
@ -30,6 +30,8 @@ class Command(object):
|
|||
"""Process the command"""
|
||||
if self.command.startswith("echo"):
|
||||
await self._echo()
|
||||
elif self.command.startswith("react"):
|
||||
await self._react()
|
||||
elif self.command.startswith("help"):
|
||||
await self._show_help()
|
||||
else:
|
||||
|
@ -40,6 +42,20 @@ class Command(object):
|
|||
response = " ".join(self.args)
|
||||
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):
|
||||
"""Show the help text"""
|
||||
if not self.args:
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
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.chat_functions import make_pill, send_text_to_room
|
||||
from my_project_name.message_responses import Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -86,3 +87,66 @@ class Callbacks(object):
|
|||
|
||||
# Successfully joined room
|
||||
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
|
||||
from typing import Optional, Union
|
||||
|
||||
from markdown import markdown
|
||||
from nio import SendRetryError
|
||||
from nio import AsyncClient, ErrorResponse, Response, SendRetryError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
(will not ping users)
|
||||
notice: 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: Whether to convert the message content to markdown.
|
||||
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
|
||||
msgtype = "m.notice" if notice else "m.text"
|
||||
|
@ -36,9 +45,74 @@ async def send_text_to_room(
|
|||
if markdown_convert:
|
||||
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:
|
||||
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:
|
||||
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,
|
||||
LoginError,
|
||||
RoomMessageText,
|
||||
UnknownEvent,
|
||||
)
|
||||
|
||||
from my_project_name.callbacks import Callbacks
|
||||
|
@ -59,6 +60,7 @@ async def main():
|
|||
callbacks = Callbacks(client, store, config)
|
||||
client.add_event_callback(callbacks.message, (RoomMessageText,))
|
||||
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)
|
||||
while True:
|
||||
|
@ -74,7 +76,8 @@ async def main():
|
|||
# Try to login with the configured username/password
|
||||
try:
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue