Compare commits
8 Commits
5ae5970ef0
...
4e6498f777
Author | SHA1 | Date | |
---|---|---|---|
4e6498f777 | |||
c3726220de | |||
a817049e12 | |||
dcf17fd40a | |||
c3b299d26c | |||
faf174c226 | |||
dfdd98da96 | |||
b994ca65ee |
17
ROADMAP.md
17
ROADMAP.md
@ -73,11 +73,20 @@
|
||||
* [x] At startup
|
||||
* [x] When added to chat
|
||||
* [x] When receiving message
|
||||
* [ ] Private chat creation by inviting Matrix puppet of KakaoTalk user to new room
|
||||
* [x] For existing recently-updated KakaoTalk channels
|
||||
* [ ] For existing long-idled KakaoTalk channels
|
||||
* [ ] For new KakaoTalk channels
|
||||
* [x] Direct chat creation by inviting Matrix puppet of KakaoTalk user to new room
|
||||
* [x] Option to use own Matrix account for messages sent from other KakaoTalk clients
|
||||
* [ ] KakaoTalk friends list management
|
||||
* [x] List friends
|
||||
* [ ] Add friend
|
||||
* [x] By KakaoTalk ID
|
||||
* [x] By Matrix puppet of KakaoTalk user
|
||||
* [ ] By phone number
|
||||
* [x] Remove friend
|
||||
* [ ] Manage favourite friends
|
||||
* [ ] Manage hidden friends
|
||||
* [x] KakaoTalk ID management
|
||||
* [x] Set/Change ID
|
||||
* [x] Make ID searchable/hidden
|
||||
|
||||
<sup>[1]</sup> Sometimes fails with "Invalid body" error
|
||||
<sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted
|
||||
|
@ -14,12 +14,9 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from mautrix.bridge.commands import HelpSection, command_handler
|
||||
from mautrix.types import SerializerError
|
||||
|
||||
from .typehint import CommandEvent
|
||||
|
||||
from ..kt.client.errors import CommandException
|
||||
|
||||
SECTION_CONNECTION = HelpSection("Connection management", 15, "")
|
||||
|
||||
|
||||
@ -57,44 +54,20 @@ async def disconnect(evt: CommandEvent) -> None:
|
||||
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
needs_auth=False,
|
||||
management_only=True,
|
||||
help_section=SECTION_CONNECTION,
|
||||
help_text="Check if you're logged into KakaoTalk and retrieve your account information",
|
||||
)
|
||||
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}")
|
||||
return
|
||||
if own_info:
|
||||
uuid = f"`{own_info.more.uuid}` ({'' if own_info.more.uuidSearchable else 'not '}searchable)" if own_info.more.uuid else "_none_"
|
||||
await evt.reply(
|
||||
f"You're logged in as **{own_info.more.nickName}** (KakaoTalk ID: {uuid}, internal ID: `{evt.sender.ktid}`)"
|
||||
)
|
||||
else:
|
||||
await evt.reply(
|
||||
f"You're logged in, but the bridge is unable to retrieve your profile information (internal ID: {evt.sender.ktid})"
|
||||
)
|
||||
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=True,
|
||||
help_section=SECTION_CONNECTION,
|
||||
help_text="Check if you're connected to KakaoTalk chats",
|
||||
help_text="Check if you're logged in to KakaoTalk and connected to chats",
|
||||
)
|
||||
async def ping(evt: CommandEvent) -> None:
|
||||
assert evt.sender.client
|
||||
is_connected = evt.sender.is_connected and await evt.sender.client.is_connected()
|
||||
await evt.reply(
|
||||
f"You are {'connected to' if is_connected else '**disconnected** from'} KakaoTalk chats."
|
||||
)
|
||||
if not await evt.sender.is_logged_in():
|
||||
await evt.reply("You are **logged out** of KakaoTalk.")
|
||||
else:
|
||||
is_connected = evt.sender.is_connected and await evt.sender.client.is_connected()
|
||||
await evt.reply(
|
||||
"You are logged into KakaoTalk.\n\n"
|
||||
f"You are {'connected to' if is_connected else '**disconnected** from'} KakaoTalk chats."
|
||||
)
|
||||
|
||||
|
||||
@command_handler(
|
||||
|
@ -15,24 +15,143 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Awaitable
|
||||
import asyncio
|
||||
|
||||
from mautrix.bridge.commands import HelpSection, command_handler
|
||||
from mautrix.types import SerializerError
|
||||
from mautrix.util import utf16_surrogate
|
||||
from mautrix.util.formatter import (
|
||||
EntityString,
|
||||
EntityType,
|
||||
MarkdownString,
|
||||
MatrixParser,
|
||||
SimpleEntity,
|
||||
)
|
||||
|
||||
from ..kt.types.api.struct import ApiUserType
|
||||
from ..rpc.types import RPCError
|
||||
|
||||
from .. import puppet as pu, user as u
|
||||
from .typehint import CommandEvent
|
||||
|
||||
from ..kt.client.errors import CommandException
|
||||
|
||||
SECTION_ACCOUNT = HelpSection("Account management", 35, "")
|
||||
SECTION_FRIENDS = HelpSection("Friends management", 40, "")
|
||||
SECTION_CHANNELS = HelpSection("Channel management", 45, "")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..kt.types.api.struct import FriendStruct
|
||||
from mautrix.types import UserID
|
||||
|
||||
from ..kt.types.api.struct import FriendStruct
|
||||
from ..kt.types.bson import Long
|
||||
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=True,
|
||||
help_section=SECTION_ACCOUNT,
|
||||
help_text="Retrieve your KakaoTalk account information",
|
||||
)
|
||||
async def whoami(evt: CommandEvent) -> None:
|
||||
await evt.mark_read()
|
||||
try:
|
||||
own_info = await evt.sender.get_own_info(force=True)
|
||||
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}")
|
||||
return
|
||||
if own_info:
|
||||
uuid = f"`{own_info.more.uuid}` ({'searchable' if own_info.more.uuidSearchable else 'hidden'})" if own_info.more.uuid else "_none_"
|
||||
await evt.reply(
|
||||
f"You're logged in as **{own_info.more.nickName}** (KakaoTalk ID: {uuid}, internal ID: `{evt.sender.ktid}`)"
|
||||
)
|
||||
else:
|
||||
await evt.reply(
|
||||
f"You're logged in, but the bridge is unable to retrieve your profile information (internal ID: {evt.sender.ktid})"
|
||||
)
|
||||
|
||||
|
||||
_CMD_CONFIRM_CHANGE_ID = "confirm-change-id"
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=False,
|
||||
help_section=SECTION_ACCOUNT,
|
||||
help_text="Set or change your KakaoTalk ID",
|
||||
help_args="<_KakaoTalk ID_>",
|
||||
aliases=["set-id"],
|
||||
)
|
||||
async def change_id(evt: CommandEvent) -> None:
|
||||
if len(evt.args) != 1:
|
||||
await evt.reply(f"**Usage:** `$cmdprefix+sp {evt.command} <KakaoTalk ID>`")
|
||||
return
|
||||
new_id = evt.args[0]
|
||||
if len(new_id) > 20:
|
||||
await evt.reply("ID must not exceed 20 characters. Please choose a shorter ID.")
|
||||
return
|
||||
await evt.mark_read()
|
||||
if await evt.sender.client.can_change_uuid(new_id):
|
||||
await evt.reply(
|
||||
"Once set, your KakaoTalk ID can be changed only once! "
|
||||
f"If you are sure that you want to change it, type `$cmdprefix+sp {_CMD_CONFIRM_CHANGE_ID}`. "
|
||||
"Otherwise, type `$cmdprefix+sp cancel`."
|
||||
)
|
||||
evt.sender.command_status = {
|
||||
"action": "ID change",
|
||||
"room_id": evt.room_id,
|
||||
"next": _confirm_change_id,
|
||||
"new_id": new_id,
|
||||
}
|
||||
else:
|
||||
await evt.reply(
|
||||
f"Cannot change KakaoTalk ID to `{new_id}`. "
|
||||
"That ID might already be in use or have restricted characters. "
|
||||
"Either try a different ID, or try again later."
|
||||
)
|
||||
|
||||
async def _confirm_change_id(evt: CommandEvent) -> None:
|
||||
assert evt.sender.command_status
|
||||
new_id = evt.sender.command_status.pop("new_id")
|
||||
evt.sender.command_status = None
|
||||
await evt.mark_read()
|
||||
try:
|
||||
await evt.sender.client.change_uuid(new_id)
|
||||
except CommandException:
|
||||
await evt.reply(f"Failed to change KakaoTalk ID to `{new_id}`. Try again later.")
|
||||
else:
|
||||
await evt.reply(f"Successfully changed ID to `{new_id}`")
|
||||
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=False,
|
||||
help_section=SECTION_ACCOUNT,
|
||||
help_text="Allow others to search by your KakaoTalk ID",
|
||||
)
|
||||
def make_id_searchable(evt: CommandEvent) -> Awaitable[None]:
|
||||
return _set_id_searchable(evt, True)
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=False,
|
||||
help_section=SECTION_ACCOUNT,
|
||||
help_text="Prevent others from searching by your KakaoTalk ID",
|
||||
)
|
||||
def make_id_hidden(evt: CommandEvent) -> Awaitable[None]:
|
||||
return _set_id_searchable(evt, False)
|
||||
|
||||
async def _set_id_searchable(evt: CommandEvent, searchable: bool) -> None:
|
||||
await evt.mark_read()
|
||||
try:
|
||||
await evt.sender.client.set_uuid_searchable(searchable)
|
||||
except RPCError as e:
|
||||
await evt.reply(str(e))
|
||||
else:
|
||||
await evt.reply(f"Successfully made KakaoTalk ID {'searchable' if searchable else 'hidden'}")
|
||||
|
||||
async def _get_search_result_puppet(source: u.User, friend_struct: FriendStruct) -> pu.Puppet:
|
||||
puppet = await pu.Puppet.get_by_ktid(friend_struct.userId)
|
||||
@ -48,9 +167,9 @@ async def _get_search_result_puppet(source: u.User, friend_struct: FriendStruct)
|
||||
help_text="List all KakaoTalk friends",
|
||||
)
|
||||
async def list_friends(evt: CommandEvent) -> None:
|
||||
await evt.mark_read()
|
||||
try:
|
||||
resp = await evt.sender.client.list_friends()
|
||||
await evt.mark_read()
|
||||
except CommandException as e:
|
||||
await evt.reply(f"Error while listing friends: {e!s}")
|
||||
return
|
||||
@ -70,6 +189,114 @@ async def list_friends(evt: CommandEvent) -> None:
|
||||
await evt.reply("No friends found.")
|
||||
|
||||
|
||||
class MentionFormatString(EntityString[SimpleEntity, EntityType], MarkdownString):
|
||||
def format(self, entity_type: EntityType, **kwargs) -> MentionFormatString:
|
||||
if entity_type == EntityType.USER_MENTION:
|
||||
self.entities.append(
|
||||
SimpleEntity(
|
||||
type=entity_type,
|
||||
offset=0,
|
||||
length=len(self.text),
|
||||
extra_info={"user_id": kwargs["user_id"]},
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
class MentionParser(MatrixParser[MentionFormatString]):
|
||||
fs = MentionFormatString
|
||||
|
||||
async def _get_id_from_mxid(mxid: UserID) -> Long | None:
|
||||
user = await u.User.get_by_mxid(mxid, create=False)
|
||||
if user and user.ktid:
|
||||
return user.ktid
|
||||
puppet = await pu.Puppet.get_by_mxid(mxid, create=False)
|
||||
return puppet.ktid if puppet else None
|
||||
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=False,
|
||||
help_section=SECTION_FRIENDS,
|
||||
help_text="Add a KakaoTalk user to your KakaoTalk friends list",
|
||||
help_args="<_KakaoTalk ID_|_Matrix user ID_>",
|
||||
)
|
||||
def add_friend(evt: CommandEvent) -> Awaitable[None]:
|
||||
return _edit_friend(evt, True)
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=False,
|
||||
help_section=SECTION_FRIENDS,
|
||||
help_text="Remove a KakaoTalk user from your KakaoTalk friends list",
|
||||
help_args="<_KakaoTalk ID_|_Matrix user ID_>",
|
||||
)
|
||||
def remove_friend(evt: CommandEvent) -> Awaitable[None]:
|
||||
return _edit_friend(evt, False)
|
||||
|
||||
async def _edit_friend(evt: CommandEvent, add: bool) -> None:
|
||||
if not evt.args:
|
||||
await evt.reply(f"**Usage:** `$cmdprefix+sp {evt.command} <KakaoTalk ID|Matrix user ID>`")
|
||||
return
|
||||
formatted_body = evt.content.get("formatted_body")
|
||||
if formatted_body:
|
||||
arg = formatted_body[len(evt.command):].strip()
|
||||
parsed = await MentionParser().parse(utf16_surrogate.add(arg))
|
||||
if not parsed.entities:
|
||||
await evt.reply("No user found")
|
||||
return
|
||||
if (
|
||||
len(parsed.entities) > 1 or
|
||||
parsed.entities[0].offset != 0 or
|
||||
parsed.entities[0].length != len(utf16_surrogate.remove(parsed.text))
|
||||
):
|
||||
await evt.reply("Can add only one friend at a time")
|
||||
return
|
||||
mxid = parsed.entities[0].extra_info["user_id"]
|
||||
ktid = await _get_id_from_mxid(mxid)
|
||||
if not ktid:
|
||||
await evt.reply("No KakaoTalk user found for this Matrix ID")
|
||||
else:
|
||||
await _edit_friend_by_ktid(evt, ktid, add)
|
||||
else:
|
||||
arg = evt.content.body[len(evt.command):].strip()
|
||||
ktid = await _get_id_from_mxid(arg)
|
||||
if ktid:
|
||||
await _edit_friend_by_ktid(evt, ktid, add)
|
||||
else:
|
||||
await _edit_friend_by_uuid(evt, arg, add)
|
||||
|
||||
async def _edit_friend_by_ktid(evt: CommandEvent, ktid: Long, add: bool) -> None:
|
||||
await evt.mark_read()
|
||||
try:
|
||||
friend_struct = await evt.sender.client.edit_friend(ktid, add)
|
||||
except RPCError as e:
|
||||
await evt.reply(str(e))
|
||||
else:
|
||||
await _on_friend_edited(evt, friend_struct, add)
|
||||
|
||||
async def _edit_friend_by_uuid(evt: CommandEvent, uuid: str, add: bool) -> None:
|
||||
await evt.mark_read()
|
||||
try:
|
||||
friend_struct = await evt.sender.client.edit_friend_by_uuid(uuid, add)
|
||||
except RPCError as e:
|
||||
await evt.reply(str(e))
|
||||
except CommandException as e:
|
||||
if e.status == -1002:
|
||||
await evt.reply(
|
||||
f"Failed to {'add' if add else 'remove'} friend. Ensure their ID is spelled correctly."
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
await _on_friend_edited(evt, friend_struct, add)
|
||||
|
||||
async def _on_friend_edited(evt: CommandEvent, friend_struct: FriendStruct | None, add: bool):
|
||||
await evt.reply(f"Friend {'added' if add else 'removed'}")
|
||||
if friend_struct:
|
||||
puppet = await pu.Puppet.get_by_ktid(friend_struct.userId)
|
||||
await puppet.update_info_from_friend(evt.sender, friend_struct)
|
||||
|
||||
|
||||
@command_handler(
|
||||
needs_auth=True,
|
||||
management_only=False,
|
||||
|
@ -324,12 +324,52 @@ class Client:
|
||||
unread_chat_ids=[c.serialize() for c in unread_chat_ids],
|
||||
)
|
||||
|
||||
async def can_change_uuid(self, uuid: str) -> bool:
|
||||
try:
|
||||
await self._api_user_request_void("can_change_uuid", uuid=uuid)
|
||||
except CommandException:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def change_uuid(self, uuid: str) -> Awaitable[None]:
|
||||
return self._api_user_request_void("change_uuid", uuid=uuid)
|
||||
|
||||
def set_uuid_searchable(self, searchable: bool) -> Awaitable[None]:
|
||||
return self._api_user_request_void("set_uuid_searchable", searchable=searchable)
|
||||
|
||||
def list_friends(self) -> Awaitable[FriendListStruct]:
|
||||
return self._api_user_request_result(
|
||||
FriendListStruct,
|
||||
"list_friends",
|
||||
)
|
||||
|
||||
async def edit_friend(self, ktid: Long, add: bool) -> FriendStruct | None:
|
||||
try:
|
||||
friend_req_struct = await self._api_user_request_result(
|
||||
FriendReqStruct,
|
||||
"edit_friend",
|
||||
user_id=ktid.serialize(),
|
||||
add=add,
|
||||
)
|
||||
return friend_req_struct.friend
|
||||
except SerializerError:
|
||||
self.log.exception("Unable to deserialize friend struct, but friend should have been edited nonetheless")
|
||||
return None
|
||||
|
||||
async def edit_friend_by_uuid(self, uuid: str, add: bool) -> FriendStruct | None:
|
||||
try:
|
||||
friend_req_struct = await self._api_user_request_result(
|
||||
FriendReqStruct,
|
||||
"edit_friend_by_uuid",
|
||||
uuid=uuid,
|
||||
add=add,
|
||||
)
|
||||
return friend_req_struct.friend
|
||||
except SerializerError:
|
||||
self.log.exception("Unable to deserialize friend struct, but friend should have been edited nonetheless")
|
||||
return None
|
||||
|
||||
async def get_friend_dm_id(self, friend_id: Long) -> Long | None:
|
||||
try:
|
||||
return await self._api_user_request_result(
|
||||
@ -465,6 +505,13 @@ class Client:
|
||||
photo_url=photo_url,
|
||||
)
|
||||
|
||||
def create_direct_chat(self, ktid: Long) -> Awaitable[Long]:
|
||||
return self._api_user_request_result(
|
||||
Long,
|
||||
"create_direct_chat",
|
||||
user_id=ktid.serialize(),
|
||||
)
|
||||
|
||||
def leave_channel(
|
||||
self,
|
||||
channel_props: ChannelProps,
|
||||
|
@ -28,7 +28,7 @@ class CommandException(Exception):
|
||||
In the case that different status codes map to the same error subclass, the status code
|
||||
can be retrieved from the "status" property.
|
||||
"""
|
||||
# NOTE unsuccessful responses do not set a result, hence using RootCommandResult here
|
||||
# TODO Handle result object
|
||||
# TODO Print _unrecognized?
|
||||
self.status = result.status
|
||||
self.message = _status_code_message_map.get(self.status, self._default_message)
|
||||
|
@ -13,7 +13,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from typing import NewType, Union
|
||||
from typing import NewType, Optional, Union
|
||||
|
||||
from attr import dataclass
|
||||
|
||||
@ -43,13 +43,13 @@ class LoginResult(SerializableAttrs):
|
||||
"""Return value of TalkClient.login"""
|
||||
channelList: list[LoginDataItem]
|
||||
userId: Long
|
||||
lastChannelId: Long
|
||||
lastTokenId: Long
|
||||
mcmRevision: int
|
||||
removedChannelIdList: list[Long]
|
||||
revision: int
|
||||
revisionInfo: str
|
||||
minLogId: Long
|
||||
lastChannelId: Optional[Long] = None # NEW Made optional
|
||||
# TODO Consider catching SerializerError for channelList entries
|
||||
|
||||
|
||||
|
@ -93,6 +93,7 @@ class RootCommandResult(ResponseState):
|
||||
ResultType = TypeVar("ResultType", bound=Serializable)
|
||||
|
||||
def ResultListType(result_type: Type[ResultType]):
|
||||
"""Custom type for setting a result to a list of serializable objects."""
|
||||
class _ResultListType(list[result_type], Serializable):
|
||||
def serialize(self) -> list[JSON]:
|
||||
return [v.serialize() for v in self]
|
||||
@ -104,6 +105,7 @@ def ResultListType(result_type: Type[ResultType]):
|
||||
return _ResultListType
|
||||
|
||||
def ResultRawType(result_type: Type):
|
||||
"""Custom type for setting a result to a primitive."""
|
||||
class _ResultRawType(result_type, Serializable):
|
||||
def serialize(self) -> result_type:
|
||||
return self
|
||||
@ -132,6 +134,7 @@ def deserialize_result(
|
||||
) -> Union[CommandResultDoneValue[ResultType], RootCommandResult]:
|
||||
"""Returns equivalent of CommandResult<T>. Does no consistency checking on success & result properties."""
|
||||
if "result" in data and data.get("success"):
|
||||
# TODO Handle result object in unsuccessful response
|
||||
# TODO Allow arbitrary result object?
|
||||
return CommandResultDoneValue.deserialize_result(result_type, data)
|
||||
else:
|
||||
|
@ -30,7 +30,7 @@ from mautrix.types import (
|
||||
UserID,
|
||||
)
|
||||
|
||||
from . import portal as po, user as u
|
||||
from . import portal as po, puppet as pu, user as u
|
||||
from .db import Message as DBMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -63,6 +63,19 @@ class MatrixHandler(BaseMatrixHandler):
|
||||
room_id, "This room has been marked as your KakaoTalk bridge notice room."
|
||||
)
|
||||
|
||||
async def handle_puppet_dm_invite(
|
||||
self, room_id: RoomID, puppet: pu.Puppet, invited_by: u.User, evt: StateEvent
|
||||
) -> None:
|
||||
# TODO Make upstream request to return custom failure message,
|
||||
# instead of having to reimplement the entire function
|
||||
portal = await invited_by.get_portal_with(puppet)
|
||||
if portal:
|
||||
await portal.accept_matrix_dm(room_id, invited_by, puppet)
|
||||
else:
|
||||
await puppet.default_mxid_intent.leave_room(
|
||||
room_id, reason="Unable to create a DM for this KakaoTalk user"
|
||||
)
|
||||
|
||||
async def handle_invite(
|
||||
self, room_id: RoomID, user_id: UserID, invited_by: u.User, event_id: EventID
|
||||
) -> None:
|
||||
|
@ -278,8 +278,8 @@ 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 | None:
|
||||
if self._client and (not self._logged_in_info or self._logged_in_info_time + 60 * 60 < time.monotonic()):
|
||||
async def get_own_info(self, *, force: bool = False) -> SettingsStruct | None:
|
||||
if force or 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
|
||||
@ -471,7 +471,7 @@ class User(DBUser, BaseUser):
|
||||
if self.config["bridge.sync_on_startup"] or not is_startup:
|
||||
sync_count = self.config["bridge.initial_chat_sync"]
|
||||
else:
|
||||
sync_count = None
|
||||
sync_count = 0
|
||||
await self.connect_and_sync(sync_count, force_sync=False)
|
||||
else:
|
||||
await self.send_bridge_notice(
|
||||
@ -685,14 +685,18 @@ class User(DBUser, BaseUser):
|
||||
else:
|
||||
ktid = memo_ids[0]
|
||||
if len(memo_ids) > 1:
|
||||
self.log.info("Found multiple memo chats, so using the first one as a fallback")
|
||||
self.log.warning("Found multiple memo chats, so using the first one as a fallback")
|
||||
if ktid:
|
||||
return await po.Portal.get_by_ktid(
|
||||
ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type
|
||||
)
|
||||
self.log.debug(f"Found existing direct chat with KakaoTalk user {puppet.ktid}")
|
||||
else:
|
||||
self.log.warning(f"Didn't find an existing DM channel with KakaoTalk user {puppet.ktid}, so not creating one")
|
||||
return None
|
||||
self.log.debug(f"Didn't find an existing direct chat with KakaoTalk user {puppet.ktid}, so will create one")
|
||||
try:
|
||||
ktid = await self.client.create_direct_chat(puppet.ktid)
|
||||
except:
|
||||
self.log.exception(f"Failed to create direct chat with {puppet.ktid}")
|
||||
return await po.Portal.get_by_ktid(
|
||||
ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type
|
||||
) if ktid else None
|
||||
|
||||
# region KakaoTalk event handling
|
||||
|
||||
|
@ -834,6 +834,51 @@ export default class PeerClient {
|
||||
return makeCommandResult(receipts)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
* @param {string} req.uuid
|
||||
*/
|
||||
canChangeUUID = async (req) => {
|
||||
return await this.#getUser(req.mxid).serviceClient.canChangeUUID(req.uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
* @param {string} req.uuid
|
||||
*/
|
||||
changeUUID = async (req) => {
|
||||
const serviceClient = this.#getUser(req.mxid).serviceClient
|
||||
|
||||
const checkRes = await serviceClient.canChangeUUID(req.uuid)
|
||||
if (!checkRes.success) return checkRes
|
||||
|
||||
return await serviceClient.changeUUID(req.uuid)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
* @param {boolean} req.searchable
|
||||
*/
|
||||
setUUIDSearchable = async (req) => {
|
||||
const serviceClient = this.#getUser(req.mxid).serviceClient
|
||||
const moreRes = await serviceClient.requestMoreSettings()
|
||||
if (!moreRes.success) {
|
||||
throw new ProtocolError("Error checking status of KakaoTalk ID")
|
||||
}
|
||||
if (!moreRes.result.uuid) {
|
||||
throw new ProtocolError("You do not yet have a KakaoTalk ID")
|
||||
}
|
||||
if (req.searchable == moreRes.result.uuidSearchable) {
|
||||
throw new ProtocolError(`Your KakaoTalk ID is already ${req.searchable ? "searchable" : "hidden"}`)
|
||||
}
|
||||
return await serviceClient.updateSettings({
|
||||
uuid_searchable: req.searchable,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
@ -842,6 +887,57 @@ export default class PeerClient {
|
||||
return await this.#getUser(req.mxid).serviceClient.requestFriendList()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
* @param {Long} req.user_id
|
||||
* @param {boolean} req.add
|
||||
*/
|
||||
editFriend = async (req) => {
|
||||
return await this.#editFriend(
|
||||
this.#getUser(req.mxid).serviceClient,
|
||||
req.user_id,
|
||||
req.add
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
* @param {string} req.uuid
|
||||
* @param {boolean} req.add
|
||||
*/
|
||||
editFriendByUUID = async (req) => {
|
||||
const serviceClient = this.#getUser(req.mxid).serviceClient
|
||||
|
||||
const res = await serviceClient.findFriendByUUID(req.uuid)
|
||||
if (!res.success) return res
|
||||
|
||||
return await this.#editFriend(
|
||||
serviceClient,
|
||||
res.result.member.userId instanceof Long
|
||||
? res.result.member.userId
|
||||
: Long.fromNumber(res.result.member.userId),
|
||||
req.add
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ServiceApiClient} serviceClient
|
||||
* @param {Long} id
|
||||
* @param {boolean} add
|
||||
*/
|
||||
async #editFriend(serviceClient, id, add) {
|
||||
const listRes = await serviceClient.requestFriendList()
|
||||
if (listRes.success) {
|
||||
const isFriend = -1 != listRes.result.friends.findIndex(friend => id.equals(friend.userId))
|
||||
if (isFriend == add) {
|
||||
throw new ProtocolError(`User is already ${add ? "in" : "absent from"} friends list`)
|
||||
}
|
||||
}
|
||||
return add ? await serviceClient.addFriend(id) : await serviceClient.removeFriend(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid The user whose friend is being looked up.
|
||||
@ -863,6 +959,7 @@ export default class PeerClient {
|
||||
/** @type Long[] */
|
||||
const channelIds = []
|
||||
const channelList = this.#getUser(req.mxid).talkClient.channelList
|
||||
// TODO channelList.all() doesn't really return *all* channels...
|
||||
for (const channel of channelList.all()) {
|
||||
if (channel.info.type == "MemoChat") {
|
||||
channelIds.push(channel.channelId)
|
||||
@ -1032,6 +1129,20 @@ export default class PeerClient {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
* @param {Long} req.user_id
|
||||
*/
|
||||
createDirectChat = async (req) => {
|
||||
const channelList = this.#getUser(req.mxid).talkClient.channelList.normal
|
||||
const res = await channelList.createChannel({
|
||||
userList: [{ userId: req.user_id }],
|
||||
})
|
||||
if (!res.success) return res
|
||||
return makeCommandResult(res.result.channelId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} req
|
||||
* @param {string} req.mxid
|
||||
@ -1123,7 +1234,12 @@ export default class PeerClient {
|
||||
get_participants: this.getParticipants,
|
||||
get_chats: this.getChats,
|
||||
get_read_receipts: this.getReadReceipts,
|
||||
can_change_uuid: this.canChangeUUID,
|
||||
change_uuid: this.changeUUID,
|
||||
set_uuid_searchable: this.setUUIDSearchable,
|
||||
list_friends: this.listFriends,
|
||||
edit_friend: this.editFriend,
|
||||
edit_friend_by_uuid: this.editFriendByUUID,
|
||||
get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"),
|
||||
get_memo_ids: this.getMemoIds,
|
||||
download_file: this.downloadFile,
|
||||
@ -1135,6 +1251,7 @@ export default class PeerClient {
|
||||
set_channel_name: this.setChannelName,
|
||||
set_channel_description: this.setChannelDescription,
|
||||
set_channel_photo: this.setChannelPhoto,
|
||||
create_direct_chat: this.createDirectChat,
|
||||
leave_channel: this.leaveChannel,
|
||||
}[req.command] || this.handleUnknownCommand
|
||||
}
|
||||
@ -1152,11 +1269,15 @@ export default class PeerClient {
|
||||
}
|
||||
} else {
|
||||
resp.command = "error"
|
||||
resp.error = err instanceof ProtocolError ? err.message : err.toString()
|
||||
this.log(`Error handling request ${resp.id} ${err.stack}`)
|
||||
// TODO Check if session is broken. If it is, close the PeerClient
|
||||
if (err instanceof ProtocolError) {
|
||||
resp.error = err.message
|
||||
} else {
|
||||
resp.error = err.toString()
|
||||
this.log(`Error handling request ${resp.id} ${err.stack}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO Check if session is broken. If it is, close the PeerClient
|
||||
await this.write(resp)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user