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 .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 ..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,

View File

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

View File

@ -123,7 +123,7 @@ def deserialize_result(
result_type: Type[ResultType], data: JSON
) -> Union[CommandResultDoneValue[ResultType], RootCommandResult]:
"""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?
return CommandResultDoneValue.deserialize_result(result_type, data)
else:

View File

@ -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

View File

@ -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:

View File

@ -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
}