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 existing long-idled KakaoTalk channels
|
||||||
* [ ] For new KakaoTalk channels
|
* [ ] For new KakaoTalk channels
|
||||||
* [x] Option to use own Matrix account for messages sent from other KakaoTalk clients
|
* [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>[1]</sup> Sometimes fails with "Invalid body" error
|
||||||
<sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted
|
<sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted
|
||||||
|
@ -19,8 +19,17 @@ from typing import TYPE_CHECKING
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from mautrix.bridge.commands import HelpSection, command_handler
|
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 ..kt.types.api.struct import ApiUserType
|
||||||
|
from ..rpc.types import RPCError
|
||||||
|
|
||||||
from .. import puppet as pu, user as u
|
from .. import puppet as pu, user as u
|
||||||
from .typehint import CommandEvent
|
from .typehint import CommandEvent
|
||||||
@ -31,7 +40,10 @@ SECTION_FRIENDS = HelpSection("Friends management", 40, "")
|
|||||||
SECTION_CHANNELS = HelpSection("Channel management", 45, "")
|
SECTION_CHANNELS = HelpSection("Channel management", 45, "")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from mautrix.types import UserID
|
||||||
|
|
||||||
from ..kt.types.api.struct import FriendStruct
|
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:
|
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.")
|
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(
|
@command_handler(
|
||||||
needs_auth=True,
|
needs_auth=True,
|
||||||
management_only=False,
|
management_only=False,
|
||||||
|
@ -330,6 +330,32 @@ class Client:
|
|||||||
"list_friends",
|
"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:
|
async def get_friend_dm_id(self, friend_id: Long) -> Long | None:
|
||||||
try:
|
try:
|
||||||
return await self._api_user_request_result(
|
return await self._api_user_request_result(
|
||||||
|
@ -842,6 +842,57 @@ export default class PeerClient {
|
|||||||
return await this.#getUser(req.mxid).serviceClient.requestFriendList()
|
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 {Object} req
|
||||||
* @param {string} req.mxid The user whose friend is being looked up.
|
* @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_chats: this.getChats,
|
||||||
get_read_receipts: this.getReadReceipts,
|
get_read_receipts: this.getReadReceipts,
|
||||||
list_friends: this.listFriends,
|
list_friends: this.listFriends,
|
||||||
|
edit_friend: this.editFriend,
|
||||||
|
edit_friend_by_uuid: this.editFriendByUUID,
|
||||||
get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"),
|
get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"),
|
||||||
get_memo_ids: this.getMemoIds,
|
get_memo_ids: this.getMemoIds,
|
||||||
download_file: this.downloadFile,
|
download_file: this.downloadFile,
|
||||||
|
Loading…
Reference in New Issue
Block a user