Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

feat: ✨ trigger the bot by mentioning it #81

Draft
wants to merge 17 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion doc/manual/match.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ async def example(room, message):
#Respond to help command
```

It is also possible to match by mention of the bot's username, matrix ID, etc.
In the next example, we can use the prefix or mention the bot to show its help message.

```python
bot.listener.on_message_event
async def help(room, message):
match = botlib.MessageMatch(room, message, bot, "!")
if match.command("help") and (match.prefix() or match.mention()):
#Respond to help command
```

A list of methods for the Match class is shown below. [Methods from the Match class](#match-methods) can also be used with the MessageMatch class.

#### List of Methods:
Expand All @@ -78,5 +89,6 @@ A list of methods for the Match class is shown below. [Methods from the Match cl
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MessageMatch.command()` or `MessageMatch.command(command)` | The "command" is the beginning of messages that are intended to be commands, but after the prefix; e.g. "help". Returns the command if the command argument is empty. Returns True if the command argument is equivalent to the command. |
| `MessageMatch.prefix()` | Returns True if the message begins with the prefix specified during the initialization of the instance of the MessageMatch class. Returns True if no prefix was specified during the initialization. |
| `MessageMatch.mention()` | Returns True if the message begins with the bot's display name, disambiguated display name, matrix ID, or pill (HTML link to the bot via matrix.to) if formatted_body is present. |
| `MessageMatch.args()` | Returns a list of strings; each string is part of the message separated by a space, with the exception of the part of the message before the first space (the prefix and command). Returns an empty list if it is a single-word command. |
| `MessageMatch.contains(string)` | Returns True if the message contains the value specified in the string argument. |
| `MessageMatch.contains(string)` | Returns True if the message contains the value specified in the string argument. |
2 changes: 1 addition & 1 deletion simplematrixbotlib/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def main(self):


resp = await self.async_client.sync(timeout=65536,
full_state=False) #Ignore prior messages
full_state=True) #Ignore prior messages
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to be required to reliably load self.room.own_user_id and self.room.users which may be empty otherwise from testing. I hope there is a better way to do it than just syncing everything.

Copy link

@ghost ghost Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does self.room.own_user_id follow the structure of @username:homeserver ? If so, it would not be neccesary to do anything with self.room.users to obtain it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.room is a Dict[str, MatrixUser]. MatrixUser contains display_name and disambiguated_name which we need for mention() matches

Copy link
Contributor Author

@HarHarLinks HarHarLinks Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this https://matrix-nio.readthedocs.io/en/latest/nio.html#nio.rooms.MatrixRoom.user_name is good enough instead? I don't think so as it does the same: if room members haven't been synced yet, it just fails.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we need the user_id, then whoami should solve that.

Copy link

@ghost ghost Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 57-63 of api.py

async with aiohttp.ClientSession() as session:
            async with session.get(f'{self.creds.homeserver}/_matrix/client/r0/account/whoami?access_token={self.creds.access_token}') as response:
                device_id = ast.literal_eval((await response.text()).replace(":false,", ":\"false\","))['device_id']
                user_id = ast.literal_eval((await response.text()).replace(":false,", ":\"false\","))['user_id']
            
            self.async_client.device_id, self.creds.device_id = device_id, device_id
            self.async_client.user_id, self.creds.user_id = user_id, user_id

Copy link

@ghost ghost Nov 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the user id not be stored in bot.async_client.user_id ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would. the issue is more about getting the displayname though

Copy link
Contributor Author

