Add command for listing friends
This commit is contained in:
parent
66262caa63
commit
2d9ae53d89
@ -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
|
||||
|
69
matrix_appservice_kakaotalk/commands/kakaotalk.py
Normal file
69
matrix_appservice_kakaotalk/commands/kakaotalk.py
Normal 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.")
|
@ -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,
|
||||
|
@ -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 *
|
||||
|
@ -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 *
|
@ -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",
|
||||
]
|
@ -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",
|
||||
]
|
@ -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",
|
||||
]
|
@ -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",
|
||||
]
|
@ -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",
|
||||
]
|
@ -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",
|
||||
]
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user