diff --git a/matrix_appservice_kakaotalk/commands/__init__.py b/matrix_appservice_kakaotalk/commands/__init__.py
index 7d42116..f92dfd5 100644
--- a/matrix_appservice_kakaotalk/commands/__init__.py
+++ b/matrix_appservice_kakaotalk/commands/__init__.py
@@ -1,2 +1,3 @@
-from .auth import SECTION_AUTH#, enter_2fa_code
+from .auth import SECTION_AUTH
from .conn import SECTION_CONNECTION
+from .kakaotalk import SECTION_FRIENDS
diff --git a/matrix_appservice_kakaotalk/commands/kakaotalk.py b/matrix_appservice_kakaotalk/commands/kakaotalk.py
new file mode 100644
index 0000000..3f149d4
--- /dev/null
+++ b/matrix_appservice_kakaotalk/commands/kakaotalk.py
@@ -0,0 +1,69 @@
+# 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
+import asyncio
+
+from mautrix.bridge.commands import HelpSection, command_handler
+
+from ..kt.types.api.struct import ApiUserType
+
+from .. import puppet as pu, user as u
+from .typehint import CommandEvent
+
+from ..kt.client.errors import CommandException
+
+SECTION_FRIENDS = HelpSection("Friends management", 40, "")
+
+if TYPE_CHECKING:
+ from ..kt.types.api.struct import FriendStruct
+
+
+async def _get_search_result_puppet(source: u.User, friend_struct: FriendStruct) -> pu.Puppet:
+ puppet = await pu.Puppet.get_by_ktid(friend_struct.userId)
+ if not puppet.name_set:
+ await puppet.update_info_from_friend(source, friend_struct)
+ return puppet
+
+
+@command_handler(
+ needs_auth=True,
+ management_only=False,
+ help_section=SECTION_FRIENDS,
+ help_text="List all KakaoTalk friends",
+)
+async def list_friends(evt: CommandEvent) -> None:
+ 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
+ puppets = await asyncio.gather(
+ *[
+ _get_search_result_puppet(evt.sender, friend_struct)
+ for friend_struct in resp.friends if friend_struct.userType == ApiUserType.NORMAL
+ # NOTE Using NORMAL to avoid listing KakaoTalk bots, which are apparently PLUS users
+ ]
+ )
+ results = "".join(
+ f"* [{puppet.name}](https://matrix.to/#/{puppet.default_mxid})\n" for puppet in puppets
+ )
+ if results:
+ await evt.reply(f"{results}")
+ else:
+ await evt.reply("No friends found.")
diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py
index 17b7e2b..7b4577d 100644
--- a/matrix_appservice_kakaotalk/kt/client/client.py
+++ b/matrix_appservice_kakaotalk/kt/client/client.py
@@ -37,6 +37,7 @@ from ...config import Config
from ...rpc import RPCClient
from ..types.api.struct.profile import ProfileReqStruct, ProfileStruct
+from ..types.api.struct import FriendListStruct
from ..types.bson import Long
from ..types.client.client_session import LoginResult
from ..types.chat.chat import Chatlog
@@ -230,6 +231,12 @@ class Client:
limit=limit
)
+ async def list_friends(self) -> FriendListStruct:
+ return await self._api_user_request_result(
+ FriendListStruct,
+ "list_friends",
+ )
+
async def send_message(self, channel_props: ChannelProps, text: str) -> Chatlog:
return await self._api_user_request_result(
Chatlog,
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py b/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py
index 06fa069..eb92a23 100644
--- a/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.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 .login import *
+##from .account import *
from .profile import *
-"""
+from .friends import *
+##from .openlink import *
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/friends/__init__.py b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/__init__.py
new file mode 100644
index 0000000..251c973
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/__init__.py
@@ -0,0 +1,21 @@
+# 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 .friend_blocked_list_struct import *
+from .friend_find_struct import *
+from .friend_list_struct import *
+from .friend_req_struct import *
+from .friend_search_struct import *
+from .friend_struct import *
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_blocked_list_struct.py b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_blocked_list_struct.py
new file mode 100644
index 0000000..12126fd
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_blocked_list_struct.py
@@ -0,0 +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 attr import dataclass
+
+from mautrix.types import SerializableAttrs
+
+from .friend_struct import FriendStruct
+
+
+@dataclass
+class FriendBlockedListStruct(SerializableAttrs):
+ total: int
+ blockedFriends: list[FriendStruct]
+
+
+__all__ = [
+ "FriendBlockedListStruct",
+]
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_find_struct.py b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_find_struct.py
new file mode 100644
index 0000000..05bdbdb
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_find_struct.py
@@ -0,0 +1,38 @@
+# 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 attr import dataclass
+
+from mautrix.types import SerializableAttrs
+
+from ....bson import Long
+from .friend_struct import FriendStruct
+
+
+@dataclass
+class FriendFindIdStruct(SerializableAttrs):
+ token: Long
+ friend: FriendStruct
+
+
+@dataclass
+class FriendFindUUIDStruct(SerializableAttrs):
+ member: FriendStruct
+
+
+__all__ = [
+ "FriendFindIdStruct",
+ "FriendFindUUIDStruct",
+]
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_list_struct.py b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_list_struct.py
new file mode 100644
index 0000000..bde3391
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_list_struct.py
@@ -0,0 +1,32 @@
+# 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 attr import dataclass
+
+from mautrix.types import SerializableAttrs
+
+from ....bson import Long
+from .friend_struct import FriendStruct
+
+
+@dataclass
+class FriendListStruct(SerializableAttrs):
+ token: Long
+ friends: list[FriendStruct]
+
+
+__all__ = [
+ "FriendListStruct",
+]
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_req_struct.py b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_req_struct.py
new file mode 100644
index 0000000..c167cff
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_req_struct.py
@@ -0,0 +1,36 @@
+# 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 attr import dataclass
+
+from mautrix.types import SerializableAttrs
+
+from .friend_struct import FriendStruct
+
+
+@dataclass
+class FriendReqStruct(SerializableAttrs):
+ friend: FriendStruct
+
+
+@dataclass
+class FriendReqPhoneNumberStruct(SerializableAttrs):
+ pstn_number: str
+
+
+__all__ = [
+ "FriendReqStruct",
+ "FriendReqPhoneNumberStruct",
+]
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_search_struct.py b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_search_struct.py
new file mode 100644
index 0000000..b3fb5d1
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_search_struct.py
@@ -0,0 +1,43 @@
+# 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 typing import Optional
+
+from attr import dataclass
+
+from mautrix.types import SerializableAttrs
+
+from .friend_struct import FriendStruct
+
+
+@dataclass
+class FriendSearchUserListStruct(SerializableAttrs):
+ count: int
+ list: list[FriendStruct]
+
+
+@dataclass(kw_only=True)
+class FriendSearchStruct(SerializableAttrs):
+ query: str
+ user: Optional[FriendSearchUserListStruct] = None
+ plus: Optional[FriendSearchUserListStruct] = None
+ categories: list[str]
+ total_counts: int
+
+
+__all__ = [
+ "FriendSearchUserListStruct",
+ "FriendSearchStruct",
+]
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_struct.py b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_struct.py
new file mode 100644
index 0000000..b7f4182
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/friends/friend_struct.py
@@ -0,0 +1,72 @@
+# 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 typing import Union, Optional
+from enum import IntEnum
+
+from attr import dataclass
+
+from mautrix.types import SerializableAttrs
+
+from ....bson import Long
+
+
+class ApiUserType(IntEnum):
+ NORMAL = 0
+ PLUS = 1
+
+
+@dataclass
+class FriendExt(SerializableAttrs):
+ addible: bool
+ yellowid: bool
+ consultable: bool
+ friendsCount: int
+ verificationType: str
+ isAdult: bool
+ writable: bool
+ serviceTypeCode: int
+ isOfficial: bool
+
+
+@dataclass(kw_only=True)
+class FriendStruct(SerializableAttrs):
+ userId: Union[Long, int]
+ nickName: str
+ type: int
+ phoneNumber: str
+ statusMessage: str
+ UUID: str
+ friendNickName: str
+ phoneticName: Optional[str] = None
+ accountId: int
+ profileImageUrl: str
+ fullProfileImageUrl: str
+ originalProfileImageUrl: str
+ userType: ApiUserType;
+ ext: Union[FriendExt, str];
+ hidden: bool
+ purged: bool
+ favorite: bool
+ screenToken: int
+ suspended: bool = False
+ directChatId: int
+
+
+__all__ = [
+ "ApiUserType",
+ "FriendExt",
+ "FriendStruct",
+]
\ No newline at end of file
diff --git a/matrix_appservice_kakaotalk/kt/types/bson.py b/matrix_appservice_kakaotalk/kt/types/bson.py
index ee71916..8456ad2 100644
--- a/matrix_appservice_kakaotalk/kt/types/bson.py
+++ b/matrix_appservice_kakaotalk/kt/types/bson.py
@@ -22,5 +22,4 @@ class Long(int, Serializable):
@classmethod
def deserialize(cls, raw: JSON) -> "Long":
- assert isinstance(raw, str), f"Long deserialization expected a string, but got non-string value {raw}"
return cls(raw)
diff --git a/matrix_appservice_kakaotalk/kt/types/request.py b/matrix_appservice_kakaotalk/kt/types/request.py
index ac92693..b945326 100644
--- a/matrix_appservice_kakaotalk/kt/types/request.py
+++ b/matrix_appservice_kakaotalk/kt/types/request.py
@@ -123,7 +123,7 @@ def deserialize_result(
result_type: Type[ResultType], data: JSON
) -> Union[CommandResultDoneValue[ResultType], RootCommandResult]:
"""Returns equivalent of CommandResult. Does no consistency checking on success & result properties."""
- if "result" in data:
+ if "result" in data and data.get("success"):
# TODO Allow arbitrary result object?
return CommandResultDoneValue.deserialize_result(result_type, data)
else:
diff --git a/matrix_appservice_kakaotalk/portal.py b/matrix_appservice_kakaotalk/portal.py
index 2b4bb94..249a756 100644
--- a/matrix_appservice_kakaotalk/portal.py
+++ b/matrix_appservice_kakaotalk/portal.py
@@ -420,7 +420,7 @@ class Portal(DBPortal, BasePortal):
# TODO nick_map?
for participant in participants:
puppet = await p.Puppet.get_by_ktid(participant.userId)
- await puppet.update_info(source, participant)
+ await puppet.update_info_from_participant(source, participant)
if self.is_direct and self._kt_sender == puppet.ktid and self.encrypted:
changed = await self._update_name(puppet.name) or changed
changed = await self._update_photo_from_puppet(puppet) or changed
diff --git a/matrix_appservice_kakaotalk/puppet.py b/matrix_appservice_kakaotalk/puppet.py
index 65a7366..f26ebbc 100644
--- a/matrix_appservice_kakaotalk/puppet.py
+++ b/matrix_appservice_kakaotalk/puppet.py
@@ -33,6 +33,7 @@ from .db import Puppet as DBPuppet
from .kt.types.bson import Long
+from .kt.types.api.struct import FriendStruct
from .kt.types.channel.channel_type import KnownChannelType
from .kt.client.types import UserInfoUnion
@@ -147,25 +148,51 @@ class Puppet(DBPuppet, BasePuppet):
# region User info updating
- async def update_info(
+ async def update_info_from_participant(
self,
source: u.User,
info: UserInfoUnion,
update_avatar: bool = True,
+ ) -> Puppet:
+ await self._update_info(
+ source,
+ info.nickname,
+ info.profileURL,
+ update_avatar
+ )
+
+ async def update_info_from_friend(
+ self,
+ source: u.User,
+ info: FriendStruct,
+ update_avatar: bool = True,
+ ) -> Puppet:
+ await self._update_info(
+ source,
+ info.nickName,
+ info.profileImageUrl,
+ update_avatar
+ )
+
+ async def _update_info(
+ self,
+ source: u.User,
+ name: str,
+ avatar_url: str,
+ update_avatar: bool = True,
) -> Puppet:
self._last_info_sync = datetime.now()
try:
- changed = await self._update_name(info)
+ changed = await self._update_name(name)
if update_avatar:
- changed = await self._update_photo(source, info.profileURL) or changed
+ changed = await self._update_photo(source, avatar_url) or changed
if changed:
await self.save()
except Exception:
self.log.exception(f"Failed to update info from source {source.ktid}")
return self
- async def _update_name(self, info: UserInfoUnion) -> bool:
- name = info.nickname
+ async def _update_name(self, name: str) -> bool:
if name != self.name or not self.name_set:
self.name = name
try:
diff --git a/node/src/client.js b/node/src/client.js
index ff300ea..33d3c47 100644
--- a/node/src/client.js
+++ b/node/src/client.js
@@ -33,6 +33,23 @@ const { KnownChatType } = chat
import { emitLines, promisify } from "./util.js"
+ServiceApiClient.prototype.requestFriendList = async function() {
+ const res = await this._client.requestData(
+ "POST",
+ `${this.getFriendsApiPath("update.json")}`,
+ {
+ phone_number_type: 1,
+ }
+ );
+
+ return {
+ status: res.status,
+ success: res.status === 0,
+ result: res,
+ };
+}
+
+
class UserClient {
static #initializing = false
@@ -270,8 +287,8 @@ export default class PeerClient {
/**
* Get the service client for the specified user ID, or create
* and return a new service client if no user ID is provided.
- * @param {string} mxid
- * @param {OAuthCredential} oauth_credential
+ * @param {?string} mxid
+ * @param {?OAuthCredential} oauth_credential
*/
async #getServiceClient(mxid, oauth_credential) {
return this.#tryGetUser(mxid)?.serviceClient ||
@@ -341,8 +358,8 @@ export default class PeerClient {
/**
* @param {Object} req
- * @param {string} req.mxid
- * @param {OAuthCredential} req.oauth_credential
+ * @param {?string} req.mxid
+ * @param {?OAuthCredential} req.oauth_credential
*/
getOwnProfile = async (req) => {
const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential)
@@ -351,8 +368,8 @@ export default class PeerClient {
/**
* @param {Object} req
- * @param {string} req.mxid
- * @param {OAuthCredential} req.oauth_credential
+ * @param {?string} req.mxid
+ * @param {?OAuthCredential} req.oauth_credential
* @param {Long} req.user_id
*/
getProfile = async (req) => {
@@ -394,8 +411,8 @@ export default class PeerClient {
* @param {Object} req
* @param {string} req.mxid
* @param {Object} req.channel_props
- * @param {Long?} req.sync_from
- * @param {Number?} req.limit
+ * @param {?Long} req.sync_from
+ * @param {?Number} req.limit
*/
getChats = async (req) => {
const userClient = this.#getUser(req.mxid)
@@ -408,6 +425,16 @@ export default class PeerClient {
return res
}
+ /**
+ * @param {Object} req
+ * @param {?string} req.mxid
+ * @param {?OAuthCredential} req.oauth_credential
+ */
+ listFriends = async (req) => {
+ const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential)
+ return await serviceClient.requestFriendList()
+ }
+
/**
* @param {Object} req
* @param {string} req.mxid
@@ -500,6 +527,7 @@ export default class PeerClient {
get_portal_channel_info: this.getPortalChannelInfo,
get_participants: this.getParticipants,
get_chats: this.getChats,
+ list_friends: this.listFriends,
send_message: this.sendMessage,
}[req.command] || this.handleUnknownCommand
}