Compare commits
No commits in common. "d7b0b9013f30beab723dacbf75e1dd5fc8183eaa" and "ace4eefec7efeab3cf19336fd023dfd4dd46dca1" have entirely different histories.
d7b0b9013f
...
ace4eefec7
17
ROADMAP.md
17
ROADMAP.md
@ -4,11 +4,11 @@
|
|||||||
* [ ] Message content
|
* [ ] Message content
|
||||||
* [x] Text
|
* [x] Text
|
||||||
* [ ] Media
|
* [ ] Media
|
||||||
* [x] Stickers
|
* [ ] Stickers
|
||||||
* [x] Files
|
* [ ] Files
|
||||||
* [x] Voice messages
|
* [ ] Voice messages
|
||||||
* [x] Videos
|
* [ ] Videos
|
||||||
* [x] Images
|
* [ ] Images
|
||||||
* [ ] Locations
|
* [ ] Locations
|
||||||
* [ ] Formatting
|
* [ ] Formatting
|
||||||
* [ ] Replies
|
* [ ] Replies
|
||||||
@ -32,11 +32,8 @@
|
|||||||
* [x] Text
|
* [x] Text
|
||||||
* [ ] Media
|
* [ ] Media
|
||||||
* [ ] Stickers
|
* [ ] Stickers
|
||||||
* [ ] Files
|
* [ ] Videos
|
||||||
* [x] Voice messages
|
* [ ] Images
|
||||||
* [x] Videos
|
|
||||||
* [x] Images
|
|
||||||
* [ ] Locations
|
|
||||||
* [ ] Formatting
|
* [ ] Formatting
|
||||||
* [ ] Replies
|
* [ ] Replies
|
||||||
* [ ] Mentions
|
* [ ] Mentions
|
||||||
|
@ -27,6 +27,7 @@ class Attachment(SerializableAttrs):
|
|||||||
shout: Optional[bool] = None
|
shout: Optional[bool] = None
|
||||||
mentions: Optional[list[MentionStruct]] = None
|
mentions: Optional[list[MentionStruct]] = None
|
||||||
urls: Optional[list[str]] = None
|
urls: Optional[list[str]] = None
|
||||||
|
url: Optional[str] = None # NOTE Added since this may have replaced urls
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -21,20 +21,14 @@ from . import Attachment
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MediaAttachment(Attachment):
|
class MediaKeyAttachment(Attachment):
|
||||||
# NOTE Added to cover Attachments that need a url but might not have a key
|
k: str
|
||||||
url: str
|
url: str
|
||||||
s: int
|
s: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MediaKeyAttachment(MediaAttachment):
|
class PhotoAttachment(Attachment):
|
||||||
k: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PhotoAttachment(MediaKeyAttachment):
|
|
||||||
# NOTE Changed superclass from Attachment
|
|
||||||
w: int
|
w: int
|
||||||
h: int
|
h: int
|
||||||
thumbnailUrl: str
|
thumbnailUrl: str
|
||||||
@ -54,13 +48,11 @@ class MultiPhotoAttachment(Attachment):
|
|||||||
thumbnailUrls: list[str]
|
thumbnailUrls: list[str]
|
||||||
thumbnailWidths: list[int]
|
thumbnailWidths: list[int]
|
||||||
thumbnailHeights: list[int]
|
thumbnailHeights: list[int]
|
||||||
sl: list[int] # NOTE Changed to a list
|
sl: int
|
||||||
mtl: list[str] # NOTE Added
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VideoAttachment(MediaAttachment):
|
class VideoAttachment(Attachment):
|
||||||
# NOTE Changed superclass from Attachment
|
|
||||||
tk: str
|
tk: str
|
||||||
w: int
|
w: int
|
||||||
h: int
|
h: int
|
||||||
@ -69,8 +61,7 @@ class VideoAttachment(MediaAttachment):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileAttachment(MediaKeyAttachment):
|
class FileAttachment(Attachment):
|
||||||
# NOTE Changed superclass from Attachment
|
|
||||||
name: str
|
name: str
|
||||||
size: int
|
size: int
|
||||||
expire: int
|
expire: int
|
||||||
@ -78,8 +69,7 @@ class FileAttachment(MediaKeyAttachment):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AudioAttachment(MediaKeyAttachment):
|
class AudioAttachment(Attachment):
|
||||||
# NOTE Changed superclass from Attachment
|
|
||||||
d: int
|
d: int
|
||||||
expire: Optional[int] = None
|
expire: Optional[int] = None
|
||||||
|
|
||||||
@ -93,7 +83,6 @@ class LongTextAttachment(Attachment):
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"MediaAttachment",
|
|
||||||
"MediaKeyAttachment",
|
"MediaKeyAttachment",
|
||||||
"PhotoAttachment",
|
"PhotoAttachment",
|
||||||
"MultiPhotoAttachment",
|
"MultiPhotoAttachment",
|
||||||
|
@ -35,7 +35,6 @@ from .attachment import (
|
|||||||
FileAttachment,
|
FileAttachment,
|
||||||
PostAttachment,
|
PostAttachment,
|
||||||
ReplyAttachment,
|
ReplyAttachment,
|
||||||
MultiPhotoAttachment,
|
|
||||||
)
|
)
|
||||||
from .chat_type import ChatType, KnownChatType
|
from .chat_type import ChatType, KnownChatType
|
||||||
|
|
||||||
@ -75,7 +74,6 @@ _attachment_type_map: dict[KnownChatType, Type[Attachment]] = {
|
|||||||
KnownChatType.FILE: FileAttachment,
|
KnownChatType.FILE: FileAttachment,
|
||||||
KnownChatType.POST: PostAttachment,
|
KnownChatType.POST: PostAttachment,
|
||||||
KnownChatType.REPLY: ReplyAttachment,
|
KnownChatType.REPLY: ReplyAttachment,
|
||||||
KnownChatType.MULTIPHOTO: MultiPhotoAttachment,
|
|
||||||
KnownChatType.OPEN_SCHEDULE: OpenScheduleAttachment,
|
KnownChatType.OPEN_SCHEDULE: OpenScheduleAttachment,
|
||||||
KnownChatType.OPEN_VOTE: VoteAttachment,
|
KnownChatType.OPEN_VOTE: VoteAttachment,
|
||||||
KnownChatType.OPEN_POST: PostAttachment,
|
KnownChatType.OPEN_POST: PostAttachment,
|
||||||
|
@ -42,7 +42,6 @@ from mautrix.types import (
|
|||||||
FileInfo,
|
FileInfo,
|
||||||
ImageInfo,
|
ImageInfo,
|
||||||
LocationMessageEventContent,
|
LocationMessageEventContent,
|
||||||
MediaInfo,
|
|
||||||
MediaMessageEventContent,
|
MediaMessageEventContent,
|
||||||
Membership,
|
Membership,
|
||||||
MessageEventContent,
|
MessageEventContent,
|
||||||
@ -69,13 +68,7 @@ from .kt.types.channel.channel_info import ChannelInfo
|
|||||||
from .kt.types.channel.channel_type import KnownChannelType, ChannelType
|
from .kt.types.channel.channel_type import KnownChannelType, ChannelType
|
||||||
from .kt.types.chat import Chatlog, KnownChatType
|
from .kt.types.chat import Chatlog, KnownChatType
|
||||||
from .kt.types.chat.attachment import (
|
from .kt.types.chat.attachment import (
|
||||||
Attachment,
|
|
||||||
AudioAttachment,
|
|
||||||
#FileAttachment,
|
|
||||||
MediaAttachment,
|
|
||||||
MultiPhotoAttachment,
|
|
||||||
PhotoAttachment,
|
PhotoAttachment,
|
||||||
VideoAttachment,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .kt.client.types import (
|
from .kt.client.types import (
|
||||||
@ -190,25 +183,14 @@ class Portal(DBPortal, BasePortal):
|
|||||||
NotificationDisabler.config_enabled = cls.config["bridge.backfill.disable_notifications"]
|
NotificationDisabler.config_enabled = cls.config["bridge.backfill.disable_notifications"]
|
||||||
|
|
||||||
# TODO More
|
# TODO More
|
||||||
cls._message_type_handler_map: dict[
|
cls._message_handler_type_map: dict[
|
||||||
KnownChatType,
|
KnownChatType,
|
||||||
Callable[
|
Callable[[Portal, u.User, IntentAPI, Chatlog], Awaitable[list[EventID]]]
|
||||||
[
|
|
||||||
Portal,
|
|
||||||
u.User,
|
|
||||||
IntentAPI,
|
|
||||||
Attachment | None,
|
|
||||||
int,
|
|
||||||
str | None
|
|
||||||
],
|
|
||||||
Awaitable[list[EventID]]
|
|
||||||
]
|
|
||||||
] = {
|
] = {
|
||||||
KnownChatType.TEXT: cls._handle_remote_text,
|
KnownChatType.TEXT: cls._handle_remote_text,
|
||||||
KnownChatType.PHOTO: cls._handle_remote_photo,
|
KnownChatType.PHOTO: cls._handle_remote_photo,
|
||||||
KnownChatType.MULTIPHOTO: cls._handle_remote_multiphoto,
|
#KnownChatType.VIDEO: cls._handle_remote_video,
|
||||||
KnownChatType.VIDEO: cls._handle_remote_video,
|
#KnownChatType.AUDIO: cls._handle_remote_audio,
|
||||||
KnownChatType.AUDIO: cls._handle_remote_audio,
|
|
||||||
#KnownChatType.FILE: cls._handle_remote_file,
|
#KnownChatType.FILE: cls._handle_remote_file,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,7 +312,6 @@ class Portal(DBPortal, BasePortal):
|
|||||||
intent: IntentAPI,
|
intent: IntentAPI,
|
||||||
*,
|
*,
|
||||||
filename: str | None = None,
|
filename: str | None = None,
|
||||||
mimetype: str | None,
|
|
||||||
encrypt: bool = False,
|
encrypt: bool = False,
|
||||||
find_size: bool = False,
|
find_size: bool = False,
|
||||||
convert_audio: bool = False,
|
convert_audio: bool = False,
|
||||||
@ -344,19 +325,18 @@ class Portal(DBPortal, BasePortal):
|
|||||||
if length > cls.matrix.media_config.upload_size:
|
if length > cls.matrix.media_config.upload_size:
|
||||||
raise ValueError("File not available: too large")
|
raise ValueError("File not available: too large")
|
||||||
data = await resp.read()
|
data = await resp.read()
|
||||||
if not mimetype:
|
mime = magic.mimetype(data)
|
||||||
mimetype = magic.mimetype(data)
|
if convert_audio and mime != "audio/ogg":
|
||||||
if convert_audio and mimetype != "audio/ogg":
|
|
||||||
data = await ffmpeg.convert_bytes(
|
data = await ffmpeg.convert_bytes(
|
||||||
data, ".ogg", output_args=("-c:a", "libopus"), input_mime=mimetype
|
data, ".ogg", output_args=("-c:a", "libopus"), input_mime=mime
|
||||||
)
|
)
|
||||||
mimetype = "audio/ogg"
|
mime = "audio/ogg"
|
||||||
info = FileInfo(mimetype=mimetype, size=len(data))
|
info = FileInfo(mimetype=mime, size=len(data))
|
||||||
if Image and mimetype.startswith("image/") and find_size:
|
if Image and mime.startswith("image/") and find_size:
|
||||||
with Image.open(BytesIO(data)) as img:
|
with Image.open(BytesIO(data)) as img:
|
||||||
width, height = img.size
|
width, height = img.size
|
||||||
info = ImageInfo(mimetype=mimetype, size=len(data), width=width, height=height)
|
info = ImageInfo(mimetype=mime, size=len(data), width=width, height=height)
|
||||||
upload_mime_type = mimetype
|
upload_mime_type = mime
|
||||||
decryption_info = None
|
decryption_info = None
|
||||||
if encrypt and encrypt_attachment:
|
if encrypt and encrypt_attachment:
|
||||||
data, decryption_info = encrypt_attachment(data)
|
data, decryption_info = encrypt_attachment(data)
|
||||||
@ -827,7 +807,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
data = await self.main_intent.download_media(message.url)
|
data = await self.main_intent.download_media(message.url)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("No file or URL specified")
|
raise NotImplementedError("No file or URL specified")
|
||||||
mimetype = message.info.mimetype or magic.mimetype(data)
|
mime = message.info.mimetype or magic.mimetype(data)
|
||||||
""" TODO Replies
|
""" TODO Replies
|
||||||
reply_to = None
|
reply_to = None
|
||||||
if message.relates_to.rel_type == RelationType.REPLY:
|
if message.relates_to.rel_type == RelationType.REPLY:
|
||||||
@ -842,6 +822,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
"""
|
"""
|
||||||
filename = message.body
|
filename = message.body
|
||||||
width, height = None, None
|
width, height = None, None
|
||||||
|
# TODO Find out why/if stickers are always blank
|
||||||
if message.info in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO):
|
if message.info in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO):
|
||||||
width = message.info.width
|
width = message.info.width
|
||||||
height = message.info.height
|
height = message.info.height
|
||||||
@ -853,7 +834,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
filename,
|
filename,
|
||||||
width=width,
|
width=width,
|
||||||
height=height,
|
height=height,
|
||||||
ext=guess_extension(mimetype)[1:],
|
ext=guess_extension(mime)[1:],
|
||||||
# TODO
|
# TODO
|
||||||
#reply_to=reply_to,
|
#reply_to=reply_to,
|
||||||
)
|
)
|
||||||
@ -996,24 +977,16 @@ class Portal(DBPortal, BasePortal):
|
|||||||
await intent.ensure_joined(self.mxid)
|
await intent.ensure_joined(self.mxid)
|
||||||
self._backfill_leave.add(intent)
|
self._backfill_leave.add(intent)
|
||||||
|
|
||||||
handler = self._message_type_handler_map.get(message.type)
|
event_ids = []
|
||||||
|
handler = self._message_handler_type_map.get(message.type)
|
||||||
if not handler:
|
if not handler:
|
||||||
self.log.warning(f"No handler for message type {message.type}, falling back to text")
|
self.log.warning(f"No handler for message type {message.type}, falling back to text")
|
||||||
handler = Portal._handle_remote_text
|
handler = Portal._handle_remote_text
|
||||||
event_ids = [
|
event_ids += await handler(self, source, intent, message)
|
||||||
event_id for event_id in
|
|
||||||
await handler(
|
|
||||||
self,
|
|
||||||
source,
|
|
||||||
intent,
|
|
||||||
message.attachment,
|
|
||||||
message.sendAt,
|
|
||||||
message.text)
|
|
||||||
if event_id
|
|
||||||
]
|
|
||||||
if not event_ids:
|
if not event_ids:
|
||||||
self.log.warning(f"Unhandled KakaoTalk message {message.logId}")
|
self.log.warning(f"Unhandled KakaoTalk message {message.logId}")
|
||||||
return
|
return
|
||||||
|
event_ids = [event_id for event_id in event_ids if event_id]
|
||||||
self.log.debug(f"Handled KakaoTalk message {message.logId} -> {event_ids}")
|
self.log.debug(f"Handled KakaoTalk message {message.logId} -> {event_ids}")
|
||||||
await DBMessage.bulk_create(
|
await DBMessage.bulk_create(
|
||||||
ktid=message.logId,
|
ktid=message.logId,
|
||||||
@ -1025,162 +998,38 @@ class Portal(DBPortal, BasePortal):
|
|||||||
)
|
)
|
||||||
await self._send_delivery_receipt(event_ids[-1])
|
await self._send_delivery_receipt(event_ids[-1])
|
||||||
|
|
||||||
async def _handle_remote_text(
|
async def _handle_remote_text(self, source: u.User, intent: IntentAPI, message: Chatlog) -> list[EventID]:
|
||||||
self,
|
|
||||||
source: u.User,
|
|
||||||
intent: IntentAPI,
|
|
||||||
attachment: None,
|
|
||||||
timestamp: int,
|
|
||||||
message_text: str | None,
|
|
||||||
) -> list[EventID]:
|
|
||||||
# TODO Handle mentions properly
|
# TODO Handle mentions properly
|
||||||
content = await kakaotalk_to_matrix(message_text)
|
content = await kakaotalk_to_matrix(message.text)
|
||||||
# TODO Replies
|
# TODO Replies
|
||||||
return [await self._send_message(intent, content, timestamp=timestamp)]
|
return [await self._send_message(intent, content, timestamp=message.sendAt)]
|
||||||
|
|
||||||
def _handle_remote_photo(
|
async def _handle_remote_photo(self, source: u.User, intent: IntentAPI, message: Chatlog) -> list[EventID]:
|
||||||
self,
|
assert message.attachment
|
||||||
source: u.User,
|
assert message.attachment.url or message.attachment.urls
|
||||||
intent: IntentAPI,
|
url = message.attachment.url or message.attachment.urls[0]
|
||||||
attachment: PhotoAttachment,
|
assert isinstance(message.attachment, PhotoAttachment)
|
||||||
timestamp: int,
|
info = ImageInfo(
|
||||||
message_text: str | None,
|
width=message.attachment.w,
|
||||||
) -> Awaitable[list[EventID]]:
|
height=message.attachment.h,
|
||||||
return asyncio.gather(self._handle_remote_uniphoto(
|
|
||||||
source, intent, attachment, timestamp, message_text
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_remote_multiphoto(
|
|
||||||
self,
|
|
||||||
source: u.User,
|
|
||||||
intent: IntentAPI,
|
|
||||||
attachment: MultiPhotoAttachment,
|
|
||||||
timestamp: int,
|
|
||||||
message_text: str | None,
|
|
||||||
) -> Awaitable[list[EventID]]:
|
|
||||||
# TODO Upload media concurrently, but post messages sequentially
|
|
||||||
return asyncio.gather(
|
|
||||||
*[
|
|
||||||
self._handle_remote_uniphoto(
|
|
||||||
source, intent,
|
|
||||||
PhotoAttachment(
|
|
||||||
shout=attachment.shout,
|
|
||||||
mentions=attachment.mentions,
|
|
||||||
urls=attachment.urls,
|
|
||||||
url=attachment.imageUrls[i],
|
|
||||||
s=attachment.sl[i],
|
|
||||||
k=attachment.kl[i],
|
|
||||||
w=attachment.wl[i],
|
|
||||||
h=attachment.hl[i],
|
|
||||||
thumbnailUrl=attachment.thumbnailUrls[i],
|
|
||||||
thumbnailWidth=attachment.thumbnailWidths[i],
|
|
||||||
thumbnailHeight=attachment.thumbnailHeights[i],
|
|
||||||
cs=attachment.csl[i],
|
|
||||||
mt=attachment.mtl[i],
|
|
||||||
),
|
|
||||||
timestamp, message_text,
|
|
||||||
)
|
|
||||||
for i in range(len(attachment.imageUrls))
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
# TODO Animated images?
|
||||||
def _handle_remote_uniphoto(
|
|
||||||
self,
|
|
||||||
source: u.User,
|
|
||||||
intent: IntentAPI,
|
|
||||||
attachment: PhotoAttachment,
|
|
||||||
timestamp: int,
|
|
||||||
message_text: str | None,
|
|
||||||
) -> Awaitable[EventID]:
|
|
||||||
return self._handle_remote_media(
|
|
||||||
source, intent, attachment, timestamp, message_text,
|
|
||||||
ImageInfo(
|
|
||||||
mimetype=attachment.mt,
|
|
||||||
size=attachment.s,
|
|
||||||
width=attachment.w,
|
|
||||||
height=attachment.h,
|
|
||||||
),
|
|
||||||
MessageType.IMAGE,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _handle_remote_video(
|
|
||||||
self,
|
|
||||||
source: u.User,
|
|
||||||
intent: IntentAPI,
|
|
||||||
attachment: VideoAttachment,
|
|
||||||
timestamp: int,
|
|
||||||
message_text: str | None,
|
|
||||||
) -> Awaitable[list[EventID]]:
|
|
||||||
return asyncio.gather(self._handle_remote_media(
|
|
||||||
source, intent, attachment, timestamp, message_text,
|
|
||||||
VideoInfo(
|
|
||||||
duration=attachment.d,
|
|
||||||
width=attachment.w,
|
|
||||||
height=attachment.h,
|
|
||||||
),
|
|
||||||
MessageType.VIDEO,
|
|
||||||
))
|
|
||||||
|
|
||||||
def _handle_remote_audio(
|
|
||||||
self,
|
|
||||||
source: u.User,
|
|
||||||
intent: IntentAPI,
|
|
||||||
attachment: AudioAttachment,
|
|
||||||
timestamp: int,
|
|
||||||
message_text: str | None,
|
|
||||||
) -> Awaitable[list[EventID]]:
|
|
||||||
return asyncio.gather(self._handle_remote_media(
|
|
||||||
source, intent, attachment, timestamp, message_text,
|
|
||||||
AudioInfo(
|
|
||||||
size=attachment.s,
|
|
||||||
duration=attachment.d,
|
|
||||||
),
|
|
||||||
MessageType.AUDIO,
|
|
||||||
))
|
|
||||||
|
|
||||||
""" TODO Find what auth is required for reading file contents
|
|
||||||
def _handle_remote_file(
|
|
||||||
self,
|
|
||||||
source: u.User,
|
|
||||||
intent: IntentAPI,
|
|
||||||
attachment: FileAttachment,
|
|
||||||
timestamp: int,
|
|
||||||
message_text: str | None,
|
|
||||||
) -> Awaitable[list[EventID]]:
|
|
||||||
return asyncio.gather(self._handle_remote_media(
|
|
||||||
source, intent, attachment, timestamp, message_text,
|
|
||||||
FileInfo(
|
|
||||||
size=attachment.size,
|
|
||||||
),
|
|
||||||
MessageType.FILE,
|
|
||||||
))
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def _handle_remote_media(
|
|
||||||
self,
|
|
||||||
source: u.User,
|
|
||||||
intent: IntentAPI,
|
|
||||||
attachment: MediaAttachment,
|
|
||||||
timestamp: int,
|
|
||||||
message_text: str | None,
|
|
||||||
info: MediaInfo,
|
|
||||||
msgtype: MessageType,
|
|
||||||
) -> EventID:
|
|
||||||
mxc, additional_info, decryption_info = await self._reupload_remote_file(
|
mxc, additional_info, decryption_info = await self._reupload_remote_file(
|
||||||
attachment.url,
|
url,
|
||||||
source,
|
source,
|
||||||
intent,
|
intent,
|
||||||
mimetype=info.mimetype,
|
|
||||||
encrypt=self.encrypted,
|
encrypt=self.encrypted,
|
||||||
find_size=False,
|
find_size=False,
|
||||||
)
|
)
|
||||||
info.size = additional_info.size
|
info.size = additional_info.size
|
||||||
info.mimetype = additional_info.mimetype
|
info.mimetype = additional_info.mimetype
|
||||||
content = MediaMessageEventContent(
|
content = MediaMessageEventContent(
|
||||||
url=mxc, file=decryption_info, msgtype=msgtype, body=message_text, info=info
|
url=mxc, file=decryption_info, msgtype=MessageType.IMAGE, body=message.text, info=info
|
||||||
)
|
)
|
||||||
|
if not content:
|
||||||
|
return []
|
||||||
# TODO Replies
|
# TODO Replies
|
||||||
return await self._send_message(intent, content, timestamp=timestamp)
|
return [await self._send_message(intent, content, timestamp=message.sendAt)]
|
||||||
|
|
||||||
# TODO Many more remote handlers
|
# TODO Many more remote handlers
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user