Allow adding/removing KakaoTalk friends from Matrix
This commit is contained in:
parent
5ae5970ef0
commit
b994ca65ee
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue