Allow adding/removing KakaoTalk friends from Matrix

remotes/1707911681074910269/master
Andrew Ferrazzutti 2022-05-06 00:57:01 -04:00
parent 5ae5970ef0
commit b994ca65ee
4 changed files with 206 additions and 0 deletions

View File

@ -78,6 +78,15 @@
* [ ] For existing long-idled KakaoTalk channels
* [ ] For new KakaoTalk channels
* [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
<sup>[1]</sup> Sometimes fails with "Invalid body" error
<sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted

View File

@ -19,8 +19,17 @@ from typing import TYPE_CHECKING
import asyncio
from mautrix.bridge.commands import HelpSection, command_handler
from mautrix.util import utf16_surrogate
from mautrix.util.formatter import (
EntityString,
EntityType,
MarkdownString,
MatrixParser,
SimpleEntity,
)
from ..kt.types.api.struct import ApiUserType
from ..rpc.types import RPCError
from .. import puppet as pu, user as u
from .typehint import CommandEvent
@ -31,7 +40,10 @@ SECTION_FRIENDS = HelpSection("Friends management", 40, "")
SECTION_CHANNELS = HelpSection("Channel management", 45, "")
if TYPE_CHECKING:
from mautrix.types import UserID
from ..kt.types.api.struct import FriendStruct
from ..kt.types.bson import Long
async def _get_search_result_puppet(source: u.User, friend_struct: FriendStruct) -> pu.Puppet:
@ -70,6 +82,112 @@ async def list_friends(evt: CommandEvent) -> None:
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_>",
)
async def add_friend(evt: CommandEvent) -> None:
await _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_>",
)
async def remove_friend(evt: CommandEvent) -> None:
await _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:
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:
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(
needs_auth=True,
management_only=False,

View File

@ -330,6 +330,32 @@ class Client:
"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:
try:
return await self._api_user_request_result(

View File

@ -842,6 +842,57 @@ export default class PeerClient {
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 {string} req.mxid The user whose friend is being looked up.
@ -1124,6 +1175,8 @@ export default class PeerClient {
get_chats: this.getChats,
get_read_receipts: this.getReadReceipts,
list_friends: this.listFriends,
edit_friend: this.editFriend,
edit_friend_by_uuid: this.editFriendByUUID,
get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"),
get_memo_ids: this.getMemoIds,
download_file: this.downloadFile,