Add command for listing friends

This commit is contained in:
Andrew Ferrazzutti 2022-03-23 03:09:30 -04:00
parent 66262caa63
commit 2d9ae53d89
16 changed files with 425 additions and 19 deletions

View File

@ -1,2 +1,3 @@
from .auth import SECTION_AUTH#, enter_2fa_code from .auth import SECTION_AUTH
from .conn import SECTION_CONNECTION from .conn import SECTION_CONNECTION
from .kakaotalk import SECTION_FRIENDS

View File

@ -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 <https://www.gnu.org/licenses/>.
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.")

View File

@ -37,6 +37,7 @@ from ...config import Config
from ...rpc import RPCClient from ...rpc import RPCClient
from ..types.api.struct.profile import ProfileReqStruct, ProfileStruct from ..types.api.struct.profile import ProfileReqStruct, ProfileStruct
from ..types.api.struct import FriendListStruct
from ..types.bson import Long from ..types.bson import Long
from ..types.client.client_session import LoginResult from ..types.client.client_session import LoginResult
from ..types.chat.chat import Chatlog from ..types.chat.chat import Chatlog
@ -230,6 +231,12 @@ class Client:
limit=limit 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: async def send_message(self, channel_props: ChannelProps, text: str) -> Chatlog:
return await self._api_user_request_result( return await self._api_user_request_result(
Chatlog, Chatlog,

View File

@ -13,6 +13,8 @@
# #
# 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 .login import *
##from .account import *
from .profile import * from .profile import *
""" from .friends import *
##from .openlink import *

View File

@ -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 <https://www.gnu.org/licenses/>.
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 *

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -22,5 +22,4 @@ class Long(int, Serializable):
@classmethod @classmethod
def deserialize(cls, raw: JSON) -> "Long": 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) return cls(raw)

View File

@ -123,7 +123,7 @@ def deserialize_result(
result_type: Type[ResultType], data: JSON result_type: Type[ResultType], data: JSON
) -> 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: if "result" in data and data.get("success"):
# 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

@ -420,7 +420,7 @@ class Portal(DBPortal, BasePortal):
# TODO nick_map? # TODO nick_map?
for participant in participants: for participant in participants:
puppet = await p.Puppet.get_by_ktid(participant.userId) 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: 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_name(puppet.name) or changed
changed = await self._update_photo_from_puppet(puppet) or changed changed = await self._update_photo_from_puppet(puppet) or changed

View File

@ -33,6 +33,7 @@ from .db import Puppet as DBPuppet
from .kt.types.bson import Long from .kt.types.bson import Long
from .kt.types.api.struct import FriendStruct
from .kt.types.channel.channel_type import KnownChannelType from .kt.types.channel.channel_type import KnownChannelType
from .kt.client.types import UserInfoUnion from .kt.client.types import UserInfoUnion
@ -147,25 +148,51 @@ class Puppet(DBPuppet, BasePuppet):
# region User info updating # region User info updating
async def update_info( async def update_info_from_participant(
self, self,
source: u.User, source: u.User,
info: UserInfoUnion, info: UserInfoUnion,
update_avatar: bool = True, 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: ) -> Puppet:
self._last_info_sync = datetime.now() self._last_info_sync = datetime.now()
try: try:
changed = await self._update_name(info) changed = await self._update_name(name)
if update_avatar: 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: if changed:
await self.save() await self.save()
except Exception: except Exception:
self.log.exception(f"Failed to update info from source {source.ktid}") self.log.exception(f"Failed to update info from source {source.ktid}")
return self return self
async def _update_name(self, info: UserInfoUnion) -> bool: async def _update_name(self, name: str) -> bool:
name = info.nickname
if name != self.name or not self.name_set: if name != self.name or not self.name_set:
self.name = name self.name = name
try: try:

View File

@ -33,6 +33,23 @@ const { KnownChatType } = chat
import { emitLines, promisify } from "./util.js" 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 { class UserClient {
static #initializing = false static #initializing = false
@ -270,8 +287,8 @@ export default class PeerClient {
/** /**
* Get the service client for the specified user ID, or create * Get the service client for the specified user ID, or create
* and return a new service client if no user ID is provided. * and return a new service client if no user ID is provided.
* @param {string} mxid * @param {?string} mxid
* @param {OAuthCredential} oauth_credential * @param {?OAuthCredential} oauth_credential
*/ */
async #getServiceClient(mxid, oauth_credential) { async #getServiceClient(mxid, oauth_credential) {
return this.#tryGetUser(mxid)?.serviceClient || return this.#tryGetUser(mxid)?.serviceClient ||
@ -341,8 +358,8 @@ export default class PeerClient {
/** /**
* @param {Object} req * @param {Object} req
* @param {string} req.mxid * @param {?string} req.mxid
* @param {OAuthCredential} req.oauth_credential * @param {?OAuthCredential} req.oauth_credential
*/ */
getOwnProfile = async (req) => { getOwnProfile = async (req) => {
const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential) const serviceClient = await this.#getServiceClient(req.mxid, req.oauth_credential)
@ -351,8 +368,8 @@ export default class PeerClient {
/** /**
* @param {Object} req * @param {Object} req
* @param {string} req.mxid * @param {?string} req.mxid
* @param {OAuthCredential} req.oauth_credential * @param {?OAuthCredential} req.oauth_credential
* @param {Long} req.user_id * @param {Long} req.user_id
*/ */
getProfile = async (req) => { getProfile = async (req) => {
@ -394,8 +411,8 @@ export default class PeerClient {
* @param {Object} req * @param {Object} req
* @param {string} req.mxid * @param {string} req.mxid
* @param {Object} req.channel_props * @param {Object} req.channel_props
* @param {Long?} req.sync_from * @param {?Long} req.sync_from
* @param {Number?} req.limit * @param {?Number} req.limit
*/ */
getChats = async (req) => { getChats = async (req) => {
const userClient = this.#getUser(req.mxid) const userClient = this.#getUser(req.mxid)
@ -408,6 +425,16 @@ export default class PeerClient {
return res 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 {Object} req
* @param {string} req.mxid * @param {string} req.mxid
@ -500,6 +527,7 @@ export default class PeerClient {
get_portal_channel_info: this.getPortalChannelInfo, get_portal_channel_info: this.getPortalChannelInfo,
get_participants: this.getParticipants, get_participants: this.getParticipants,
get_chats: this.getChats, get_chats: this.getChats,
list_friends: this.listFriends,
send_message: this.sendMessage, send_message: this.sendMessage,
}[req.command] || this.handleUnknownCommand }[req.command] || this.handleUnknownCommand
} }