Skip to content
This repository was archived by the owner on May 22, 2024. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from 9 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
58 changes: 48 additions & 10 deletions simplematrixbotlib/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ def __init__(self, room, event, bot, prefix="") -> None:
"""
super().__init__(room, event, bot)
self._prefix = prefix
bot_user = self.room.users[self.room.own_user_id]
# in case another user uses the same display name, we need to regard the disambiguation if present
self._disambiguated_name = bot_user.disambiguated_name
# still we allow to mention without disambiguation if typing manually for now
self._display_name = bot_user.display_name
# element generates a "pill" from formatted (HTML) links to matrix.to/#/@matrix:id
# this isn't really specced but still the status quo
self._pill = f'<a href="https://matrix.to/#/{self.room.own_user_id}">'
self._body_without_prefix = None

def command(self, command=None):
"""
Expand All @@ -99,18 +108,17 @@ 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 []
# we cache this part
if self._body_without_prefix is None:
if self._prefix == self.event.body[0:len(self._prefix)]:
self._body_without_prefix = self.event.body[len(self._prefix):]
elif not self.mention(): # if mention() is True then it also sets the _body_without_prefix
self._body_without_prefix = self.event.body

if command:
return body_without_prefix.split()[0] == command
return self._body_without_prefix.split()[0] == command
else:
return body_without_prefix.split()[0]
return self._body_without_prefix.split()[0]

def prefix(self):
"""
Expand All @@ -123,6 +131,36 @@ def prefix(self):

return self.event.body.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.
"""

body = self.event.body
for id in [self._disambiguated_name, self._display_name, self.room.own_user_id]:
if body.startswith(id):
body_ = body[len(id):]
# the match needs to end here, otherwise someone else is mentioned
# this isn't perfect but probably the best effort
if body_[0] in [' ', ':']:
self._body_without_prefix = body_[1:].strip()
return True

# pills on the other hand are a clearer case thanks to HTML tags which include delimiters
body = self.event.formatted_body
if body is not None and body.startswith(self._pill):
# remove the first half of the pill
body = body[len(self._pill):]
# find pill end + trailing delimiter + maybe whitespace
self._body_without_prefix = body[body.index('</a>')+5:].strip()
return True

return False

def args(self):
"""

Expand All @@ -132,7 +170,7 @@ def args(self):
Returns a list of strings that are the "words" of the message, except for the first "word", which would be the command.
"""

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

def contains(self, string):
"""
Expand Down
60 changes: 56 additions & 4 deletions tests/match/test_messagematch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,54 @@

mock_room = mock.MagicMock()

mock_room2 = mock.MagicMock()
mock_room2.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_room2.own_user_id})"
mock_room2.users = {mock_room2.own_user_id: mock_user}

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_bot = mock.MagicMock()

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

def test_init():
assert issubclass(MessageMatch, Match)
Expand All @@ -30,6 +63,25 @@ def test_command():
assert match2.command() == "p!help"
assert match2.command("p!help") == 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

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