Compare commits

...

3 commits
v1.0.0 ... main

Author SHA1 Message Date
f6292b1a13
v1.1.1
Gracefully shutdown program

format the code
2023-07-21 12:58:39 +08:00
f20b1d5d40
v1.1.0
Update dependencies

remove useless
2023-07-20 16:04:49 +08:00
d474d14879
Add JetBrains sponsor link 2023-05-02 10:15:17 +08:00
5 changed files with 237 additions and 182 deletions

View file

@ -98,4 +98,7 @@ docker compose up -d
## Thanks ## Thanks
1. https://github.com/guillaumekln/faster-whisper 1. https://github.com/guillaumekln/faster-whisper
2. https://github.com/poljar/matrix-nio 2. https://github.com/poljar/matrix-nio
3. https://github.com/8go/matrix-commander 3. https://github.com/8go/matrix-commander
<a href="https://jb.gg/OpenSourceSupport" target="_blank">
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo." width="200" height="200">
</a>

299
bot.py
View file

@ -1,4 +1,5 @@
import os import os
import signal
import sys import sys
import traceback import traceback
from typing import Union, Optional from typing import Union, Optional
@ -6,12 +7,27 @@ import aiofiles
import asyncio import asyncio
import uuid import uuid
import json import json
from nio import (AsyncClient, AsyncClientConfig, InviteMemberEvent, JoinError, from nio import (
KeyVerificationCancel, KeyVerificationEvent, DownloadError, AsyncClient,
KeyVerificationKey, KeyVerificationMac, KeyVerificationStart, AsyncClientConfig,
LocalProtocolError, LoginResponse, MatrixRoom, MegolmEvent, InviteMemberEvent,
RoomMessageAudio, RoomEncryptedAudio, ToDeviceError, crypto, JoinError,
EncryptionError) KeyVerificationCancel,
KeyVerificationEvent,
DownloadError,
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationStart,
LocalProtocolError,
LoginResponse,
MatrixRoom,
MegolmEvent,
RoomMessageAudio,
RoomEncryptedAudio,
ToDeviceError,
crypto,
EncryptionError,
)
from nio.store.database import SqliteStore from nio.store.database import SqliteStore
from faster_whisper import WhisperModel from faster_whisper import WhisperModel
@ -40,12 +56,11 @@ class Bot:
num_workers: int = 1, num_workers: int = 1,
download_root: str = "models", download_root: str = "models",
): ):
if (homeserver is None or user_id is None if homeserver is None or user_id is None or device_id is None:
or device_id is None):
logger.warning("homeserver && user_id && device_id is required") logger.warning("homeserver && user_id && device_id is required")
sys.exit(1) sys.exit(1)
if (password is None and access_token is None): if password is None and access_token is None:
logger.warning("password or access_toekn is required") logger.warning("password or access_toekn is required")
sys.exit(1) sys.exit(1)
@ -87,73 +102,57 @@ class Bot:
# initialize AsyncClient object # initialize AsyncClient object
self.store_path = os.getcwd() self.store_path = os.getcwd()
self.config = AsyncClientConfig(store=SqliteStore, self.config = AsyncClientConfig(
store_name="db", store=SqliteStore,
store_sync_tokens=True, store_name="db",
encryption_enabled=True, store_sync_tokens=True,
) encryption_enabled=True,
self.client = AsyncClient(homeserver=self.homeserver, user=self.user_id, device_id=self.device_id, )
config=self.config, store_path=self.store_path,) self.client = AsyncClient(
homeserver=self.homeserver,
user=self.user_id,
device_id=self.device_id,
config=self.config,
store_path=self.store_path,
)
if self.access_token is not None: if self.access_token is not None:
self.client.access_token = self.access_token self.client.access_token = self.access_token
# setup event callbacks # setup event callbacks
self.client.add_event_callback( self.client.add_event_callback(
self.message_callback, (RoomMessageAudio, RoomEncryptedAudio, )) self.message_callback,
self.client.add_event_callback( (
self.decryption_failure, (MegolmEvent, )) RoomMessageAudio,
self.client.add_event_callback( RoomEncryptedAudio,
self.invite_callback, (InviteMemberEvent, )) ),
)
self.client.add_event_callback(self.decryption_failure, (MegolmEvent,))
self.client.add_event_callback(self.invite_callback, (InviteMemberEvent,))
self.client.add_to_device_callback( self.client.add_to_device_callback(
self.to_device_callback, (KeyVerificationEvent, )) self.to_device_callback, (KeyVerificationEvent,)
)
# intialize whisper model # intialize whisper model
if not os.path.exists(os.path.join(self.download_root, "model.txt")):
# that means we have not downloaded the model yet
logger.info("Model downloading......")
self.model_size_or_path = self.model_size
with open(os.path.join(self.download_root, "model.txt"), "w") as f:
f.write(self.model_size)
f.close()
else:
# model exists
f = open(os.path.join(self.download_root, "model.txt"), "r")
old_model = f.read()
if old_model != self.model_size:
# model size changed
self.model_size_or_path = self.model_size
f.close()
with open(os.path.join(self.download_root, "model.txt"), "w") as f:
f.write(self.model_size)
f.close()
else:
# use existed model
self.model_size_or_path = self.download_root
self.model = WhisperModel( self.model = WhisperModel(
model_size_or_path=self.model_size_or_path, model_size_or_path=self.model_size,
device=self.device, device=self.device,
compute_type=self.compute_type, compute_type=self.compute_type,
cpu_threads=self.cpu_threads, cpu_threads=self.cpu_threads,
num_workers=self.num_workers, num_workers=self.num_workers,
download_root=self.download_root,) download_root=self.download_root,
)
def __del__(self): async def close(self, task: asyncio.Task = None) -> None:
try:
loop = asyncio.get_running_loop()
except RuntimeError as e:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self._close())
async def _close(self):
await self.client.close() await self.client.close()
logger.info("Bot stopped!") task.cancel()
logger.info("Bot closed!")
# message_callback event # message_callback event
async def message_callback(self, room: MatrixRoom,
event: Union[RoomMessageAudio, RoomEncryptedAudio]) -> None: async def message_callback(
self, room: MatrixRoom, event: Union[RoomMessageAudio, RoomEncryptedAudio]
) -> None:
if self.room_id is None: if self.room_id is None:
room_id = room.room_id room_id = room.room_id
else: else:
@ -200,12 +199,8 @@ class Bot:
await f.write( await f.write(
crypto.attachments.decrypt_attachment( crypto.attachments.decrypt_attachment(
media_data, media_data,
event.source["content"]["file"]["key"][ event.source["content"]["file"]["key"]["k"],
"k" event.source["content"]["file"]["hashes"]["sha256"],
],
event.source["content"]["file"]["hashes"][
"sha256"
],
event.source["content"]["file"]["iv"], event.source["content"]["file"]["iv"],
) )
) )
@ -238,8 +233,8 @@ class Bot:
return return
logger.error( logger.error(
f"Failed to decrypt message: {event.event_id} from {event.sender} in {room.room_id}\n" + f"Failed to decrypt message: {event.event_id} from {event.sender} in {room.room_id}\n"
"Please make sure the bot current session is verified" + "Please make sure the bot current session is verified"
) )
# invite_callback event # invite_callback event
@ -255,7 +250,8 @@ class Bot:
if type(result) == JoinError: if type(result) == JoinError:
logger.error( logger.error(
f"Error joining room {room.room_id} (attempt %d): %s", f"Error joining room {room.room_id} (attempt %d): %s",
attempt, result.message, attempt,
result.message,
) )
else: else:
break break
@ -277,11 +273,11 @@ class Bot:
try: try:
client = self.client client = self.client
logger.debug( logger.debug(
f"Device Event of type {type(event)} received in " f"Device Event of type {type(event)} received in " "to_device_cb()."
"to_device_cb().") )
if isinstance(event, KeyVerificationStart): # first step if isinstance(event, KeyVerificationStart): # first step
""" first step: receive KeyVerificationStart """first step: receive KeyVerificationStart
KeyVerificationStart( KeyVerificationStart(
source={'content': source={'content':
{'method': 'm.sas.v1', {'method': 'm.sas.v1',
@ -311,13 +307,14 @@ class Bot:
""" """
if "emoji" not in event.short_authentication_string: if "emoji" not in event.short_authentication_string:
estr = ("Other device does not support emoji verification " estr = (
f"{event.short_authentication_string}. Aborting.") "Other device does not support emoji verification "
f"{event.short_authentication_string}. Aborting."
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
return return
resp = await client.accept_key_verification( resp = await client.accept_key_verification(event.transaction_id)
event.transaction_id)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = f"accept_key_verification() failed with {resp}" estr = f"accept_key_verification() failed with {resp}"
print(estr) print(estr)
@ -333,7 +330,7 @@ class Bot:
logger.info(estr) logger.info(estr)
elif isinstance(event, KeyVerificationCancel): # anytime elif isinstance(event, KeyVerificationCancel): # anytime
""" at any time: receive KeyVerificationCancel """at any time: receive KeyVerificationCancel
KeyVerificationCancel(source={ KeyVerificationCancel(source={
'content': {'code': 'm.mismatched_sas', 'content': {'code': 'm.mismatched_sas',
'reason': 'Mismatched authentication string', 'reason': 'Mismatched authentication string',
@ -350,13 +347,15 @@ class Bot:
# client.cancel_key_verification(tx_id, reject=False) # client.cancel_key_verification(tx_id, reject=False)
# here. The SAS flow is already cancelled. # here. The SAS flow is already cancelled.
# We only need to inform the user. # We only need to inform the user.
estr = (f"Verification has been cancelled by {event.sender} " estr = (
f"for reason \"{event.reason}\".") f"Verification has been cancelled by {event.sender} "
f'for reason "{event.reason}".'
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
elif isinstance(event, KeyVerificationKey): # second step elif isinstance(event, KeyVerificationKey): # second step
""" Second step is to receive KeyVerificationKey """Second step is to receive KeyVerificationKey
KeyVerificationKey( KeyVerificationKey(
source={'content': { source={'content': {
'key': 'SomeCryptoKey', 'key': 'SomeCryptoKey',
@ -381,42 +380,44 @@ class Bot:
# automatic match, so we use y # automatic match, so we use y
yn = "y" yn = "y"
if yn.lower() == "y": if yn.lower() == "y":
estr = ("Match! The verification for this " estr = (
"device will be accepted.") "Match! The verification for this " "device will be accepted."
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
resp = await client.confirm_short_auth_string( resp = await client.confirm_short_auth_string(event.transaction_id)
event.transaction_id)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = ("confirm_short_auth_string() " estr = "confirm_short_auth_string() " f"failed with {resp}"
f"failed with {resp}")
print(estr) print(estr)
logger.info(estr) logger.info(estr)
elif yn.lower() == "n": # no, don't match, reject elif yn.lower() == "n": # no, don't match, reject
estr = ("No match! Device will NOT be verified " estr = (
"by rejecting verification.") "No match! Device will NOT be verified "
"by rejecting verification."
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
resp = await client.cancel_key_verification( resp = await client.cancel_key_verification(
event.transaction_id, reject=True) event.transaction_id, reject=True
)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = (f"cancel_key_verification failed with {resp}") estr = f"cancel_key_verification failed with {resp}"
print(estr) print(estr)
logger.info(estr) logger.info(estr)
else: # C or anything for cancel else: # C or anything for cancel
estr = ("Cancelled by user! Verification will be " estr = "Cancelled by user! Verification will be " "cancelled."
"cancelled.")
print(estr) print(estr)
logger.info(estr) logger.info(estr)
resp = await client.cancel_key_verification( resp = await client.cancel_key_verification(
event.transaction_id, reject=False) event.transaction_id, reject=False
)
if isinstance(resp, ToDeviceError): if isinstance(resp, ToDeviceError):
estr = (f"cancel_key_verification failed with {resp}") estr = f"cancel_key_verification failed with {resp}"
print(estr) print(estr)
logger.info(estr) logger.info(estr)
elif isinstance(event, KeyVerificationMac): # third step elif isinstance(event, KeyVerificationMac): # third step
""" Third step is to receive KeyVerificationMac """Third step is to receive KeyVerificationMac
KeyVerificationMac( KeyVerificationMac(
source={'content': { source={'content': {
'mac': {'ed25519:DEVICEIDXY': 'SomeKey1', 'mac': {'ed25519:DEVICEIDXY': 'SomeKey1',
@ -436,9 +437,11 @@ class Bot:
todevice_msg = sas.get_mac() todevice_msg = sas.get_mac()
except LocalProtocolError as e: except LocalProtocolError as e:
# e.g. it might have been cancelled by ourselves # e.g. it might have been cancelled by ourselves
estr = (f"Cancelled or protocol error: Reason: {e}.\n" estr = (
f"Verification with {event.sender} not concluded. " f"Cancelled or protocol error: Reason: {e}.\n"
"Try again?") f"Verification with {event.sender} not concluded. "
"Try again?"
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
else: else:
@ -447,25 +450,31 @@ class Bot:
estr = f"to_device failed with {resp}" estr = f"to_device failed with {resp}"
print(estr) print(estr)
logger.info(estr) logger.info(estr)
estr = (f"sas.we_started_it = {sas.we_started_it}\n" estr = (
f"sas.sas_accepted = {sas.sas_accepted}\n" f"sas.we_started_it = {sas.we_started_it}\n"
f"sas.canceled = {sas.canceled}\n" f"sas.sas_accepted = {sas.sas_accepted}\n"
f"sas.timed_out = {sas.timed_out}\n" f"sas.canceled = {sas.canceled}\n"
f"sas.verified = {sas.verified}\n" f"sas.timed_out = {sas.timed_out}\n"
f"sas.verified_devices = {sas.verified_devices}\n") f"sas.verified = {sas.verified}\n"
f"sas.verified_devices = {sas.verified_devices}\n"
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
estr = ("Emoji verification was successful!\n" estr = (
"Initiate another Emoji verification from " "Emoji verification was successful!\n"
"another device or room if desired. " "Initiate another Emoji verification from "
"Or if done verifying, hit Control-C to stop the " "another device or room if desired. "
"bot in order to restart it as a service or to " "Or if done verifying, hit Control-C to stop the "
"run it in the background.") "bot in order to restart it as a service or to "
"run it in the background."
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
else: else:
estr = (f"Received unexpected event type {type(event)}. " estr = (
f"Event is {event}. Event will be ignored.") f"Received unexpected event type {type(event)}. "
f"Event is {event}. Event will be ignored."
)
print(estr) print(estr)
logger.info(estr) logger.info(estr)
except BaseException: except BaseException:
@ -474,7 +483,6 @@ class Bot:
logger.info(estr) logger.info(estr)
# bot login # bot login
async def login(self) -> None: async def login(self) -> None:
if self.access_token is not None: if self.access_token is not None:
logger.info("Login via access_token") logger.info("Login via access_token")
@ -502,14 +510,14 @@ class Bot:
# import keys # import keys
async def import_keys(self): async def import_keys(self):
resp = await self.client.import_keys( resp = await self.client.import_keys(
self.import_keys_path, self.import_keys_path, self.import_keys_password
self.import_keys_password
) )
if isinstance(resp, EncryptionError): if isinstance(resp, EncryptionError):
logger.error(f"import_keys failed with {resp}") logger.error(f"import_keys failed with {resp}")
else: else:
logger.info( logger.info(
f"import_keys success, please remove import_keys configuration!!!") f"import_keys success, please remove import_keys configuration!!!"
)
# whisper function # whisper function
def transcribe(self, filename: str) -> str: def transcribe(self, filename: str) -> str:
@ -529,30 +537,33 @@ async def main():
config = json.load(fp) config = json.load(fp)
bot = Bot( bot = Bot(
homeserver=config.get('homeserver'), homeserver=config.get("homeserver"),
user_id=config.get('user_id'), user_id=config.get("user_id"),
password=config.get('password'), password=config.get("password"),
device_id=config.get('device_id'), device_id=config.get("device_id"),
room_id=config.get('room_id'), room_id=config.get("room_id"),
access_token=config.get('access_token'), access_token=config.get("access_token"),
import_keys_path=config.get('import_keys_path'), import_keys_path=config.get("import_keys_path"),
import_keys_password=config.get('import_keys_password'), import_keys_password=config.get("import_keys_password"),
model_size=config.get('model_size'), model_size=config.get("model_size"),
device=config.get('device'), device=config.get("device"),
compute_type=config.get('compute_type'), compute_type=config.get("compute_type"),
cpu_threads=config.get('cpu_threads'), cpu_threads=config.get("cpu_threads"),
num_workers=config.get('num_workers'), num_workers=config.get("num_workers"),
download_root=config.get('download_root'), download_root=config.get("download_root"),
) )
if config.get('import_keys_path') and config.get('import_keys_password') is not None: if (
config.get("import_keys_path")
and config.get("import_keys_password") is not None
):
need_import_keys = True need_import_keys = True
else: else:
bot = Bot( bot = Bot(
homeserver=os.environ.get('HOMESERVER'), homeserver=os.environ.get("HOMESERVER"),
user_id=os.environ.get('USER_ID'), user_id=os.environ.get("USER_ID"),
password=os.environ.get('PASSWORD'), password=os.environ.get("PASSWORD"),
device_id=os.environ.get("DEVICE_ID"), device_id=os.environ.get("DEVICE_ID"),
room_id=os.environ.get("ROOM_ID"), room_id=os.environ.get("ROOM_ID"),
access_token=os.environ.get("ACCESS_TOKEN"), access_token=os.environ.get("ACCESS_TOKEN"),
@ -565,7 +576,10 @@ async def main():
num_workers=os.environ.get("NUM_WORKERS"), num_workers=os.environ.get("NUM_WORKERS"),
download_root=os.environ.get("DOWNLOAD_ROOT"), download_root=os.environ.get("DOWNLOAD_ROOT"),
) )
if os.environ.get("IMPORT_KEYS_PATH") and os.environ.get("IMPORT_KEYS_PASSWORD") is not None: if (
os.environ.get("IMPORT_KEYS_PATH")
and os.environ.get("IMPORT_KEYS_PASSWORD") is not None
):
need_import_keys = True need_import_keys = True
await bot.login() await bot.login()
@ -573,8 +587,21 @@ async def main():
logger.info("start import_keys process, this may take a while...") logger.info("start import_keys process, this may take a while...")
await bot.import_keys() await bot.import_keys()
await bot.sync_forever() sync_task = asyncio.create_task(bot.sync_forever())
if __name__ == '__main__': # handle signal interrupt
loop = asyncio.get_running_loop()
for signame in (
"SIGINT",
"SIGTERM",
):
loop.add_signal_handler(
getattr(signal, signame), lambda: asyncio.create_task(bot.close(sync_task))
)
await sync_task
if __name__ == "__main__":
logger.info("Bot started!") logger.info("Bot started!")
asyncio.run(main()) asyncio.run(main())

10
log.py
View file

@ -9,17 +9,19 @@ def getlogger():
# create handlers # create handlers
warn_handler = logging.StreamHandler() warn_handler = logging.StreamHandler()
info_handler = logging.StreamHandler() info_handler = logging.StreamHandler()
error_handler = logging.FileHandler('bot.log', mode='a') error_handler = logging.FileHandler("bot.log", mode="a")
warn_handler.setLevel(logging.WARNING) warn_handler.setLevel(logging.WARNING)
error_handler.setLevel(logging.ERROR) error_handler.setLevel(logging.ERROR)
info_handler.setLevel(logging.INFO) info_handler.setLevel(logging.INFO)
# create formatters # create formatters
warn_format = logging.Formatter( warn_format = logging.Formatter(
'%(asctime)s - %(funcName)s - %(levelname)s - %(message)s') "%(asctime)s - %(funcName)s - %(levelname)s - %(message)s"
)
error_format = logging.Formatter( error_format = logging.Formatter(
'%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s') "%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s"
info_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') )
info_format = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
# set formatter # set formatter
warn_handler.setFormatter(warn_format) warn_handler.setFormatter(warn_format)

View file

@ -1,5 +1,5 @@
aiofiles==23.1.0 aiofiles==23.1.0
aiohttp==3.8.4 aiohttp==3.8.5
aiohttp-socks==0.7.1 aiohttp-socks==0.7.1
aiosignal==1.3.1 aiosignal==1.3.1
async-timeout==4.0.2 async-timeout==4.0.2
@ -7,44 +7,48 @@ atomicwrites==1.4.1
attrs==23.1.0 attrs==23.1.0
av==10.0.0 av==10.0.0
cachetools==4.2.4 cachetools==4.2.4
certifi==2022.12.7 certifi==2023.5.7
cffi==1.15.1 cffi==1.15.1
charset-normalizer==3.1.0 charset-normalizer==3.2.0
coloredlogs==15.0.1 coloredlogs==15.0.1
ctranslate2==3.12.0 ctranslate2==3.17.0
faster-whisper @ git+https://github.com/guillaumekln/faster-whisper.git@3adcc12d0f91369446a624e33185c555facc8ed2 faster-whisper==0.7.0
filelock==3.12.0 filelock==3.12.2
flatbuffers==23.3.3 flatbuffers==23.5.26
frozenlist==1.3.3 frozenlist==1.4.0
fsspec==2023.6.0
future==0.18.3 future==0.18.3
h11==0.14.0 h11==0.14.0
h2==4.1.0 h2==4.1.0
hpack==4.0.0 hpack==4.0.0
huggingface-hub @ git+https://github.com/huggingface/huggingface_hub.git@a433410ac37f0efaacb92b09684efa3ae46ed3fd huggingface-hub==0.16.4
humanfriendly==10.0 humanfriendly==10.0
hyperframe==6.0.1 hyperframe==6.0.1
idna==3.4 idna==3.4
jsonschema==4.17.3 jsonschema==4.18.4
jsonschema-specifications==2023.7.1
Logbook==1.5.3 Logbook==1.5.3
matrix-nio[e2e]==0.20.2 matrix-nio==0.21.2
mpmath==1.3.0 mpmath==1.3.0
multidict==6.0.4 multidict==6.0.4
numpy==1.24.2 numpy==1.25.1
onnxruntime==1.14.1 onnxruntime==1.15.1
packaging==23.1 packaging==23.1
peewee==3.16.1 peewee==3.16.2
protobuf==4.22.3 protobuf==4.23.4
pycparser==2.21 pycparser==2.21
pycryptodome==3.17 pycryptodome==3.18.0
pyrsistent==0.19.3 pyrsistent==0.19.3
python-olm==3.1.3 python-olm==3.2.15
python-socks==2.2.0 python-socks==2.3.0
PyYAML==6.0 PyYAML==6.0.1
requests==2.28.2 referencing==0.30.0
sympy==1.11.1 requests==2.31.0
rpds-py==0.9.2
sympy==1.12
tokenizers==0.13.3 tokenizers==0.13.3
tqdm==4.65.0 tqdm==4.65.0
typing_extensions==4.5.0 typing_extensions==4.7.1
unpaddedbase64==2.1.0 unpaddedbase64==2.1.0
urllib3==1.26.15 urllib3==2.0.4
yarl==1.8.2 yarl==1.9.2

View file

@ -1,23 +1,42 @@
from nio import AsyncClient from nio import AsyncClient
async def send_room_message(client: AsyncClient,
room_id: str,
reply_message: str,
sender_id: str = '',
reply_to_event_id: str = '',
) -> None:
NORMAL_BODY = content = {"msgtype": "m.text", "body": reply_message, }
if reply_to_event_id == '':
content = NORMAL_BODY
else:
body = r'> <' + sender_id + r'> sent an audio file.\n\n' + reply_message
format = r'org.matrix.custom.html'
formatted_body = r'<mx-reply><blockquote><a href="https://matrix.to/#/' + room_id + r'/' + reply_to_event_id \
+ r'">In reply to</a> <a href="https://matrix.to/#/' + sender_id + r'">' + sender_id \
+ r'</a><br>sent an audio file.</blockquote></mx-reply>' + reply_message
content = {"msgtype": "m.text", "body": body, "format": format, "formatted_body": formatted_body, async def send_room_message(
"m.relates_to": {"m.in_reply_to": {"event_id": reply_to_event_id}}, } client: AsyncClient,
room_id: str,
reply_message: str,
sender_id: str = "",
reply_to_event_id: str = "",
) -> None:
NORMAL_BODY = content = {
"msgtype": "m.text",
"body": reply_message,
}
if reply_to_event_id == "":
content = NORMAL_BODY
else:
body = r"> <" + sender_id + r"> sent an audio file.\n\n" + reply_message
format = r"org.matrix.custom.html"
formatted_body = (
r'<mx-reply><blockquote><a href="https://matrix.to/#/'
+ room_id
+ r"/"
+ reply_to_event_id
+ r'">In reply to</a> <a href="https://matrix.to/#/'
+ sender_id
+ r'">'
+ sender_id
+ r"</a><br>sent an audio file.</blockquote></mx-reply>"
+ reply_message
)
content = {
"msgtype": "m.text",
"body": body,
"format": format,
"formatted_body": formatted_body,
"m.relates_to": {"m.in_reply_to": {"event_id": reply_to_event_id}},
}
await client.room_send( await client.room_send(
room_id, room_id,
message_type="m.room.message", message_type="m.room.message",