From e952c05d35379595ad084cc762cafb591c9d65f0 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Thu, 28 Apr 2022 01:50:47 -0400 Subject: [PATCH] Many fixes thanks to mypy Also add some missing license headers --- .../commands/__init__.py | 15 ++++ matrix_appservice_kakaotalk/commands/conn.py | 11 ++- .../commands/typehint.py | 23 +++++- matrix_appservice_kakaotalk/db/__init__.py | 15 ++++ matrix_appservice_kakaotalk/db/message.py | 4 +- .../db/upgrade/__init__.py | 15 ++++ matrix_appservice_kakaotalk/db/user.py | 4 +- .../formatter/__init__.py | 15 ++++ .../formatter/from_matrix.py | 8 +- .../kt/client/client.py | 6 +- .../kt/client/errors.py | 2 +- .../kt/types/api/struct/account.py | 6 +- matrix_appservice_kakaotalk/kt/types/bson.py | 5 ++ .../kt/types/packet/chat/kickout.py | 1 - matrix_appservice_kakaotalk/portal.py | 81 ++++++++++++------- matrix_appservice_kakaotalk/user.py | 45 ++++++----- 16 files changed, 188 insertions(+), 68 deletions(-) diff --git a/matrix_appservice_kakaotalk/commands/__init__.py b/matrix_appservice_kakaotalk/commands/__init__.py index f92dfd5..d0a7f6f 100644 --- a/matrix_appservice_kakaotalk/commands/__init__.py +++ b/matrix_appservice_kakaotalk/commands/__init__.py @@ -1,3 +1,18 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . from .auth import SECTION_AUTH from .conn import SECTION_CONNECTION from .kakaotalk import SECTION_FRIENDS diff --git a/matrix_appservice_kakaotalk/commands/conn.py b/matrix_appservice_kakaotalk/commands/conn.py index 42eb58b..8af0b99 100644 --- a/matrix_appservice_kakaotalk/commands/conn.py +++ b/matrix_appservice_kakaotalk/commands/conn.py @@ -60,16 +60,19 @@ async def whoami(evt: CommandEvent) -> None: await evt.mark_read() try: own_info = await evt.sender.get_own_info() + except SerializerError: + evt.sender.log.exception("Failed to deserialize settings struct") + own_info = None + except CommandException as e: + await evt.reply(f"Error from KakaoTalk: {e}") + if own_info: await evt.reply( f"You're logged in as `{own_info.more.uuid}` (nickname: {own_info.more.nickName}, user ID: {evt.sender.ktid})." ) - except SerializerError: - evt.sender.log.exception("Failed to deserialize settings struct") + else: await evt.reply( f"You're logged in, but the bridge is unable to retrieve your profile information (user ID: {evt.sender.ktid})." ) - except CommandException as e: - await evt.reply(f"Error from KakaoTalk: {e}") @command_handler( diff --git a/matrix_appservice_kakaotalk/commands/typehint.py b/matrix_appservice_kakaotalk/commands/typehint.py index 9833394..e04e840 100644 --- a/matrix_appservice_kakaotalk/commands/typehint.py +++ b/matrix_appservice_kakaotalk/commands/typehint.py @@ -1,12 +1,31 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from __future__ import annotations + from typing import TYPE_CHECKING from mautrix.bridge.commands import CommandEvent as BaseCommandEvent if TYPE_CHECKING: from ..__main__ import KakaoTalkBridge + from ..portal import Portal from ..user import User class CommandEvent(BaseCommandEvent): - bridge: "KakaoTalkBridge" - sender: "User" + bridge: KakaoTalkBridge + portal: Portal + sender: User diff --git a/matrix_appservice_kakaotalk/db/__init__.py b/matrix_appservice_kakaotalk/db/__init__.py index 22a68f4..3dc2b44 100644 --- a/matrix_appservice_kakaotalk/db/__init__.py +++ b/matrix_appservice_kakaotalk/db/__init__.py @@ -1,3 +1,18 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . from mautrix.util.async_db import Database from .message import Message diff --git a/matrix_appservice_kakaotalk/db/message.py b/matrix_appservice_kakaotalk/db/message.py index 1010565..893d9c0 100644 --- a/matrix_appservice_kakaotalk/db/message.py +++ b/matrix_appservice_kakaotalk/db/message.py @@ -23,7 +23,7 @@ from attr import dataclass, field from mautrix.types import EventID, RoomID from mautrix.util.async_db import Database, Scheme -from ..kt.types.bson import Long +from ..kt.types.bson import Long, to_optional_long fake_db = Database.create("") if TYPE_CHECKING else None @@ -34,7 +34,7 @@ class Message: mxid: EventID mx_room: RoomID - ktid: Long | None = field(converter=lambda ktid: Long(ktid) if ktid is not None else None) + ktid: Long | None = field(converter=to_optional_long) index: int kt_chat: Long = field(converter=Long) kt_receiver: Long = field(converter=Long) diff --git a/matrix_appservice_kakaotalk/db/upgrade/__init__.py b/matrix_appservice_kakaotalk/db/upgrade/__init__.py index 2079708..af7dd17 100644 --- a/matrix_appservice_kakaotalk/db/upgrade/__init__.py +++ b/matrix_appservice_kakaotalk/db/upgrade/__init__.py @@ -1,3 +1,18 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . from mautrix.util.async_db import UpgradeTable upgrade_table = UpgradeTable() diff --git a/matrix_appservice_kakaotalk/db/user.py b/matrix_appservice_kakaotalk/db/user.py index 476e186..7dee70e 100644 --- a/matrix_appservice_kakaotalk/db/user.py +++ b/matrix_appservice_kakaotalk/db/user.py @@ -23,7 +23,7 @@ from attr import dataclass, field from mautrix.types import RoomID, UserID from mautrix.util.async_db import Database -from ..kt.types.bson import Long +from ..kt.types.bson import Long, to_optional_long fake_db = Database.create("") if TYPE_CHECKING else None @@ -33,7 +33,7 @@ class User: db: ClassVar[Database] = fake_db mxid: UserID - ktid: Long | None = field(converter=lambda x: Long(x) if x is not None else None) + ktid: Long | None = field(converter=to_optional_long) uuid: str | None access_token: str | None refresh_token: str | None diff --git a/matrix_appservice_kakaotalk/formatter/__init__.py b/matrix_appservice_kakaotalk/formatter/__init__.py index da0da8e..30c0ee7 100644 --- a/matrix_appservice_kakaotalk/formatter/__init__.py +++ b/matrix_appservice_kakaotalk/formatter/__init__.py @@ -1,2 +1,17 @@ +# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge. +# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . from .from_kakaotalk import kakaotalk_to_matrix from .from_matrix import matrix_to_kakaotalk diff --git a/matrix_appservice_kakaotalk/formatter/from_matrix.py b/matrix_appservice_kakaotalk/formatter/from_matrix.py index 3588cab..9976ea6 100644 --- a/matrix_appservice_kakaotalk/formatter/from_matrix.py +++ b/matrix_appservice_kakaotalk/formatter/from_matrix.py @@ -111,7 +111,7 @@ async def matrix_to_kakaotalk( # NOTE By design, this *throws* if user intent can't be matched (i.e. if a reply can't be created) if content.relates_to.rel_type == RelationType.REPLY and not skip_reply: message = await DBMessage.get_by_mxid(content.relates_to.event_id, room_id) - if not message: + if not message or not message.ktid: raise ValueError( f"Couldn't find reply target {content.relates_to.event_id}" " to bridge text message reply metadata to KakaoTalk" @@ -167,17 +167,17 @@ async def matrix_to_kakaotalk( mxid = mention.extra_info["user_id"] if mxid not in joined_members: continue - ktid = await _get_id_from_mxid(mxid) + ktid = await _get_id_from_mxid(mxid, portal) if ktid is None: continue at += text[last_offset:mention.offset+1].count("@") last_offset = mention.offset+1 - mention = mentions_by_user.setdefault(ktid, MentionStruct( + mention_by_user = mentions_by_user.setdefault(ktid, MentionStruct( at=[], len=mention.length, user_id=ktid, )) - mention.at.append(at) + mention_by_user.at.append(at) mentions = list(mentions_by_user.values()) if mentions_by_user else None else: text = content.body diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py index e0b4ff1..ff9ce1a 100644 --- a/matrix_appservice_kakaotalk/kt/client/client.py +++ b/matrix_appservice_kakaotalk/kt/client/client.py @@ -137,7 +137,7 @@ class Client: return cls._api_request_void("register_device", passcode=passcode, **req) @classmethod - async def login(cls, **req: JSON) -> Awaitable[OAuthCredential]: + def login(cls, **req: JSON) -> Awaitable[OAuthCredential]: """ Obtain a session token by logging in with user-provided credentials. Must have first called register_device with these credentials. @@ -493,7 +493,7 @@ class Client: async def _api_user_cred_request_void(self, command: str, *, renew: bool = True, **data: JSON) -> None: while True: try: - return await self._api_user_request_result( + await self._api_user_request_void( command, oauth_credential=self._oauth_credential, renew=False, **data ) except InvalidAccessToken: @@ -599,7 +599,7 @@ class Client: res = None return self._on_disconnect(res) - def _on_switch_server(self) -> Awaitable[None]: + def _on_switch_server(self, _: dict[str, JSON]) -> Awaitable[None]: # TODO Reconnect automatically instead return self._on_disconnect(KickoutRes(KnownKickoutType.CHANGE_SERVER)) diff --git a/matrix_appservice_kakaotalk/kt/client/errors.py b/matrix_appservice_kakaotalk/kt/client/errors.py index 5acebbf..426f7fd 100644 --- a/matrix_appservice_kakaotalk/kt/client/errors.py +++ b/matrix_appservice_kakaotalk/kt/client/errors.py @@ -70,7 +70,7 @@ class ResponseError(Exception): pass -_status_code_message_map: dict[KnownAuthStatusCode | KnownDataStatusCode | int] = { +_status_code_message_map: dict[KnownAuthStatusCode | KnownDataStatusCode | int, str] = { KnownAuthStatusCode.INVALID_PHONE_NUMBER: "Invalid phone number", KnownAuthStatusCode.SUCCESS_WITH_ACCOUNT: "Success", KnownAuthStatusCode.SUCCESS_WITH_DEVICE_CHANGED: "Success (device changed)", diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/account.py b/matrix_appservice_kakaotalk/kt/types/api/struct/account.py index 3e879e0..53ef528 100644 --- a/matrix_appservice_kakaotalk/kt/types/api/struct/account.py +++ b/matrix_appservice_kakaotalk/kt/types/api/struct/account.py @@ -26,9 +26,9 @@ from ...bson import Long class OpenChatSettingsStruct(SerializableAttrs): chatMemberMaxJoin: int chatRoomMaxJoin: int - createLinkLimit: 10; - createCardLinkLimit: 3; - numOfStaffLimit: 5; + createLinkLimit = 10 + createCardLinkLimit = 3 + numOfStaffLimit = 5 rewritable: bool handoverEnabled: bool diff --git a/matrix_appservice_kakaotalk/kt/types/bson.py b/matrix_appservice_kakaotalk/kt/types/bson.py index 8456ad2..40fae9d 100644 --- a/matrix_appservice_kakaotalk/kt/types/bson.py +++ b/matrix_appservice_kakaotalk/kt/types/bson.py @@ -13,6 +13,8 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Any + from mautrix.types import Serializable, JSON @@ -23,3 +25,6 @@ class Long(int, Serializable): @classmethod def deserialize(cls, raw: JSON) -> "Long": return cls(raw) + +def to_optional_long(x: Any | None) -> Long | None: + return Long(x) if x is not None else None diff --git a/matrix_appservice_kakaotalk/kt/types/packet/chat/kickout.py b/matrix_appservice_kakaotalk/kt/types/packet/chat/kickout.py index 078eb41..9422d77 100644 --- a/matrix_appservice_kakaotalk/kt/types/packet/chat/kickout.py +++ b/matrix_appservice_kakaotalk/kt/types/packet/chat/kickout.py @@ -21,7 +21,6 @@ from attr import dataclass from mautrix.types import SerializableAttrs -@dataclass class KnownKickoutType(IntEnum): CHANGE_SERVER = -2 LOGIN_ANOTHER = 0 diff --git a/matrix_appservice_kakaotalk/portal.py b/matrix_appservice_kakaotalk/portal.py index d66553b..075c562 100644 --- a/matrix_appservice_kakaotalk/portal.py +++ b/matrix_appservice_kakaotalk/portal.py @@ -21,8 +21,10 @@ from typing import ( AsyncGenerator, Awaitable, Callable, - NamedTuple, + Coroutine, + Generic, Pattern, + TypeVar, cast, ) from io import BytesIO @@ -31,6 +33,8 @@ import asyncio import re import time +from attr import dataclass + from mautrix.appservice import IntentAPI from mautrix.bridge import BasePortal, NotificationDisabler, async_getter_lock from mautrix.errors import MatrixError, MForbidden, MNotFound, SessionNotFound @@ -125,11 +129,15 @@ class FakeLock: pass -class StateEventHandler(NamedTuple): - # TODO Can this use Generic to force the two StateEventContent parameters to be of the same type? - # Or, just have a single StateEvent parameter - apply: Callable[[u.User, StateEventContent, StateEventContent], Awaitable[None]] - revert: Callable[[StateEventContent], Awaitable[None]] +T = TypeVar("T") +ACallable = Coroutine[None, None, T] + +StateEventHandlerContentType = TypeVar("StateEventHandlerContentType", bound=StateEventContent) + +@dataclass +class StateEventHandler(Generic[StateEventHandlerContentType]): + apply: Callable[[Portal, u.User, StateEventHandlerContentType, StateEventHandlerContentType], ACallable[None]] + revert: Callable[[Portal, StateEventHandlerContentType], ACallable[None]] action_name: str @@ -155,6 +163,9 @@ class Portal(DBPortal, BasePortal): _scheduled_resync: asyncio.Task | None _resync_targets: dict[int, p.Puppet] + _CHAT_TYPE_HANDLER_MAP: dict[ChatType, Callable[..., ACallable[list[EventID]]]] + _STATE_EVENT_HANDLER_MAP: dict[EventType, StateEventHandler] + def __init__( self, ktid: Long, @@ -226,7 +237,7 @@ class Portal(DBPortal, BasePortal): 16385: cls._handle_kakaotalk_deleted, } - cls._STATE_EVENT_HANDLER_MAP: dict[EventType, StateEventHandler] = { + cls._STATE_EVENT_HANDLER_MAP = { EventType.ROOM_POWER_LEVELS: StateEventHandler( cls._handle_matrix_power_levels, cls._revert_matrix_power_levels, @@ -506,7 +517,7 @@ class Portal(DBPortal, BasePortal): decryption_info.url = url return url, info, decryption_info - async def _update_name(self, name: str) -> bool: + async def _update_name(self, name: str | None) -> bool: if not name: self.log.warning("Got empty name in _update_name call") return False @@ -593,6 +604,8 @@ class Portal(DBPortal, BasePortal): return False if not puppet: puppet = await self.get_dm_puppet() + if not puppet: + return False changed = await self._update_name(puppet.name) changed = await self._update_photo_from_puppet(puppet) or changed return changed @@ -815,15 +828,20 @@ class Portal(DBPortal, BasePortal): async def _create_matrix_room( self, source: u.User, info: PortalChannelInfo | None = None - ) -> RoomID | None: + ) -> RoomID: if self.mxid: await self._update_matrix_room(source, info) return self.mxid self.log.debug(f"Creating Matrix room") + if self.is_direct: # NOTE Must do this to find the other member of the DM, since the channel ID != the member's ID! + if not info or not info.participantInfo: + info = await source.client.get_portal_channel_info(self.channel_props) + assert info.participantInfo await self._update_participants(source, info.participantInfo) + name: str | None = None description: str | None = None initial_state = [ @@ -843,6 +861,9 @@ class Portal(DBPortal, BasePortal): if self.is_open: preset = RoomCreatePreset.PUBLIC # TODO Find whether perms apply to any non-direct channel, or just open ones + if not info or not info.participantInfo: + info = await source.client.get_portal_channel_info(self.channel_props) + assert info.participantInfo user_power_levels = await self._get_mapped_participant_power_levels(info.participantInfo.participants) # NOTE Giving the bot a +1 power level if necessary so it can demote non-puppet admins user_power_levels[self.main_intent.mxid] = max(100, 1 + FROM_PERM_MAP[OpenChannelUserPerm.OWNER]) @@ -924,6 +945,8 @@ class Portal(DBPortal, BasePortal): await self._update_participants(source, info.participantInfo) try: + # TODO Think of better typing for this + assert info.channel_info await self.backfill(source, is_initial=True, channel_info=info.channel_info) except Exception: self.log.exception("Failed to backfill new portal") @@ -1075,10 +1098,11 @@ class Portal(DBPortal, BasePortal): mimetype = message.info.mimetype or magic.mimetype(data) filename = message.body width, height = None, None - if message.info in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO): + if message.msgtype in (MessageType.IMAGE, MessageType.STICKER, MessageType.VIDEO): width = message.info.width height = message.info.height try: + ext = guess_extension(mimetype) log_id = await sender.client.send_media( self.channel_props, TO_MSGTYPE_MAP[message.msgtype], @@ -1086,7 +1110,7 @@ class Portal(DBPortal, BasePortal): filename, width=width, height=height, - ext=guess_extension(mimetype)[1:], + ext=ext[1:] if ext else "", ) except CommandException as e: self.log.debug(f"Error uploading media for Matrix message {event_id}: {e!s}") @@ -1191,7 +1215,8 @@ class Portal(DBPortal, BasePortal): return try: effective_sender, _ = await self.get_relay_sender(sender, f"{handler.action_name} {evt.event_id}") - await handler.apply(self, effective_sender, evt.prev_content, evt.content) + if effective_sender: + await handler.apply(self, effective_sender, evt.prev_content, evt.content) except Exception as e: self.log.error( f"Failed to handle Matrix {handler.action_name} {evt.event_id}: {e}", @@ -1314,7 +1339,7 @@ class Portal(DBPortal, BasePortal): await self.save() async def _revert_matrix_room_topic(self, prev_content: RoomTopicStateEventContent) -> None: - await self.main_intent.set_room_topic(self.mxid, prev_content.topic) + await self.main_intent.set_room_topic(self.mxid, prev_content.topic or "") async def _handle_matrix_room_avatar( self, @@ -1479,7 +1504,7 @@ class Portal(DBPortal, BasePortal): chat_text: str | None, chat_type: ChatType, **_ - ) -> Awaitable[list[EventID]]: + ) -> list[EventID]: try: type_str = KnownChatType(chat_type).name.lower() except ValueError: @@ -1546,14 +1571,14 @@ class Portal(DBPortal, BasePortal): await self._add_kakaotalk_reply(content, attachment) return [await self._send_message(intent, content, timestamp=timestamp)] - def _handle_kakaotalk_photo(self, **kwargs) -> Awaitable[list[EventID]]: - return asyncio.gather(self._handle_kakaotalk_uniphoto(**kwargs)) + async def _handle_kakaotalk_photo(self, **kwargs) -> list[EventID]: + return [await self._handle_kakaotalk_uniphoto(**kwargs)] async def _handle_kakaotalk_multiphoto( self, attachment: MultiPhotoAttachment, **kwargs - ) -> Awaitable[list[EventID]]: + ) -> list[EventID]: # TODO Upload media concurrently, but post messages sequentially return [ await self._handle_kakaotalk_uniphoto( @@ -1594,12 +1619,12 @@ class Portal(DBPortal, BasePortal): **kwargs ) - def _handle_kakaotalk_video( + async def _handle_kakaotalk_video( self, attachment: VideoAttachment, **kwargs - ) -> Awaitable[list[EventID]]: - return asyncio.gather(self._handle_kakaotalk_media( + ) -> list[EventID]: + return [await self._handle_kakaotalk_media( attachment, VideoInfo( duration=attachment.d, @@ -1608,14 +1633,14 @@ class Portal(DBPortal, BasePortal): ), MessageType.VIDEO, **kwargs - )) + )] - def _handle_kakaotalk_audio( + async def _handle_kakaotalk_audio( self, attachment: AudioAttachment, **kwargs - ) -> Awaitable[list[EventID]]: - return asyncio.gather(self._handle_kakaotalk_media( + ) -> list[EventID]: + return [await self._handle_kakaotalk_media( attachment, AudioInfo( size=attachment.s, @@ -1623,11 +1648,11 @@ class Portal(DBPortal, BasePortal): ), MessageType.AUDIO, **kwargs - )) + )] async def _handle_kakaotalk_media( self, - attachment: MediaAttachment, + attachment: MediaAttachment | AudioAttachment, info: MediaInfo, msgtype: MessageType, *, @@ -1648,7 +1673,7 @@ class Portal(DBPortal, BasePortal): info.size = additional_info.size info.mimetype = additional_info.mimetype content = MediaMessageEventContent( - url=mxc, file=decryption_info, msgtype=msgtype, body=chat_text, info=info + url=mxc, file=decryption_info, msgtype=msgtype, body=chat_text or "", info=info ) return await self._send_message(intent, content, timestamp=timestamp) @@ -1787,7 +1812,7 @@ class Portal(DBPortal, BasePortal): # TODO Should this be removed? With it, can't sync an empty portal! #elif (not most_recent or not most_recent.timestamp) and not is_initial: # self.log.debug("Not backfilling %s: no most recent message found", self.ktid_log) - elif last_log_id and most_recent and int(most_recent.ktid) >= int(last_log_id): + elif last_log_id and most_recent and int(most_recent.ktid or 0) >= int(last_log_id): self.log.debug( "Not backfilling %s: last activity is equal to most recent bridged " "message (%s >= %s)", diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py index 3d93d5a..fd0e01f 100644 --- a/matrix_appservice_kakaotalk/user.py +++ b/matrix_appservice_kakaotalk/user.py @@ -89,7 +89,7 @@ class User(DBUser, BaseUser): by_mxid: dict[UserID, User] = {} by_ktid: dict[int, User] = {} - client: Client | None + _client: Client | None _notice_room_lock: asyncio.Lock _notice_send_lock: asyncio.Lock @@ -141,7 +141,7 @@ class User(DBUser, BaseUser): self._logged_in_info = None self._logged_in_info_time = 0 - self.client = None + self._client = None @classmethod def init_cls(cls, bridge: KakaoTalkBridge) -> AsyncIterable[Awaitable[bool]]: @@ -151,6 +151,12 @@ class User(DBUser, BaseUser): cls.temp_disconnect_notices = bridge.config["bridge.temporary_disconnect_notices"] return (user.reload_session(is_startup=True) async for user in cls.all_logged_in()) + @property + def client(self) -> Client: + if not self._client: + raise ValueError("User must be logged in before its client can be used") + return self._client + @property def is_connected(self) -> bool | None: return self._is_connected @@ -242,7 +248,10 @@ class User(DBUser, BaseUser): @property def oauth_credential(self) -> OAuthCredential: - assert None not in (self.ktid, self.uuid, self.access_token, self.refresh_token) + assert self.ktid is not None + assert self.uuid is not None + assert self.access_token is not None + assert self.refresh_token is not None return OAuthCredential( self.ktid, self.uuid, @@ -259,9 +268,9 @@ class User(DBUser, BaseUser): self.log.warning(f"UUID mismatch: expected {self.uuid}, got {oauth_credential.deviceUUID}") self.uuid = oauth_credential.deviceUUID - async def get_own_info(self) -> SettingsStruct: - if not self._logged_in_info or self._logged_in_info_time + 60 * 60 < time.monotonic(): - self._logged_in_info = await self.client.get_settings() + async def get_own_info(self) -> SettingsStruct | None: + if self._client and (not self._logged_in_info or self._logged_in_info_time + 60 * 60 < time.monotonic()): + self._logged_in_info = await self._client.get_settings() self._logged_in_info_time = time.monotonic() return self._logged_in_info @@ -280,7 +289,7 @@ class User(DBUser, BaseUser): user_info = await client.start() # NOTE On failure, client.start throws instead of returning something falsy self.log.info("Loaded session successfully") - self.client = client + self._client = client self._logged_in_info = user_info self._logged_in_info_time = time.monotonic() self._track_metric(METRIC_LOGGED_IN, True) @@ -304,7 +313,7 @@ class User(DBUser, BaseUser): await self.logout(remove_ktid=False) async def is_logged_in(self, _override: bool = False) -> bool: - if not self.has_state or not self.client: + if not self.has_state or not self._client: return False if self._is_logged_in is None or _override: try: @@ -360,9 +369,9 @@ class User(DBUser, BaseUser): self._is_rpc_reconnecting = False async def logout(self, *, remove_ktid: bool = True, reset_device: bool = False) -> None: - if self.client: + if self._client: # TODO Look for a logout API call - await self.client.stop() + await self._client.stop() if remove_ktid: await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT) self._track_metric(METRIC_LOGGED_IN, False) @@ -374,7 +383,7 @@ class User(DBUser, BaseUser): self._is_logged_in = False self.is_connected = None - self.client = None + self._client = None if self.ktid and remove_ktid: #await UserPortal.delete_all(self.ktid) @@ -581,12 +590,12 @@ class User(DBUser, BaseUser): state.remote_name = puppet.name async def get_bridge_states(self) -> list[BridgeState]: - if not self.state: + if not self.has_state: return [] state = BridgeState(state_event=BridgeStateEvent.UNKNOWN_ERROR) if self.is_connected: state.state_event = BridgeStateEvent.CONNECTED - elif self._is_rpc_reconnecting or self.client: + elif self._is_rpc_reconnecting or self._client: state.state_event = BridgeStateEvent.TRANSIENT_DISCONNECT return [state] @@ -660,8 +669,8 @@ class User(DBUser, BaseUser): await self.logout() await self.send_bridge_notice(f"Disconnected from KakaoTalk: {reason_str} {reason_suffix}") - def on_error(self, error: JSON) -> Awaitable[None]: - return self.send_bridge_notice( + async def on_error(self, error: JSON) -> None: + await self.send_bridge_notice( f"Got error event from KakaoTalk:\n\n> {error}", # TODO Which error code to use? #error_code="kt-connection-error", @@ -671,7 +680,7 @@ class User(DBUser, BaseUser): async def on_client_disconnect(self) -> None: self.is_connected = False self._track_metric(METRIC_CONNECTED, False) - self.client = None + self._client = None if self._is_logged_in: if self.temp_disconnect_notices: await self.send_bridge_notice( @@ -685,12 +694,12 @@ class User(DBUser, BaseUser): self.log.debug(f"Successfully logged in as {oauth_credential.userId}") self.oauth_credential = oauth_credential await self.push_bridge_state(BridgeStateEvent.CONNECTING) - self.client = Client(self, log=self.log.getChild("ktclient")) + self._client = Client(self, log=self.log.getChild("ktclient")) await self.save() self._is_logged_in = True # TODO Retry network connection failures here, or in the client (like token refreshes are)? # Should also catch unlikely authentication errors - self._logged_in_info = await self.client.start() + self._logged_in_info = await self._client.start() self._logged_in_info_time = time.monotonic() asyncio.create_task(self.post_login(is_startup=True))