Compare commits

..

No commits in common. "4e6498f7775ea700373857d50d7af9c0559d8acb" and "5ae5970ef00ec8e3faaadc7f91c075cd348c8d99" have entirely different histories.

10 changed files with 59 additions and 456 deletions

View File

@ -73,20 +73,11 @@
* [x] At startup * [x] At startup
* [x] When added to chat * [x] When added to chat
* [x] When receiving message * [x] When receiving message
* [x] Direct chat creation by inviting Matrix puppet of KakaoTalk user to new room * [ ] 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] Option to use own Matrix account for messages sent from other KakaoTalk clients * [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>[1]</sup> Sometimes fails with "Invalid body" error
<sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted <sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted

View File

@ -14,9 +14,12 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.bridge.commands import HelpSection, command_handler from mautrix.bridge.commands import HelpSection, command_handler
from mautrix.types import SerializerError
from .typehint import CommandEvent from .typehint import CommandEvent
from ..kt.client.errors import CommandException
SECTION_CONNECTION = HelpSection("Connection management", 15, "") SECTION_CONNECTION = HelpSection("Connection management", 15, "")
@ -54,18 +57,42 @@ async def disconnect(evt: CommandEvent) -> None:
@command_handler( @command_handler(
needs_auth=False, needs_auth=True,
management_only=True, management_only=True,
help_section=SECTION_CONNECTION, help_section=SECTION_CONNECTION,
help_text="Check if you're logged in to KakaoTalk and connected to chats", 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",
) )
async def ping(evt: CommandEvent) -> None: async def ping(evt: CommandEvent) -> None:
if not await evt.sender.is_logged_in(): assert evt.sender.client
await evt.reply("You are **logged out** of KakaoTalk.")
else:
is_connected = evt.sender.is_connected and await evt.sender.client.is_connected() is_connected = evt.sender.is_connected and await evt.sender.client.is_connected()
await evt.reply( await evt.reply(
"You are logged into KakaoTalk.\n\n"
f"You are {'connected to' if is_connected else '**disconnected** from'} KakaoTalk chats." f"You are {'connected to' if is_connected else '**disconnected** from'} KakaoTalk chats."
) )

View File

@ -15,144 +15,25 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Awaitable from typing import TYPE_CHECKING
import asyncio import asyncio
from mautrix.bridge.commands import HelpSection, command_handler 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 ..kt.types.api.struct import ApiUserType
from ..rpc.types import RPCError
from .. import puppet as pu, user as u from .. import puppet as pu, user as u
from .typehint import CommandEvent from .typehint import CommandEvent
from ..kt.client.errors import CommandException from ..kt.client.errors import CommandException
SECTION_ACCOUNT = HelpSection("Account management", 35, "")
SECTION_FRIENDS = HelpSection("Friends management", 40, "") SECTION_FRIENDS = HelpSection("Friends management", 40, "")
SECTION_CHANNELS = HelpSection("Channel management", 45, "") SECTION_CHANNELS = HelpSection("Channel management", 45, "")
if TYPE_CHECKING: if TYPE_CHECKING:
from mautrix.types import UserID
from ..kt.types.api.struct import FriendStruct 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: async def _get_search_result_puppet(source: u.User, friend_struct: FriendStruct) -> pu.Puppet:
puppet = await pu.Puppet.get_by_ktid(friend_struct.userId) puppet = await pu.Puppet.get_by_ktid(friend_struct.userId)
if not puppet.name_set: if not puppet.name_set:
@ -167,9 +48,9 @@ async def _get_search_result_puppet(source: u.User, friend_struct: FriendStruct)
help_text="List all KakaoTalk friends", help_text="List all KakaoTalk friends",
) )
async def list_friends(evt: CommandEvent) -> None: async def list_friends(evt: CommandEvent) -> None:
await evt.mark_read()
try: try:
resp = await evt.sender.client.list_friends() resp = await evt.sender.client.list_friends()
await evt.mark_read()
except CommandException as e: except CommandException as e:
await evt.reply(f"Error while listing friends: {e!s}") await evt.reply(f"Error while listing friends: {e!s}")
return return
@ -189,114 +70,6 @@ async def list_friends(evt: CommandEvent) -> None:
await evt.reply("No friends found.") 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( @command_handler(
needs_auth=True, needs_auth=True,
management_only=False, management_only=False,

View File

@ -324,52 +324,12 @@ class Client:
unread_chat_ids=[c.serialize() for c in unread_chat_ids], 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]: def list_friends(self) -> Awaitable[FriendListStruct]:
return self._api_user_request_result( return self._api_user_request_result(
FriendListStruct, FriendListStruct,
"list_friends", "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: async def get_friend_dm_id(self, friend_id: Long) -> Long | None:
try: try:
return await self._api_user_request_result( return await self._api_user_request_result(
@ -505,13 +465,6 @@ class Client:
photo_url=photo_url, 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( def leave_channel(
self, self,
channel_props: ChannelProps, channel_props: ChannelProps,

View File

@ -28,7 +28,7 @@ class CommandException(Exception):
In the case that different status codes map to the same error subclass, the status code In the case that different status codes map to the same error subclass, the status code
can be retrieved from the "status" property. can be retrieved from the "status" property.
""" """
# TODO Handle result object # NOTE unsuccessful responses do not set a result, hence using RootCommandResult here
# TODO Print _unrecognized? # TODO Print _unrecognized?
self.status = result.status self.status = result.status
self.message = _status_code_message_map.get(self.status, self._default_message) self.message = _status_code_message_map.get(self.status, self._default_message)

View File

@ -13,7 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import NewType, Optional, Union from typing import NewType, Union
from attr import dataclass from attr import dataclass
@ -43,13 +43,13 @@ class LoginResult(SerializableAttrs):
"""Return value of TalkClient.login""" """Return value of TalkClient.login"""
channelList: list[LoginDataItem] channelList: list[LoginDataItem]
userId: Long userId: Long
lastChannelId: Long
lastTokenId: Long lastTokenId: Long
mcmRevision: int mcmRevision: int
removedChannelIdList: list[Long] removedChannelIdList: list[Long]
revision: int revision: int
revisionInfo: str revisionInfo: str
minLogId: Long minLogId: Long
lastChannelId: Optional[Long] = None # NEW Made optional
# TODO Consider catching SerializerError for channelList entries # TODO Consider catching SerializerError for channelList entries

View File

@ -93,7 +93,6 @@ class RootCommandResult(ResponseState):
ResultType = TypeVar("ResultType", bound=Serializable) ResultType = TypeVar("ResultType", bound=Serializable)
def ResultListType(result_type: Type[ResultType]): def ResultListType(result_type: Type[ResultType]):
"""Custom type for setting a result to a list of serializable objects."""
class _ResultListType(list[result_type], Serializable): class _ResultListType(list[result_type], Serializable):
def serialize(self) -> list[JSON]: def serialize(self) -> list[JSON]:
return [v.serialize() for v in self] return [v.serialize() for v in self]
@ -105,7 +104,6 @@ def ResultListType(result_type: Type[ResultType]):
return _ResultListType return _ResultListType
def ResultRawType(result_type: Type): def ResultRawType(result_type: Type):
"""Custom type for setting a result to a primitive."""
class _ResultRawType(result_type, Serializable): class _ResultRawType(result_type, Serializable):
def serialize(self) -> result_type: def serialize(self) -> result_type:
return self return self
@ -134,7 +132,6 @@ def deserialize_result(
) -> Union[CommandResultDoneValue[ResultType], RootCommandResult]: ) -> Union[CommandResultDoneValue[ResultType], RootCommandResult]:
"""Returns equivalent of CommandResult<T>. Does no consistency checking on success & result properties.""" """Returns equivalent of CommandResult<T>. Does no consistency checking on success & result properties."""
if "result" in data and data.get("success"): if "result" in data and data.get("success"):
# TODO Handle result object in unsuccessful response
# TODO Allow arbitrary result object? # TODO Allow arbitrary result object?
return CommandResultDoneValue.deserialize_result(result_type, data) return CommandResultDoneValue.deserialize_result(result_type, data)
else: else:

View File

@ -30,7 +30,7 @@ from mautrix.types import (
UserID, UserID,
) )
from . import portal as po, puppet as pu, user as u from . import portal as po, user as u
from .db import Message as DBMessage from .db import Message as DBMessage
if TYPE_CHECKING: if TYPE_CHECKING:
@ -63,19 +63,6 @@ class MatrixHandler(BaseMatrixHandler):
room_id, "This room has been marked as your KakaoTalk bridge notice room." 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( async def handle_invite(
self, room_id: RoomID, user_id: UserID, invited_by: u.User, event_id: EventID self, room_id: RoomID, user_id: UserID, invited_by: u.User, event_id: EventID
) -> None: ) -> None:

View File

@ -278,8 +278,8 @@ class User(DBUser, BaseUser):
self.log.warning(f"UUID mismatch: expected {self.uuid}, got {oauth_credential.deviceUUID}") self.log.warning(f"UUID mismatch: expected {self.uuid}, got {oauth_credential.deviceUUID}")
self.uuid = oauth_credential.deviceUUID self.uuid = oauth_credential.deviceUUID
async def get_own_info(self, *, force: bool = False) -> SettingsStruct | None: async def get_own_info(self) -> SettingsStruct | None:
if force or self._client and (not self._logged_in_info or self._logged_in_info_time + 60 * 60 < time.monotonic()): 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 = await self._client.get_settings()
self._logged_in_info_time = time.monotonic() self._logged_in_info_time = time.monotonic()
return self._logged_in_info return self._logged_in_info
@ -471,7 +471,7 @@ class User(DBUser, BaseUser):
if self.config["bridge.sync_on_startup"] or not is_startup: if self.config["bridge.sync_on_startup"] or not is_startup:
sync_count = self.config["bridge.initial_chat_sync"] sync_count = self.config["bridge.initial_chat_sync"]
else: else:
sync_count = 0 sync_count = None
await self.connect_and_sync(sync_count, force_sync=False) await self.connect_and_sync(sync_count, force_sync=False)
else: else:
await self.send_bridge_notice( await self.send_bridge_notice(
@ -685,18 +685,14 @@ class User(DBUser, BaseUser):
else: else:
ktid = memo_ids[0] ktid = memo_ids[0]
if len(memo_ids) > 1: if len(memo_ids) > 1:
self.log.warning("Found multiple memo chats, so using the first one as a fallback") self.log.info("Found multiple memo chats, so using the first one as a fallback")
if ktid: if ktid:
self.log.debug(f"Found existing direct chat with KakaoTalk user {puppet.ktid}")
else:
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( return await po.Portal.get_by_ktid(
ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type
) if ktid else None )
else:
self.log.warning(f"Didn't find an existing DM channel with KakaoTalk user {puppet.ktid}, so not creating one")
return None
# region KakaoTalk event handling # region KakaoTalk event handling

View File

@ -834,51 +834,6 @@ export default class PeerClient {
return makeCommandResult(receipts) 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 {Object} req
* @param {string} req.mxid * @param {string} req.mxid
@ -887,57 +842,6 @@ export default class PeerClient {
return await this.#getUser(req.mxid).serviceClient.requestFriendList() 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 {Object} req
* @param {string} req.mxid The user whose friend is being looked up. * @param {string} req.mxid The user whose friend is being looked up.
@ -959,7 +863,6 @@ export default class PeerClient {
/** @type Long[] */ /** @type Long[] */
const channelIds = [] const channelIds = []
const channelList = this.#getUser(req.mxid).talkClient.channelList const channelList = this.#getUser(req.mxid).talkClient.channelList
// TODO channelList.all() doesn't really return *all* channels...
for (const channel of channelList.all()) { for (const channel of channelList.all()) {
if (channel.info.type == "MemoChat") { if (channel.info.type == "MemoChat") {
channelIds.push(channel.channelId) channelIds.push(channel.channelId)
@ -1129,20 +1032,6 @@ 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 {Object} req
* @param {string} req.mxid * @param {string} req.mxid
@ -1234,12 +1123,7 @@ export default class PeerClient {
get_participants: this.getParticipants, get_participants: this.getParticipants,
get_chats: this.getChats, get_chats: this.getChats,
get_read_receipts: this.getReadReceipts, get_read_receipts: this.getReadReceipts,
can_change_uuid: this.canChangeUUID,
change_uuid: this.changeUUID,
set_uuid_searchable: this.setUUIDSearchable,
list_friends: this.listFriends, list_friends: this.listFriends,
edit_friend: this.editFriend,
edit_friend_by_uuid: this.editFriendByUUID,
get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"), get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"),
get_memo_ids: this.getMemoIds, get_memo_ids: this.getMemoIds,
download_file: this.downloadFile, download_file: this.downloadFile,
@ -1251,7 +1135,6 @@ export default class PeerClient {
set_channel_name: this.setChannelName, set_channel_name: this.setChannelName,
set_channel_description: this.setChannelDescription, set_channel_description: this.setChannelDescription,
set_channel_photo: this.setChannelPhoto, set_channel_photo: this.setChannelPhoto,
create_direct_chat: this.createDirectChat,
leave_channel: this.leaveChannel, leave_channel: this.leaveChannel,
}[req.command] || this.handleUnknownCommand }[req.command] || this.handleUnknownCommand
} }
@ -1269,15 +1152,11 @@ export default class PeerClient {
} }
} else { } else {
resp.command = "error" resp.command = "error"
if (err instanceof ProtocolError) { resp.error = err instanceof ProtocolError ? err.message : err.toString()
resp.error = err.message
} else {
resp.error = err.toString()
this.log(`Error handling request ${resp.id} ${err.stack}`) this.log(`Error handling request ${resp.id} ${err.stack}`)
}
}
}
// TODO Check if session is broken. If it is, close the PeerClient // TODO Check if session is broken. If it is, close the PeerClient
}
}
await this.write(resp) await this.write(resp)
} }