Compare commits

...

8 Commits

Author SHA1 Message Date
4e6498f777 Create new direct chat if necessary when creating new DM portal
Might fail on first attempt, though
2022-05-06 04:25:08 -04:00
c3726220de Actually skip channel syncing when bridge.sync_on_startup is false 2022-05-06 03:51:20 -04:00
a817049e12 Document that error responses can have a result object after all
Also document some custom result types
2022-05-06 02:50:55 -04:00
dcf17fd40a Awaitable and mark_read for account/friend management commands 2022-05-06 02:49:49 -04:00
c3b299d26c Make LoginResult.lastChannelId optional
Needed for backfilling users who have not joined any channels
2022-05-06 02:48:58 -04:00
faf174c226 Move "whoami" command to Account Management section 2022-05-06 02:47:42 -04:00
dfdd98da96 KakaoTalk ID management from Matrix
Add commands to set ID and make it searchable/hidden.
Also don't print stack traces of ProtocolErrors.
2022-05-06 02:47:42 -04:00
b994ca65ee Allow adding/removing KakaoTalk friends from Matrix 2022-05-06 00:57:01 -04:00
10 changed files with 457 additions and 60 deletions

View File

@ -73,11 +73,20 @@
* [x] At startup * [x] At startup
* [x] When added to chat * [x] When added to chat
* [x] When receiving message * [x] When receiving message
* [ ] Private chat creation by inviting Matrix puppet of KakaoTalk user to new room * [x] Direct 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,12 +14,9 @@
# 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, "")
@ -57,44 +54,20 @@ async def disconnect(evt: CommandEvent) -> None:
@command_handler( @command_handler(
needs_auth=True, needs_auth=False,
management_only=True, management_only=True,
help_section=SECTION_CONNECTION, help_section=SECTION_CONNECTION,
help_text="Check if you're logged into KakaoTalk and retrieve your account information", help_text="Check if you're logged in to KakaoTalk and connected to chats",
)
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:
assert evt.sender.client if not await evt.sender.is_logged_in():
is_connected = evt.sender.is_connected and await evt.sender.client.is_connected() await evt.reply("You are **logged out** of KakaoTalk.")
await evt.reply( else:
f"You are {'connected to' if is_connected else '**disconnected** from'} KakaoTalk chats." 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( @command_handler(

View File

@ -15,24 +15,143 @@
# 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 from typing import TYPE_CHECKING, Awaitable
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 ..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: 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)
@ -48,9 +167,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
@ -70,6 +189,114 @@ 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,12 +324,52 @@ 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(
@ -465,6 +505,13 @@ 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.
""" """
# NOTE unsuccessful responses do not set a result, hence using RootCommandResult here # TODO Handle result object
# 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, Union from typing import NewType, Optional, 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,6 +93,7 @@ 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]
@ -104,6 +105,7 @@ 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
@ -132,6 +134,7 @@ 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, user as u from . import portal as po, puppet as pu, user as u
from .db import Message as DBMessage from .db import Message as DBMessage
if TYPE_CHECKING: if TYPE_CHECKING:
@ -63,6 +63,19 @@ 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) -> SettingsStruct | None: async def get_own_info(self, *, force: bool = False) -> SettingsStruct | None:
if self._client and (not self._logged_in_info or self._logged_in_info_time + 60 * 60 < time.monotonic()): 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 = 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 = None sync_count = 0
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,14 +685,18 @@ 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.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: if ktid:
return await po.Portal.get_by_ktid( self.log.debug(f"Found existing direct chat with KakaoTalk user {puppet.ktid}")
ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type
)
else: else:
self.log.warning(f"Didn't find an existing DM channel with KakaoTalk user {puppet.ktid}, so not creating one") self.log.debug(f"Didn't find an existing direct chat with KakaoTalk user {puppet.ktid}, so will create one")
return None 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 # region KakaoTalk event handling

View File

@ -834,6 +834,51 @@ 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
@ -842,6 +887,57 @@ 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.
@ -863,6 +959,7 @@ 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)
@ -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 {Object} req
* @param {string} req.mxid * @param {string} req.mxid
@ -1123,7 +1234,12 @@ 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,
@ -1135,6 +1251,7 @@ 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
} }
@ -1152,11 +1269,15 @@ export default class PeerClient {
} }
} else { } else {
resp.command = "error" resp.command = "error"
resp.error = err instanceof ProtocolError ? err.message : err.toString() if (err instanceof ProtocolError) {
this.log(`Error handling request ${resp.id} ${err.stack}`) resp.error = err.message
// TODO Check if session is broken. If it is, close the PeerClient } 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) await this.write(resp)
} }