Updated bot implementation
- Added ability to send text messages - Added ability to verify all devices - Added configuration field to enable autoverification of all known devices - Added ability to get client
This commit is contained in:
@@ -14,4 +14,7 @@ class MatrixBotConfig:
|
|||||||
"""Localpart of matrix username (without homeserver), for example: `valid-username`"""
|
"""Localpart of matrix username (without homeserver), for example: `valid-username`"""
|
||||||
|
|
||||||
storage_directory: Path
|
storage_directory: Path
|
||||||
"""Path to the storage directory (will be created if needed)"""
|
"""Path to the storage directory (will be created if needed)"""
|
||||||
|
|
||||||
|
auto_verify_all_known_devices: bool = True
|
||||||
|
"""Enable automatic verification of all known devices on related errors"""
|
||||||
11
main.py
11
main.py
@@ -5,6 +5,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
import signal
|
import signal
|
||||||
|
import html
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import config
|
import config
|
||||||
@@ -14,11 +15,20 @@ from new_bot import MatrixBot
|
|||||||
from bot_types import MatrixBotConfig
|
from bot_types import MatrixBotConfig
|
||||||
|
|
||||||
import nio.events
|
import nio.events
|
||||||
|
from nio import MatrixRoom, RoomMessageText
|
||||||
|
|
||||||
from datatypes import AppConfig
|
from datatypes import AppConfig
|
||||||
|
|
||||||
|
bot: MatrixBot = None
|
||||||
|
|
||||||
|
async def callback_test_text(room: MatrixRoom, event: RoomMessageText) -> None:
|
||||||
|
if event.sender == bot.get_client().user_id:
|
||||||
|
return
|
||||||
|
await bot.send_text_to_room(room.room_id, f"<b>{html.escape(event.body)}</b>")
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
"""Entry point"""
|
"""Entry point"""
|
||||||
|
global bot
|
||||||
# setup signal handler
|
# setup signal handler
|
||||||
util.setup_app_stop_event()
|
util.setup_app_stop_event()
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
@@ -42,6 +52,7 @@ async def main() -> None:
|
|||||||
storage_directory=Path(cfg.store_dir)
|
storage_directory=Path(cfg.store_dir)
|
||||||
)
|
)
|
||||||
bot = MatrixBot(matrix_bot_config)
|
bot = MatrixBot(matrix_bot_config)
|
||||||
|
bot.add_event_callback(callback_test_text, RoomMessageText)
|
||||||
bot.add_event_callback(None, nio.events.Event)
|
bot.add_event_callback(None, nio.events.Event)
|
||||||
|
|
||||||
# start the app
|
# start the app
|
||||||
|
|||||||
108
new_bot.py
108
new_bot.py
@@ -7,12 +7,17 @@ import time
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from html.parser import HTMLParser
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from typing import Any, Callable, Awaitable
|
from typing import Any, Callable, Awaitable
|
||||||
|
|
||||||
from nio import AsyncClient, AsyncClientConfig, SyncResponse
|
from nio import AsyncClient, AsyncClientConfig, SyncResponse
|
||||||
from nio import LoginResponse, LoginError, WhoamiResponse, WhoamiError
|
from nio import LoginResponse, LoginError, WhoamiResponse, WhoamiError
|
||||||
|
|
||||||
|
from nio import RoomSendResponse, RoomSendError
|
||||||
|
|
||||||
|
from nio import OlmUnverifiedDeviceError
|
||||||
|
|
||||||
import nio.events
|
import nio.events
|
||||||
|
|
||||||
from bot_types import *
|
from bot_types import *
|
||||||
@@ -97,6 +102,32 @@ class MatrixBot:
|
|||||||
localpart = config.matrix_username_localpart
|
localpart = config.matrix_username_localpart
|
||||||
return f"@{localpart}:{homeserver_name}"
|
return f"@{localpart}:{homeserver_name}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _check_and_remove_html(text_to_check: str) -> tuple[bool, str]:
|
||||||
|
"""Checks if `text_to_check` is HTML and sanitizes it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[bool, str] where the first element is True if `text_to_check` contains
|
||||||
|
valid HTML, and the second element is text without HTML (or just copy of
|
||||||
|
`text_to_check` if it does not contain HTML)
|
||||||
|
"""
|
||||||
|
has_tags = False
|
||||||
|
text_fragments = []
|
||||||
|
class Extractor(HTMLParser):
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
nonlocal has_tags
|
||||||
|
has_tags = True
|
||||||
|
def handle_data(self, data):
|
||||||
|
text_fragments.append(data)
|
||||||
|
parser = Extractor(convert_charrefs=True)
|
||||||
|
parser.feed(text_to_check)
|
||||||
|
try:
|
||||||
|
if has_tags:
|
||||||
|
return (True, " ".join("".join(text_fragments).split()))
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return (False, text_to_check)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _wait_for_task_and_stop_event(payload_task: asyncio.Task, stop_wait_task: asyncio.Task, print_exc: bool = False) -> Any:
|
async def _wait_for_task_and_stop_event(payload_task: asyncio.Task, stop_wait_task: asyncio.Task, print_exc: bool = False) -> Any:
|
||||||
"""Waits for `payload_task` to finish. Uses `stop_wait_task` to check if payload task
|
"""Waits for `payload_task` to finish. Uses `stop_wait_task` to check if payload task
|
||||||
@@ -376,9 +407,84 @@ class MatrixBot:
|
|||||||
self._stop_event = None
|
self._stop_event = None
|
||||||
self._background_task = None
|
self._background_task = None
|
||||||
|
|
||||||
|
def verify_all_known_devices(self) -> bool:
|
||||||
|
"""Verifies all known devices.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if there were unverified devices that are verified now.
|
||||||
|
"""
|
||||||
|
result = False
|
||||||
|
for user_id in self._client.device_store.users:
|
||||||
|
for device_id, olm_device in self._client.device_store[user_id].items():
|
||||||
|
# can't trust ourselves
|
||||||
|
if device_id == self._client.device_id and user_id == self._client.user_id:
|
||||||
|
continue
|
||||||
|
# they are already verified
|
||||||
|
if olm_device.verified:
|
||||||
|
continue
|
||||||
|
# verify them
|
||||||
|
self._client.verify_device(olm_device)
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
|
|
||||||
def add_event_callback(self, callback: Callable[[Any], Awaitable[None]] | None, event_class: nio.events.Event) -> None:
|
def add_event_callback(self, callback: Callable[[Any], Awaitable[None]] | None, event_class: nio.events.Event) -> None:
|
||||||
"""Added event callback for events of specified class.
|
"""Added event callback for events of specified class.
|
||||||
Use `None` instead of callback to print parameter types you need to use in your callback."""
|
Use `None` instead of callback to print parameter types you need to use in your callback."""
|
||||||
if callback is None:
|
if callback is None:
|
||||||
callback = self._debug_event_callback
|
callback = self._debug_event_callback
|
||||||
self._client.add_event_callback(callback, event_class) # type: ignore
|
self._client.add_event_callback(callback, event_class) # type: ignore
|
||||||
|
|
||||||
|
def get_client(self) -> AsyncClient:
|
||||||
|
"""Get AsyncClient in use"""
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
async def send_text_to_room(self, room_id: str, text: str, is_html: bool | None = None, **kwargs) -> str:
|
||||||
|
"""Sends a text message to the room and handle HTML as specified.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text - text to send
|
||||||
|
is_html - True if text is HTML; False if text is not HTML; None if the value should be guessed
|
||||||
|
kwargs - passed as `m.room.message` content keys
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
event_id of the message on success. Raises an exception on error.
|
||||||
|
"""
|
||||||
|
text_with_html = text
|
||||||
|
# guess if text is HTML
|
||||||
|
if is_html is None:
|
||||||
|
is_html, text = self._check_and_remove_html(text)
|
||||||
|
# text is HTML
|
||||||
|
elif is_html:
|
||||||
|
_, text = self._check_and_remove_html(text)
|
||||||
|
# create `content` for `room_send()`
|
||||||
|
content = {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": text,
|
||||||
|
**kwargs
|
||||||
|
}
|
||||||
|
if is_html:
|
||||||
|
content["format"] = "org.matrix.custom.html"
|
||||||
|
content["formatted_body"] = content["body"]
|
||||||
|
# try to send the message
|
||||||
|
try:
|
||||||
|
result = await self._client.room_send(
|
||||||
|
room_id=room_id,
|
||||||
|
message_type="m.room.message",
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
except OlmUnverifiedDeviceError:
|
||||||
|
if self._config.auto_verify_all_known_devices:
|
||||||
|
if not self.verify_all_known_devices():
|
||||||
|
raise
|
||||||
|
return await self.send_text_to_room(room_id, text, is_html, **kwargs)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
# success
|
||||||
|
if type(result) is RoomSendResponse:
|
||||||
|
return result.event_id
|
||||||
|
# error
|
||||||
|
elif type(result) is RoomSendError:
|
||||||
|
raise RuntimeError(result)
|
||||||
|
# unknown error
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unknown error has occured", result)
|
||||||
Reference in New Issue
Block a user