@HarHarLinks HarHarLinks Nov 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think doing a full sync is actually ok if we enable storage in this PR (store is needed for #79)

https://github.com/poljar/matrix-nio/blob/a4fb83fd515568e269646d2111dc68e17cc251c6/nio/client/async_client.py#L368-L380

then only the very first time would be a "big" sync

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is acceptable.


if isinstance(resp, SyncResponse):
print(f"Connected to {self.async_client.homeserver} as {self.async_client.user_id} ({self.async_client.device_id})")
Expand Down
68 changes: 54 additions & 14 deletions simplematrixbotlib/match.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Union, Optional

class Match:
"""
Class with methods to filter events
Expand Down Expand Up @@ -83,7 +85,27 @@ def __init__(self, room, event, bot, prefix="") -> None:
super().__init__(room, event, bot)
self._prefix = prefix

def command(self, command=None):
"""Forms of identification"""
self._own_user_id = room.own_user_id
self._own_nio_user = self.room.users[self._own_user_id]
self._own_disambiguated_name = self._own_nio_user.disambiguated_name
self._own_display_name = self._own_nio_user.display_name
self._own_display_name_colon = f"{self._own_display_name}:"
This conversation was marked as resolved.
Show resolved Hide resolved
self._own_pill = f"<a href=\"https://matrix.to/#/{self._own_user_id}\">"

self.mention() # Set self._mention_id_length
self._body_without_prefix = self.event.body[len(self._prefix):]
self._body_without_mention = self.event.body[self._mention_id_length:]
Copy link
Contributor Author

@HarHarLinks HarHarLinks Nov 30, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why initialize these here while it's unknown whether prefix or mention is used?
why create member variables that are only ever used 2 lines later?
mention() is executed twice unnecessarily

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention() sets ._mention_id_length, which is neccesary for calculating ._body_without_mention

Copy link

@ghost ghost Dec 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be a good idea to look at bot libraries for other networks to see how they handle matching.


if self.mention():
body = self._body_without_mention
elif self.prefix():
body = self._body_without_prefix
else:
body = self.event.body
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use distinct member variables? they can't both apply at once

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you reword this?

self._split_body = body.split()

def command(self, command: Optional[str] = None) -> Union[bool, str]:
"""
Parameters
----------
Expand All @@ -99,18 +121,19 @@ def command(self, command=None):
Returns the string after the prefix and before the first space if no arg is passed to this method.
"""

if self._prefix == self.event.body[0:len(self._prefix)]:
body_without_prefix = self.event.body[len(self._prefix):]
else:
body_without_prefix = self.event.body

if not body_without_prefix:
return []
if not (self._body_without_prefix and self._body_without_mention):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use len(self._split_body) instead

"""Body is empty after removing prefix or mention"""
if command is None:
return ""
elif command:
return False
else:
return True
This conversation was marked as resolved.
Show resolved Hide resolved

This conversation was marked as resolved.
Show resolved Hide resolved
if command:
return body_without_prefix.split()[0] == command
if command is not None:
return self._split_body[0] == command
else:
return body_without_prefix.split()[0]
return self._split_body[0]

def prefix(self):
"""
Expand All @@ -121,18 +144,35 @@ def prefix(self):
Returns True if the message begins with the prefix, and False otherwise. If there is no prefix specified during the creation of this MessageMatch object, then return True.
"""

return self.event.body.startswith(self._prefix)
return self.event.body[self._mention_id_length:].startswith(self._prefix)

def mention(self):
"""

Returns
-------
boolean
Returns True if the message begins with the bot's username, MXID, or pill targeting the MXID, and False otherwise.
"""

for id in [self._own_disambiguated_name, self._own_display_name, self._own_user_id, self._own_display_name_colon]:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will always match _own_display_name and never _own_display_name_colon

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps _own_display_name_colon could be removed.

if self.event.body.startswith(id):
self._mention_id_length = len(id)+1
return True
self._mention_id_length = 0

return False

def args(self):
"""

Returns
-------
list
Returns a list of strings that are the "words" of the message, except for the first "word", which would be the command.
Returns a list of strings that are the "words" of the message, except for the first "word", which would be the prefix/mention + command.
"""

return self.event.body.split()[1:]
return self._split_body[1:]

def contains(self, string):
"""
Expand Down
83 changes: 78 additions & 5 deletions tests/match/test_messagematch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,68 @@
from unittest import mock

mock_room = mock.MagicMock()
mock_room.own_user_id = "@bot:matrix.org"
mock_user = mock.MagicMock()
mock_user.display_name = "bot"
mock_user.disambiguated_name = f"{mock_user.display_name} ({mock_room.own_user_id})"
mock_room.users = {mock_room.own_user_id: mock_user}

mock_room2 = mock.MagicMock()
mock_room2.own_user_id = "@bot:matrix.org"
mock_user2 = mock.MagicMock()
mock_user2.display_name = "bot"
mock_user2.disambiguated_name = f"{mock_user2.display_name} ({mock_room2.own_user_id})"
mock_room2.users = {mock_room2.own_user_id: mock_user2}

mock_event = mock.MagicMock()
mock_event.body = "p!help example"

mock_event2 = mock.MagicMock()
mock_event2.body = "p!help"

mock_event3 = mock.MagicMock()
mock_event3.body = "bot help"
mock_event3.formatted_body = None
mock_event4 = mock.MagicMock()
mock_event4.body = "bot: help"
mock_event4.formatted_body = None
mock_event5 = mock.MagicMock()
mock_event5.body = f"{mock_room2.own_user_id} help"
mock_event5.formatted_body = None
mock_event6 = mock.MagicMock()
mock_event6.body = f"bot ({mock_room2.own_user_id}) help"
mock_event6.formatted_body = None
mock_event7 = mock.MagicMock()
mock_event7.body = "something else"
mock_event7.formatted_body = "<a href=\"https://matrix.to/#/@bot:matrix.org\">bot</a> help"
mock_event8 = mock.MagicMock()
mock_event8.body = "bottom help"
mock_event8.formatted_body = None

mock_event9 = mock.MagicMock()
mock_event9.body = "p!"

mock_bot = mock.MagicMock()
mock_bot.creds.username = "bot"
mock_bot.creds.homeserver = "https://matrix.org"

prefix = "p!"
prefix2 = "!!"

match = MessageMatch(mock_room, mock_event, mock_bot, prefix)
match2 = MessageMatch(mock_room, mock_event, mock_bot)
match3 = MessageMatch(mock_room, mock_event, mock_bot, prefix2)
match4 = MessageMatch(mock_room, mock_event2, mock_bot, prefix)
match = MessageMatch(mock_room, mock_event, mock_bot, prefix) # prefix match
match2 = MessageMatch(mock_room, mock_event, mock_bot) # no prefix given
match3 = MessageMatch(mock_room, mock_event, mock_bot, prefix2) # wrong prefix given
match4 = MessageMatch(mock_room, mock_event2, mock_bot, prefix) # no arguments given

match5 = MessageMatch(mock_room2, mock_event3, mock_bot, prefix) # mention with display name
match6 = MessageMatch(mock_room2, mock_event4, mock_bot, prefix) # mention with colon
match7 = MessageMatch(mock_room2, mock_event5, mock_bot, prefix) # mention with user id
match8 = MessageMatch(mock_room2, mock_event6, mock_bot, prefix) # mention with disambiguated name
match9 = MessageMatch(mock_room2, mock_event7, mock_bot, prefix) # mention with pill
match10 = MessageMatch(mock_room2, mock_event8, mock_bot, prefix) # mention someone else
match11 = MessageMatch(mock_room2, mock_event3, mock_bot) # mention without prefix

match12 = MessageMatch(mock_room, mock_event9, mock_bot, prefix) # prefix match with empty command

def test_init():
assert issubclass(MessageMatch, Match)
Expand All @@ -30,11 +76,38 @@ def test_command():
assert match2.command() == "p!help"
assert match2.command("p!help") == True

assert match12.command() == ""
assert match12.command("") == True

def test_mention():
assert match5.command() == "help"
assert match5.mention() == True

assert match6.command() == "help"
assert match6.mention() == True

assert match7.command() == "help"
assert match7.mention() == True

assert match8.command() == "help"
assert match8.mention() == True

assert match9.command() == "help"
assert match9.mention() == True

assert match10.command() == "bottom"
assert match10.mention() == False

assert match11.command() == "help"
assert match11.mention() == True

def test_prefix():
assert match.prefix() == True
assert match3.prefix() == False

#assert match2.prefix() == True
assert match2.prefix() == True

assert match12.prefix() == True

def test_args():
assert match.args() == ["example"]
Expand Down