Compare commits
33 Commits
095641fe08
...
0dc75b8f1c
Author | SHA1 | Date |
---|---|---|
Andrew Ferrazzutti | 0dc75b8f1c | |
Andrew Ferrazzutti | 46ace01fea | |
Andrew Ferrazzutti | 82d64b9b37 | |
Andrew Ferrazzutti | 47b9623446 | |
Andrew Ferrazzutti | 1541732d0b | |
Andrew Ferrazzutti | bccd0ed4e0 | |
Andrew Ferrazzutti | bb9cdbd15e | |
Andrew Ferrazzutti | 1897c1e494 | |
Andrew Ferrazzutti | 2cd7697aa5 | |
Andrew Ferrazzutti | 27b2c15ad3 | |
Andrew Ferrazzutti | 454d1b72cc | |
Andrew Ferrazzutti | 79fe8748b1 | |
Andrew Ferrazzutti | d7d8cbbbb6 | |
Andrew Ferrazzutti | d4d02e8aba | |
Andrew Ferrazzutti | fbd3d514e3 | |
Andrew Ferrazzutti | 53d3170c04 | |
Andrew Ferrazzutti | 45fdd5ca29 | |
Andrew Ferrazzutti | 36598c34f6 | |
Andrew Ferrazzutti | 3f8660a3c4 | |
Andrew Ferrazzutti | 652aa22048 | |
Andrew Ferrazzutti | c691372c6a | |
Andrew Ferrazzutti | 76d0ead99f | |
Andrew Ferrazzutti | 8da7f1efbd | |
Andrew Ferrazzutti | 20bdbf9cd1 | |
Andrew Ferrazzutti | 3c0d890577 | |
Andrew Ferrazzutti | 54c772d3ac | |
Andrew Ferrazzutti | b8b451b751 | |
Andrew Ferrazzutti | d594fb98d1 | |
Andrew Ferrazzutti | c90f86849e | |
Andrew Ferrazzutti | 5b16694f78 | |
Andrew Ferrazzutti | 37101f42c1 | |
Andrew Ferrazzutti | a49e2768a3 | |
Andrew Ferrazzutti | 128979e06b |
20
ROADMAP.md
20
ROADMAP.md
|
@ -24,10 +24,10 @@
|
||||||
* [x] Leave<sup>[3]</sup>
|
* [x] Leave<sup>[3]</sup>
|
||||||
* [ ] Ban<sup>[4]</sup>
|
* [ ] Ban<sup>[4]</sup>
|
||||||
* [ ] Unban<sup>[4]</sup>
|
* [ ] Unban<sup>[4]</sup>
|
||||||
* [ ] Room metadata changes<sup>[1]</sup>
|
* [ ] Room metadata changes
|
||||||
* [x] Name
|
* [x] Name
|
||||||
* [x] Topic
|
* [x] Topic
|
||||||
* [x] Avatar
|
* [ ] Avatar
|
||||||
* [ ] Per-room user nick
|
* [ ] Per-room user nick
|
||||||
* KakaoTalk → Matrix
|
* KakaoTalk → Matrix
|
||||||
* [ ] Message content
|
* [ ] Message content
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
* [x] On live event
|
* [x] On live event
|
||||||
* [x] Kick<sup>[4]</sup>
|
* [x] Kick<sup>[4]</sup>
|
||||||
* [x] Unkick<sup>[4]</sup>
|
* [x] Unkick<sup>[4]</sup>
|
||||||
* [ ] Channel metadata
|
* [x] Channel metadata
|
||||||
* [x] Name
|
* [x] Name
|
||||||
* [x] Description
|
* [x] Description
|
||||||
* [x] Cover photo<sup>[5]</sup>
|
* [x] Cover photo<sup>[5]</sup>
|
||||||
|
@ -74,6 +74,13 @@
|
||||||
* [x] When added to chat
|
* [x] When added to chat
|
||||||
* [x] When receiving message
|
* [x] When receiving message
|
||||||
* [x] Direct chat creation by inviting Matrix puppet of KakaoTalk user to new room
|
* [x] Direct chat creation by inviting Matrix puppet of KakaoTalk user to new room
|
||||||
|
* [ ] Open Chat creation by bot command
|
||||||
|
* [ ] Group Chat
|
||||||
|
* [ ] 1:1 Chat
|
||||||
|
* [ ] Open Chat settings management
|
||||||
|
* [ ] Public search
|
||||||
|
* [ ] Max number of participants
|
||||||
|
* [ ] Chatroom code
|
||||||
* [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
|
* [ ] KakaoTalk friends list management
|
||||||
* [x] List friends
|
* [x] List friends
|
||||||
|
@ -82,8 +89,9 @@
|
||||||
* [x] By Matrix puppet of KakaoTalk user
|
* [x] By Matrix puppet of KakaoTalk user
|
||||||
* [ ] By phone number
|
* [ ] By phone number
|
||||||
* [x] Remove friend
|
* [x] Remove friend
|
||||||
* [ ] Manage favourite friends
|
* [ ] Favourite friends
|
||||||
* [ ] Manage hidden friends
|
* [ ] Hidden friends
|
||||||
|
* [ ] Blocked users
|
||||||
* [x] KakaoTalk ID management
|
* [x] KakaoTalk ID management
|
||||||
* [x] Set/Change ID
|
* [x] Set/Change ID
|
||||||
* [x] Make ID searchable/hidden
|
* [x] Make ID searchable/hidden
|
||||||
|
@ -92,4 +100,4 @@
|
||||||
<sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted
|
<sup>[2]</sup> Only recently-sent KakaoTalk messages can be deleted
|
||||||
<sup>[3]</sup> To make your KakaoTalk account leave a channel, send the `leave` command in a Matrix portal room. Simply leaving a Matrix portal room will keep your KakaoTalk account in the channel.
|
<sup>[3]</sup> To make your KakaoTalk account leave a channel, send the `leave` command in a Matrix portal room. Simply leaving a Matrix portal room will keep your KakaoTalk account in the channel.
|
||||||
<sup>[4]</sup> Kicks in KakaoTalk are equivalent to bans in Matrix
|
<sup>[4]</sup> Kicks in KakaoTalk are equivalent to bans in Matrix
|
||||||
<sup>[5]</sup> Might only get synced on backfill, or on changing channel name/description
|
<sup>[5]</sup> Might not get synced on backfill or initial portal creation. Might get synced on live update to channel name/description
|
||||||
|
|
|
@ -69,10 +69,30 @@ async def login(evt: CommandEvent) -> None:
|
||||||
await evt.reply("You're already logged in")
|
await evt.reply("You're already logged in")
|
||||||
return
|
return
|
||||||
|
|
||||||
save = len(evt.args) > 0 and evt.args[0] == "--save"
|
num_args = len(evt.args)
|
||||||
email = evt.args[0 if not save else 1] if len(evt.args) > 0 else None
|
save = num_args > 0 and evt.args[0] == "--save"
|
||||||
|
# TODO Once web login is implemented, don't make <email> a mandatory argument
|
||||||
|
if not save and num_args != 1:
|
||||||
|
await evt.reply("**Usage:** `$cmdprefix+sp login [--save] <email>`")
|
||||||
|
return
|
||||||
|
email = evt.args[0 if not save else 1] if num_args > 0 else None
|
||||||
|
|
||||||
if email:
|
if email:
|
||||||
|
try:
|
||||||
|
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
||||||
|
except:
|
||||||
|
evt.log.exception("Exception while looking for saved password")
|
||||||
|
creds = None
|
||||||
|
if creds and creds.email == email:
|
||||||
|
await evt.reply("Logging in with saved password")
|
||||||
|
evt.sender.command_status = {
|
||||||
|
"action": "Login with saved password",
|
||||||
|
"room_id": evt.room_id,
|
||||||
|
"save": True,
|
||||||
|
}
|
||||||
|
await _login_with_password(evt, email, creds.password, evt.sender.force_login)
|
||||||
|
return
|
||||||
|
|
||||||
evt.sender.command_status = {
|
evt.sender.command_status = {
|
||||||
"action": "Login",
|
"action": "Login",
|
||||||
"room_id": evt.room_id,
|
"room_id": evt.room_id,
|
||||||
|
@ -81,20 +101,6 @@ async def login(evt: CommandEvent) -> None:
|
||||||
"save": save,
|
"save": save,
|
||||||
"forced": evt.sender.force_login,
|
"forced": evt.sender.force_login,
|
||||||
}
|
}
|
||||||
try:
|
|
||||||
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
|
||||||
except:
|
|
||||||
evt.log.exception("Exception while looking for saved password")
|
|
||||||
creds = None
|
|
||||||
if creds and creds.email == email:
|
|
||||||
await evt.reply("Logging in with saved password")
|
|
||||||
await _login_with_password(
|
|
||||||
evt,
|
|
||||||
evt.sender.command_status.pop("email"),
|
|
||||||
creds.password,
|
|
||||||
evt.sender.command_status.pop("forced"),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
""" TODO Implement web login
|
""" TODO Implement web login
|
||||||
if evt.bridge.public_website:
|
if evt.bridge.public_website:
|
||||||
|
@ -277,7 +283,7 @@ async def _handle_login_failure(evt: CommandEvent, e: Exception) -> None:
|
||||||
needs_auth=True,
|
needs_auth=True,
|
||||||
management_only=True,
|
management_only=True,
|
||||||
help_section=SECTION_AUTH,
|
help_section=SECTION_AUTH,
|
||||||
help_text="Delete saved login password, if it was saved"
|
help_text="Delete saved login password, if it was saved",
|
||||||
)
|
)
|
||||||
async def forget_password(evt: CommandEvent) -> None:
|
async def forget_password(evt: CommandEvent) -> None:
|
||||||
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
creds = await LoginCredential.get_by_mxid(evt.sender.mxid)
|
||||||
|
@ -337,7 +343,8 @@ async def reset_device(evt: CommandEvent) -> None:
|
||||||
@command_handler(
|
@command_handler(
|
||||||
needs_auth=False,
|
needs_auth=False,
|
||||||
help_section=SECTION_AUTH,
|
help_section=SECTION_AUTH,
|
||||||
help_text="When logging in, automatically log out of any other existing KakaoTalk session"
|
help_text="When logging in, automatically log out of any other existing KakaoTalk session",
|
||||||
|
aliases=["enable-force-login"],
|
||||||
)
|
)
|
||||||
async def enable_forced_login(evt: CommandEvent) -> None:
|
async def enable_forced_login(evt: CommandEvent) -> None:
|
||||||
if evt.sender.force_login:
|
if evt.sender.force_login:
|
||||||
|
@ -350,7 +357,8 @@ async def enable_forced_login(evt: CommandEvent) -> None:
|
||||||
@command_handler(
|
@command_handler(
|
||||||
needs_auth=False,
|
needs_auth=False,
|
||||||
help_section=SECTION_AUTH,
|
help_section=SECTION_AUTH,
|
||||||
help_text="When logging in, ask before logging out of another existing KakaoTalk session, if one exists"
|
help_text="When logging in, ask before logging out of another existing KakaoTalk session, if one exists",
|
||||||
|
aliases=["disable-force-login"],
|
||||||
)
|
)
|
||||||
async def disable_forced_login(evt: CommandEvent) -> None:
|
async def disable_forced_login(evt: CommandEvent) -> None:
|
||||||
if not evt.sender.force_login:
|
if not evt.sender.force_login:
|
||||||
|
|
|
@ -63,7 +63,7 @@ async def ping(evt: CommandEvent) -> None:
|
||||||
if not await evt.sender.is_logged_in():
|
if not await evt.sender.is_logged_in():
|
||||||
await evt.reply("You are **logged out** of KakaoTalk.")
|
await evt.reply("You are **logged out** of KakaoTalk.")
|
||||||
else:
|
else:
|
||||||
is_connected = evt.sender.is_connected and await evt.sender.client.is_connected()
|
is_connected = await evt.sender.is_connected_now()
|
||||||
await evt.reply(
|
await evt.reply(
|
||||||
"You are logged into KakaoTalk.\n\n"
|
"You are logged into KakaoTalk.\n\n"
|
||||||
f"You are {'connected to' if is_connected else '**disconnected** from'} KakaoTalk chats."
|
f"You are {'connected to' if is_connected else '**disconnected** from'} KakaoTalk chats."
|
||||||
|
@ -76,6 +76,7 @@ async def ping(evt: CommandEvent) -> None:
|
||||||
help_section=SECTION_CONNECTION,
|
help_section=SECTION_CONNECTION,
|
||||||
help_text="(Re)connect to KakaoTalk chats & sync any missed chat updates",
|
help_text="(Re)connect to KakaoTalk chats & sync any missed chat updates",
|
||||||
help_args="[_number of channels to sync_]",
|
help_args="[_number of channels to sync_]",
|
||||||
|
aliases=["connect"],
|
||||||
)
|
)
|
||||||
async def sync(evt: CommandEvent) -> None:
|
async def sync(evt: CommandEvent) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Awaitable
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from mautrix.bridge.commands import HelpSection, command_handler
|
from mautrix.bridge.commands import HelpSection, command_handler
|
||||||
from mautrix.types import SerializerError
|
from mautrix.types import Format, SerializerError
|
||||||
from mautrix.util import utf16_surrogate
|
from mautrix.util import utf16_surrogate
|
||||||
from mautrix.util.formatter import (
|
from mautrix.util.formatter import (
|
||||||
EntityString,
|
EntityString,
|
||||||
|
@ -65,9 +65,13 @@ async def whoami(evt: CommandEvent) -> None:
|
||||||
await evt.reply(f"Error from KakaoTalk: {e}")
|
await evt.reply(f"Error from KakaoTalk: {e}")
|
||||||
return
|
return
|
||||||
if own_info:
|
if own_info:
|
||||||
|
puppet = await pu.Puppet.get_by_ktid(evt.sender.ktid)
|
||||||
uuid = f"`{own_info.more.uuid}` ({'searchable' if own_info.more.uuidSearchable else 'hidden'})" if own_info.more.uuid else "_none_"
|
uuid = f"`{own_info.more.uuid}` ({'searchable' if own_info.more.uuidSearchable else 'hidden'})" if own_info.more.uuid else "_none_"
|
||||||
await evt.reply(
|
await evt.reply(
|
||||||
f"You're logged in as **{own_info.more.nickName}** (KakaoTalk ID: {uuid}, internal ID: `{evt.sender.ktid}`)"
|
f"You're logged in as **{own_info.more.nickName}**"
|
||||||
|
f"\n* KakaoTalk ID: {uuid}"
|
||||||
|
f"\n* Internal ID: `{evt.sender.ktid}`"
|
||||||
|
f"\n* Matrix user: [{puppet.name}](https://matrix.to/#/{puppet.default_mxid})"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await evt.reply(
|
await evt.reply(
|
||||||
|
@ -153,13 +157,13 @@ async def _set_id_searchable(evt: CommandEvent, searchable: bool) -> None:
|
||||||
else:
|
else:
|
||||||
await evt.reply(f"Successfully made KakaoTalk ID {'searchable' if searchable else 'hidden'}")
|
await evt.reply(f"Successfully made KakaoTalk ID {'searchable' if searchable else 'hidden'}")
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
puppet = await pu.Puppet.get_by_ktid(friend_struct.userId)
|
puppet = await pu.Puppet.get_by_ktid(friend_struct.userId)
|
||||||
if not puppet.name_set:
|
if not puppet.name_set:
|
||||||
await puppet.update_info_from_friend(source, friend_struct)
|
await puppet.update_info_from_friend(source, friend_struct)
|
||||||
return puppet
|
return puppet
|
||||||
|
|
||||||
|
|
||||||
@command_handler(
|
@command_handler(
|
||||||
needs_auth=True,
|
needs_auth=True,
|
||||||
management_only=False,
|
management_only=False,
|
||||||
|
@ -237,10 +241,10 @@ async def _edit_friend(evt: CommandEvent, add: bool) -> None:
|
||||||
if not evt.args:
|
if not evt.args:
|
||||||
await evt.reply(f"**Usage:** `$cmdprefix+sp {evt.command} <KakaoTalk ID|Matrix user ID>`")
|
await evt.reply(f"**Usage:** `$cmdprefix+sp {evt.command} <KakaoTalk ID|Matrix user ID>`")
|
||||||
return
|
return
|
||||||
formatted_body = evt.content.get("formatted_body")
|
if evt.content.get("format", None) == Format.HTML and evt.content["formatted_body"]:
|
||||||
if formatted_body:
|
parsed = await MentionParser().parse(utf16_surrogate.add(
|
||||||
arg = formatted_body[len(evt.command):].strip()
|
evt.content["formatted_body"][len(evt.command):].strip()
|
||||||
parsed = await MentionParser().parse(utf16_surrogate.add(arg))
|
))
|
||||||
if not parsed.entities:
|
if not parsed.entities:
|
||||||
await evt.reply("No user found")
|
await evt.reply("No user found")
|
||||||
return
|
return
|
||||||
|
@ -311,5 +315,7 @@ async def leave(evt: CommandEvent) -> None:
|
||||||
await evt.reply("This command may only be used in a KakaoTalk channel portal room")
|
await evt.reply("This command may only be used in a KakaoTalk channel portal room")
|
||||||
return
|
return
|
||||||
await evt.mark_read()
|
await evt.mark_read()
|
||||||
await evt.sender.client.leave_channel(evt.portal.channel_props)
|
try:
|
||||||
await evt.sender.on_channel_left(evt.portal.ktid, evt.portal.kt_type)
|
await evt.sender.leave_channel(evt.portal)
|
||||||
|
except CommandException as e:
|
||||||
|
await evt.reply(f"Error from KakaoTalk: {e}")
|
||||||
|
|
|
@ -154,7 +154,11 @@ async def matrix_to_kakaotalk(
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
reply_to = None
|
reply_to = None
|
||||||
if content.get("format", None) == Format.HTML and content["formatted_body"] and content.msgtype.is_text:
|
if (
|
||||||
|
content.get("format", None) == Format.HTML and content["formatted_body"] and
|
||||||
|
content.msgtype.is_text and
|
||||||
|
not portal.is_direct
|
||||||
|
):
|
||||||
parsed = await ToKakaoTalkParser().parse(utf16_surrogate.add(content["formatted_body"]))
|
parsed = await ToKakaoTalkParser().parse(utf16_surrogate.add(content["formatted_body"]))
|
||||||
text = utf16_surrogate.remove(parsed.text)
|
text = utf16_surrogate.remove(parsed.text)
|
||||||
mentions_by_user: dict[Long, MentionStruct] = {}
|
mentions_by_user: dict[Long, MentionStruct] = {}
|
||||||
|
|
|
@ -109,6 +109,10 @@ class Client:
|
||||||
await cls._rpc_client.connect()
|
await cls._rpc_client.connect()
|
||||||
await cls._rpc_client.wait_for_disconnection()
|
await cls._rpc_client.wait_for_disconnection()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def wait_for_connection(cls) -> Awaitable[None]:
|
||||||
|
return cls._rpc_client.wait_for_connection()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def stop_cls(cls) -> None:
|
def stop_cls(cls) -> None:
|
||||||
"""Stop and disconnect from the Node backend."""
|
"""Stop and disconnect from the Node backend."""
|
||||||
|
@ -494,6 +498,7 @@ class Client:
|
||||||
description=description,
|
description=description,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
""" TODO
|
||||||
def set_channel_photo(
|
def set_channel_photo(
|
||||||
self,
|
self,
|
||||||
channel_props: ChannelProps,
|
channel_props: ChannelProps,
|
||||||
|
@ -504,6 +509,7 @@ class Client:
|
||||||
channel_props=channel_props.serialize(),
|
channel_props=channel_props.serialize(),
|
||||||
photo_url=photo_url,
|
photo_url=photo_url,
|
||||||
)
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
def create_direct_chat(self, ktid: Long) -> Awaitable[Long]:
|
def create_direct_chat(self, ktid: Long) -> Awaitable[Long]:
|
||||||
return self._api_user_request_result(
|
return self._api_user_request_result(
|
||||||
|
@ -681,6 +687,9 @@ class Client:
|
||||||
def _on_error(self, data: dict[str, JSON]) -> Awaitable[None]:
|
def _on_error(self, data: dict[str, JSON]) -> Awaitable[None]:
|
||||||
return self.user.on_error(data)
|
return self.user.on_error(data)
|
||||||
|
|
||||||
|
def _on_unexpected_disconnect(self, _: dict[str, JSON]) -> Awaitable[None]:
|
||||||
|
return self.user.on_unexpected_disconnect()
|
||||||
|
|
||||||
|
|
||||||
def _start_listen(self) -> None:
|
def _start_listen(self) -> None:
|
||||||
self._add_event_handler("chat", self._on_chat)
|
self._add_event_handler("chat", self._on_chat)
|
||||||
|
@ -698,6 +707,7 @@ class Client:
|
||||||
self._add_event_handler("disconnected", self._on_listen_disconnect)
|
self._add_event_handler("disconnected", self._on_listen_disconnect)
|
||||||
self._add_event_handler("switch_server", self._on_switch_server)
|
self._add_event_handler("switch_server", self._on_switch_server)
|
||||||
self._add_event_handler("error", self._on_error)
|
self._add_event_handler("error", self._on_error)
|
||||||
|
self._add_event_handler("unexpected_disconnect", self._on_unexpected_disconnect)
|
||||||
|
|
||||||
def _stop_listen(self) -> None:
|
def _stop_listen(self) -> None:
|
||||||
for method in self._handler_methods:
|
for method in self._handler_methods:
|
||||||
|
|
|
@ -96,12 +96,12 @@ class MatrixHandler(BaseMatrixHandler):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
elif (
|
elif (
|
||||||
not await user.is_logged_in()
|
not user.is_connected
|
||||||
and not portal.has_relay
|
and not portal.has_relay
|
||||||
and not self.config["bridge.allow_invites"]
|
and not self.config["bridge.allow_invites"]
|
||||||
):
|
):
|
||||||
await portal.main_intent.kick_user(
|
await portal.main_intent.kick_user(
|
||||||
room_id, user.mxid, "You are not logged in to this KakaoTalk bridge."
|
room_id, user.mxid, "You are not connected to this KakaoTalk bridge."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -1219,9 +1219,6 @@ class Portal(DBPortal, BasePortal):
|
||||||
# Misses should be guarded by supports_state_event, but handle this just in case
|
# Misses should be guarded by supports_state_event, but handle this just in case
|
||||||
self.log.error(f"Skipping Matrix state event {evt.event_id} of unsupported type {evt.type}")
|
self.log.error(f"Skipping Matrix state event {evt.event_id} of unsupported type {evt.type}")
|
||||||
return
|
return
|
||||||
if not self.is_open:
|
|
||||||
self.log.info(f"Not bridging f{handler.action_name} change of portal for non-open channel")
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
effective_sender, _ = await self.get_relay_sender(sender, f"{handler.action_name} {evt.event_id}")
|
effective_sender, _ = await self.get_relay_sender(sender, f"{handler.action_name} {evt.event_id}")
|
||||||
if effective_sender:
|
if effective_sender:
|
||||||
|
@ -1338,6 +1335,9 @@ class Portal(DBPortal, BasePortal):
|
||||||
) -> None:
|
) -> None:
|
||||||
if content.topic == prev_content.topic:
|
if content.topic == prev_content.topic:
|
||||||
return
|
return
|
||||||
|
if not self.is_open:
|
||||||
|
self.log.info(f"Not bridging topic change of portal for non-open channel")
|
||||||
|
return
|
||||||
if not (sender and sender.is_connected):
|
if not (sender and sender.is_connected):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Only users connected to KakaoTalk can set the description of a KakaoTalk channel"
|
"Only users connected to KakaoTalk can set the description of a KakaoTalk channel"
|
||||||
|
@ -1758,19 +1758,20 @@ class Portal(DBPortal, BasePortal):
|
||||||
self, source: u.User, sender: p.Puppet | None, removed: p.Puppet
|
self, source: u.User, sender: p.Puppet | None, removed: p.Puppet
|
||||||
) -> None:
|
) -> None:
|
||||||
sender_intent = sender.intent_for(self) if sender else self.main_intent
|
sender_intent = sender.intent_for(self) if sender else self.main_intent
|
||||||
|
removed_user = await u.User.get_by_ktid(removed.ktid)
|
||||||
if sender == removed:
|
if sender == removed:
|
||||||
removed_intent = removed.intent_for(self)
|
if not removed.is_real_user and removed_user:
|
||||||
if removed_intent != self.main_intent:
|
try:
|
||||||
await removed_intent.leave_room(self.mxid)
|
await sender_intent.kick_user(self.mxid, removed_user.mxid, "Left channel from KakaoTalk")
|
||||||
if not removed.is_real_user:
|
except MForbidden:
|
||||||
user = await u.User.get_by_ktid(removed.ktid)
|
pass
|
||||||
if user:
|
await removed.intent_for(self).leave_room(self.mxid)
|
||||||
await self.main_intent.kick_user(self.mxid, user.mxid, "Left channel from KakaoTalk")
|
|
||||||
else:
|
else:
|
||||||
for removed_mxid in (r.mxid for r in (
|
for removed_mxid in (r.mxid for r in (
|
||||||
removed,
|
removed,
|
||||||
await u.User.get_by_ktid(removed.ktid) if not removed.is_real_user else None
|
removed_user if not removed.is_real_user else None
|
||||||
) if r):
|
) if r):
|
||||||
|
# NOTE KakaoTalk kick = Matrix ban
|
||||||
try:
|
try:
|
||||||
await sender_intent.ban_user(
|
await sender_intent.ban_user(
|
||||||
self.mxid, removed_mxid, None if sender else "Kicked by channel admin"
|
self.mxid, removed_mxid, None if sender else "Kicked by channel admin"
|
||||||
|
@ -1781,7 +1782,11 @@ class Portal(DBPortal, BasePortal):
|
||||||
await self.main_intent.ban_user(
|
await self.main_intent.ban_user(
|
||||||
self.mxid, removed_mxid, reason=f"Kicked by {sender.name}"
|
self.mxid, removed_mxid, reason=f"Kicked by {sender.name}"
|
||||||
)
|
)
|
||||||
# TODO Clean and delete if removed is real user and portal is direct / not open
|
if self.is_direct and removed.ktid == self.kt_receiver:
|
||||||
|
self.log.info(
|
||||||
|
f"{removed.ktid} was the recipient puppet of this portal. Cleaning up and deleting..."
|
||||||
|
)
|
||||||
|
await self.cleanup_and_delete()
|
||||||
|
|
||||||
# TODO Find when or if there is a listener for this
|
# TODO Find when or if there is a listener for this
|
||||||
# TODO Confirm whether this can refer to any user that was kicked, or only to the current user
|
# TODO Confirm whether this can refer to any user that was kicked, or only to the current user
|
||||||
|
@ -1866,7 +1871,7 @@ class Portal(DBPortal, BasePortal):
|
||||||
self.log.trace("Leaving room with %s post-backfill", intent.mxid)
|
self.log.trace("Leaving room with %s post-backfill", intent.mxid)
|
||||||
await intent.leave_room(self.mxid)
|
await intent.leave_room(self.mxid)
|
||||||
self.log.info("Backfilled %d messages through %s", len(chats), source.mxid)
|
self.log.info("Backfilled %d messages through %s", len(chats), source.mxid)
|
||||||
self._sync_read_receipts(source)
|
await self._sync_read_receipts(source)
|
||||||
|
|
||||||
# region Database getters
|
# region Database getters
|
||||||
|
|
||||||
|
@ -1877,7 +1882,7 @@ class Portal(DBPortal, BasePortal):
|
||||||
if not self.is_direct:
|
if not self.is_direct:
|
||||||
self._main_intent = self.az.intent
|
self._main_intent = self.az.intent
|
||||||
else:
|
else:
|
||||||
# TODO Save kt_sender in DB instead? Depends on if DM channels are shared...
|
# TODO Save kt_sender in DB instead? Only do that if keeping a unique DM portal for each receiver
|
||||||
user = await u.User.get_by_ktid(self.kt_receiver)
|
user = await u.User.get_by_ktid(self.kt_receiver)
|
||||||
assert user, f"Found no user for this portal's receiver of {self.kt_receiver}"
|
assert user, f"Found no user for this portal's receiver of {self.kt_receiver}"
|
||||||
if self.kt_type == KnownChannelType.MemoChat:
|
if self.kt_type == KnownChannelType.MemoChat:
|
||||||
|
@ -1916,7 +1921,7 @@ class Portal(DBPortal, BasePortal):
|
||||||
create: bool = True,
|
create: bool = True,
|
||||||
kt_type: ChannelType | None = None,
|
kt_type: ChannelType | None = None,
|
||||||
) -> Portal | None:
|
) -> Portal | None:
|
||||||
# TODO Find out if direct channels are shared. If so, don't need kt_receiver!
|
# TODO Direct chats are shared, so can remove kt_receiver if DM portals should be shared
|
||||||
if kt_type:
|
if kt_type:
|
||||||
kt_receiver = kt_receiver if KnownChannelType.is_direct(kt_type) else 0
|
kt_receiver = kt_receiver if KnownChannelType.is_direct(kt_type) else 0
|
||||||
ktid_full = (ktid, kt_receiver)
|
ktid_full = (ktid, kt_receiver)
|
||||||
|
|
|
@ -247,6 +247,7 @@ class RPCClient:
|
||||||
self.log.debug(f"Nobody waiting for response to {req_id}")
|
self.log.debug(f"Nobody waiting for response to {req_id}")
|
||||||
return
|
return
|
||||||
if command == "response":
|
if command == "response":
|
||||||
|
self.log.debug("Received response %d", req_id)
|
||||||
waiter.set_result(req.get("response"))
|
waiter.set_result(req.get("response"))
|
||||||
elif command == "error":
|
elif command == "error":
|
||||||
waiter.set_exception(RPCError(req.get("error", line)))
|
waiter.set_exception(RPCError(req.get("error", line)))
|
||||||
|
|
|
@ -177,6 +177,9 @@ class User(DBUser, BaseUser):
|
||||||
self._is_connected = val
|
self._is_connected = val
|
||||||
self._connection_time = time.monotonic()
|
self._connection_time = time.monotonic()
|
||||||
|
|
||||||
|
async def is_connected_now(self) -> bool:
|
||||||
|
return self._client is not None and await self._client.is_connected()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connection_time(self) -> float:
|
def connection_time(self) -> float:
|
||||||
return self._connection_time
|
return self._connection_time
|
||||||
|
@ -317,6 +320,8 @@ class User(DBUser, BaseUser):
|
||||||
oauth_credential = await Client.login(uuid=uuid, form=form, forced=True)
|
oauth_credential = await Client.login(uuid=uuid, form=form, forced=True)
|
||||||
except OAuthException as e:
|
except OAuthException as e:
|
||||||
latest_exc = e
|
latest_exc = e
|
||||||
|
else:
|
||||||
|
return False
|
||||||
if oauth_credential:
|
if oauth_credential:
|
||||||
self.oauth_credential = oauth_credential
|
self.oauth_credential = oauth_credential
|
||||||
await self.save()
|
await self.save()
|
||||||
|
@ -388,6 +393,9 @@ class User(DBUser, BaseUser):
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
if not await self._load_session(is_startup=is_startup) and self.has_state:
|
if not await self._load_session(is_startup=is_startup) and self.has_state:
|
||||||
|
self.log.debug("reload_session failure: wait for connection to Node module before prompting for manual login")
|
||||||
|
await Client.wait_for_connection()
|
||||||
|
self.log.debug("reload_session failure: now connected to Node module")
|
||||||
await self.send_bridge_notice(
|
await self.send_bridge_notice(
|
||||||
"Logged out of KakaoTalk. Must use the `login` command to log back in.",
|
"Logged out of KakaoTalk. Must use the `login` command to log back in.",
|
||||||
important=True,
|
important=True,
|
||||||
|
@ -429,7 +437,6 @@ class User(DBUser, BaseUser):
|
||||||
|
|
||||||
async def logout(self, *, remove_ktid: bool = True, reset_device: bool = False) -> None:
|
async def logout(self, *, remove_ktid: bool = True, reset_device: bool = False) -> None:
|
||||||
if self._client:
|
if self._client:
|
||||||
# TODO Look for a logout API call
|
|
||||||
await self._client.stop()
|
await self._client.stop()
|
||||||
if remove_ktid:
|
if remove_ktid:
|
||||||
await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT)
|
await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT)
|
||||||
|
@ -681,7 +688,7 @@ class User(DBUser, BaseUser):
|
||||||
kt_type = KnownChannelType.MemoChat
|
kt_type = KnownChannelType.MemoChat
|
||||||
memo_ids = await self.client.get_memo_ids()
|
memo_ids = await self.client.get_memo_ids()
|
||||||
if not memo_ids:
|
if not memo_ids:
|
||||||
ktid = Long(0)
|
ktid = None
|
||||||
else:
|
else:
|
||||||
ktid = memo_ids[0]
|
ktid = memo_ids[0]
|
||||||
if len(memo_ids) > 1:
|
if len(memo_ids) > 1:
|
||||||
|
@ -698,6 +705,13 @@ class User(DBUser, BaseUser):
|
||||||
ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type
|
ktid, kt_receiver=self.ktid, create=create, kt_type=kt_type
|
||||||
) if ktid else None
|
) if ktid else None
|
||||||
|
|
||||||
|
# region Matrix->KakaoTalk commands
|
||||||
|
|
||||||
|
async def leave_channel(self, portal: po.Portal) -> None:
|
||||||
|
await self.client.leave_channel(portal.channel_props)
|
||||||
|
await self.on_channel_left(portal.ktid, portal.kt_type)
|
||||||
|
|
||||||
|
# endregion
|
||||||
# region KakaoTalk event handling
|
# region KakaoTalk event handling
|
||||||
|
|
||||||
async def on_connect(self, force_sync: bool) -> bool:
|
async def on_connect(self, force_sync: bool) -> bool:
|
||||||
|
@ -746,7 +760,10 @@ class User(DBUser, BaseUser):
|
||||||
reason_suffix = "To reconnect, use the `sync` command."
|
reason_suffix = "To reconnect, use the `sync` command."
|
||||||
else:
|
else:
|
||||||
reason_suffix = "You are now logged out. To log back in, use the `login` command."
|
reason_suffix = "You are now logged out. To log back in, use the `login` command."
|
||||||
await self.send_bridge_notice(f"Disconnected from KakaoTalk: {reason_str} {reason_suffix}")
|
await self.send_bridge_notice(
|
||||||
|
f"Disconnected from KakaoTalk: {reason_str} {reason_suffix}",
|
||||||
|
important=True,
|
||||||
|
)
|
||||||
|
|
||||||
async def on_error(self, error: JSON) -> None:
|
async def on_error(self, error: JSON) -> None:
|
||||||
await self.send_bridge_notice(
|
await self.send_bridge_notice(
|
||||||
|
@ -756,6 +773,22 @@ class User(DBUser, BaseUser):
|
||||||
error_message=str(error),
|
error_message=str(error),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def on_unexpected_disconnect(self) -> None:
|
||||||
|
self.is_connected = False
|
||||||
|
self._track_metric(METRIC_CONNECTED, False)
|
||||||
|
if self.config["bridge.remain_logged_in_on_disconnect"]:
|
||||||
|
# TODO What bridge state to push?
|
||||||
|
self.was_connected = False
|
||||||
|
await self.save()
|
||||||
|
reason_suffix = "To reconnect, use the `sync` command."
|
||||||
|
else:
|
||||||
|
await self.logout()
|
||||||
|
reason_suffix = "You are now logged out. To log back in, use the `login` command."
|
||||||
|
await self.send_bridge_notice(
|
||||||
|
f"Disconnected from KakaoTalk: unexpected error in backend helper module. {reason_suffix}",
|
||||||
|
important=True,
|
||||||
|
)
|
||||||
|
|
||||||
async def on_client_disconnect(self) -> None:
|
async def on_client_disconnect(self) -> None:
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
self._track_metric(METRIC_CONNECTED, False)
|
self._track_metric(METRIC_CONNECTED, False)
|
||||||
|
|
|
@ -2,3 +2,11 @@
|
||||||
If `type` is `unix`, `path` is the path where to create the socket, and `force` is whether to overwrite the socket file if it already exists.
|
If `type` is `unix`, `path` is the path where to create the socket, and `force` is whether to overwrite the socket file if it already exists.
|
||||||
|
|
||||||
If `type` is `tcp`, `port` and `host` are the host/port where to listen.
|
If `type` is `tcp`, `port` and `host` are the host/port where to listen.
|
||||||
|
|
||||||
|
### Register timeout config
|
||||||
|
`register_timeout` is the amount of time (in milliseconds) that a connecting peer must send a "register" command after initiating a connection.
|
||||||
|
|
||||||
|
### Logging config
|
||||||
|
`logging_keys` specifies which properties of RPC request & response objects to print in logs. Optional.
|
||||||
|
|
||||||
|
A special-case logging key for responses is `value`, which enables logging responses that are primitives instead of objects.
|
||||||
|
|
|
@ -3,5 +3,10 @@
|
||||||
"type": "unix",
|
"type": "unix",
|
||||||
"path": "/data/rpc.sock",
|
"path": "/data/rpc.sock",
|
||||||
"force": true
|
"force": true
|
||||||
|
},
|
||||||
|
"register_timeout": 3000,
|
||||||
|
"logging_keys": {
|
||||||
|
"request": ["mxid"],
|
||||||
|
"response": ["status"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,10 @@
|
||||||
"type": "unix",
|
"type": "unix",
|
||||||
"path": "/var/run/matrix-appservice-kakaotalk/rpc.sock",
|
"path": "/var/run/matrix-appservice-kakaotalk/rpc.sock",
|
||||||
"force": false
|
"force": false
|
||||||
|
},
|
||||||
|
"register_timeout": 3000,
|
||||||
|
"logging_keys": {
|
||||||
|
"request": ["mxid"],
|
||||||
|
"response": ["status"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ const { KnownChatType } = chat
|
||||||
import { emitLines, promisify } from "./util.js"
|
import { emitLines, promisify } from "./util.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} ChannelProps
|
* @typedef {object} ChannelProps
|
||||||
* @property {Long} id
|
* @property {Long} id
|
||||||
* @property {ChannelType} type
|
* @property {ChannelType} type
|
||||||
*/
|
*/
|
||||||
|
@ -80,6 +80,7 @@ class PermError extends ProtocolError {
|
||||||
/**
|
/**
|
||||||
* @param {?OpenChannelUserPerm[]} permNeeded
|
* @param {?OpenChannelUserPerm[]} permNeeded
|
||||||
* @param {?OpenChannelUserPerm} permActual
|
* @param {?OpenChannelUserPerm} permActual
|
||||||
|
* @param {string} action
|
||||||
*/
|
*/
|
||||||
constructor(permNeeded, permActual, action) {
|
constructor(permNeeded, permActual, action) {
|
||||||
const who =
|
const who =
|
||||||
|
@ -97,6 +98,7 @@ class PermError extends ProtocolError {
|
||||||
class UserClient {
|
class UserClient {
|
||||||
static #initializing = false
|
static #initializing = false
|
||||||
|
|
||||||
|
#connected = false
|
||||||
#talkClient = new TalkClient()
|
#talkClient = new TalkClient()
|
||||||
get talkClient() { return this.#talkClient }
|
get talkClient() { return this.#talkClient }
|
||||||
|
|
||||||
|
@ -155,10 +157,19 @@ class UserClient {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.#talkClient.on("chat_read", (chat, channel, reader) => {
|
this.#talkClient.on("chat_read", (chat, channel, reader) => {
|
||||||
this.log(`Chat ${chat.logId} read in channel ${channel.channelId} by ${reader.userId}`)
|
let senderId
|
||||||
|
if (reader) {
|
||||||
|
senderId = reader.userId
|
||||||
|
} else if (channel.info.type == "MemoChat") {
|
||||||
|
senderId = channel.clientUser.userId
|
||||||
|
} else {
|
||||||
|
this.error(`Chat ${chat.logId} read in channel ${channel.channelId} by unknown reader (channel type: ${channel.info.type || "none"})`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.log(`Chat ${chat.logId} read in channel ${channel.channelId} by ${senderId}`)
|
||||||
this.write("chat_read", {
|
this.write("chat_read", {
|
||||||
chatId: chat.logId,
|
chatId: chat.logId,
|
||||||
senderId: reader.userId,
|
senderId: senderId,
|
||||||
channelId: channel.channelId,
|
channelId: channel.channelId,
|
||||||
channelType: channel.info.type,
|
channelType: channel.info.type,
|
||||||
})
|
})
|
||||||
|
@ -298,7 +309,7 @@ class UserClient {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.#talkClient.on("disconnected", (reason) => {
|
this.#talkClient.on("disconnected", reason => {
|
||||||
this.log(`Disconnected (reason=${reason})`)
|
this.log(`Disconnected (reason=${reason})`)
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
this.write("disconnected", {
|
this.write("disconnected", {
|
||||||
|
@ -313,7 +324,7 @@ class UserClient {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
this.#talkClient.on("error", (err) => {
|
this.#talkClient.on("error", err => {
|
||||||
this.log(`Client error: ${err}`)
|
this.log(`Client error: ${err}`)
|
||||||
this.write("error", {
|
this.write("error", {
|
||||||
error: err,
|
error: err,
|
||||||
|
@ -355,20 +366,39 @@ class UserClient {
|
||||||
* @param {ChannelProps} channelProps
|
* @param {ChannelProps} channelProps
|
||||||
*/
|
*/
|
||||||
async getChannel(channelProps) {
|
async getChannel(channelProps) {
|
||||||
let channel = this.#talkClient.channelList.get(channelProps.id)
|
const talkChannel = this.#talkClient.channelList.get(channelProps.id)
|
||||||
if (channel) {
|
if (talkChannel) {
|
||||||
return channel
|
return talkChannel
|
||||||
} else {
|
|
||||||
const channelList = getChannelListForType(
|
|
||||||
this.#talkClient.channelList,
|
|
||||||
channelProps.type
|
|
||||||
)
|
|
||||||
const res = await channelList.addChannel({ channelId: channelProps.id })
|
|
||||||
if (!res.success) {
|
|
||||||
throw new Error(`Unable to add ${channelProps.type} channel ${channelProps.id}`)
|
|
||||||
}
|
|
||||||
return res.result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const channelList = getChannelListForType(
|
||||||
|
this.#talkClient.channelList,
|
||||||
|
channelProps.type
|
||||||
|
)
|
||||||
|
const res = await channelList.addChannel({ channelId: channelProps.id })
|
||||||
|
if (!res.success) {
|
||||||
|
this.error(`Unable to add ${channelProps.type} channel ${channelProps.id}`)
|
||||||
|
throw res
|
||||||
|
}
|
||||||
|
return res.result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Long} channelId
|
||||||
|
*/
|
||||||
|
async getNormalChannel(channelId) {
|
||||||
|
const channelList = this.#talkClient.channelList.normal
|
||||||
|
const talkChannel = channelList.get(channelId)
|
||||||
|
if (talkChannel) {
|
||||||
|
return talkChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await channelList.addChannel({ channelId: channelId })
|
||||||
|
if (!res.success) {
|
||||||
|
this.error(`Unable to add normal channel ${channelProps.id}`)
|
||||||
|
throw res
|
||||||
|
}
|
||||||
|
return res.result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,19 +410,26 @@ class UserClient {
|
||||||
if (credential && this.#credential != credential) {
|
if (credential && this.#credential != credential) {
|
||||||
await this.setCredential(credential)
|
await this.setCredential(credential)
|
||||||
}
|
}
|
||||||
return await this.#talkClient.login(this.#credential)
|
const res = await this.#talkClient.login(this.#credential)
|
||||||
|
this.#connected = res.success
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.#talkClient.logon) {
|
if (this.isConnected()) {
|
||||||
this.#talkClient.close()
|
this.#talkClient.close()
|
||||||
}
|
}
|
||||||
|
this.#connected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnected() {
|
isConnected() {
|
||||||
return this.#talkClient?.logon || false
|
return this.#talkClient?.logon || false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isUnexpectedlyDisconnected() {
|
||||||
|
return this.#connected && !this.isConnected()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a user-specific command with (optional) data to the socket.
|
* Send a user-specific command with (optional) data to the socket.
|
||||||
*
|
*
|
||||||
|
@ -416,15 +453,17 @@ export default class PeerClient {
|
||||||
*/
|
*/
|
||||||
constructor(manager, socket, connID) {
|
constructor(manager, socket, connID) {
|
||||||
this.manager = manager
|
this.manager = manager
|
||||||
|
this.registerTimeout = manager.registerTimeout
|
||||||
|
this.loggingKeys = manager.loggingKeys
|
||||||
|
|
||||||
this.socket = socket
|
this.socket = socket
|
||||||
this.connID = connID
|
this.connID = connID
|
||||||
|
|
||||||
this.stopped = false
|
this.stopped = false
|
||||||
this.notificationID = 0
|
this.notificationID = 0
|
||||||
this.maxCommandID = 0
|
this.maxCommandID = 0
|
||||||
this.peerID = null
|
this.peerID = ""
|
||||||
this.deviceName = "KakaoTalk Bridge"
|
this.deviceName = "KakaoTalk Bridge"
|
||||||
|
|
||||||
/** @type {Map<string, UserClient>} */
|
/** @type {Map<string, UserClient>} */
|
||||||
this.userClients = new Map()
|
this.userClients = new Map()
|
||||||
}
|
}
|
||||||
|
@ -455,10 +494,10 @@ export default class PeerClient {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.peerID && !this.stopped) {
|
if (!this.peerID && !this.stopped) {
|
||||||
this.log("Didn't receive register request within 3 seconds, terminating")
|
this.log(`Didn't receive register request within ${this.registerTimeout/1000} seconds, terminating`)
|
||||||
this.stop("Register request timeout")
|
this.stop("Register request timeout")
|
||||||
}
|
}
|
||||||
}, 3000)
|
}, this.registerTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(error = null) {
|
async stop(error = null) {
|
||||||
|
@ -482,11 +521,11 @@ export default class PeerClient {
|
||||||
if (this.peerID && this.manager.clients.get(this.peerID) === this) {
|
if (this.peerID && this.manager.clients.get(this.peerID) === this) {
|
||||||
this.manager.clients.delete(this.peerID)
|
this.manager.clients.delete(this.peerID)
|
||||||
}
|
}
|
||||||
this.log(`Connection closed (peer: ${this.peerID})`)
|
this.log(`Connection closed (peer: ${this.peerID || "unknown peer"})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
#closeUsers() {
|
#closeUsers() {
|
||||||
this.log("Closing all API clients for", this.peerID)
|
this.log(`Closing all API clients for ${this.peerID || "unknown peer"}`)
|
||||||
for (const userClient of this.userClients.values()) {
|
for (const userClient of this.userClients.values()) {
|
||||||
userClient.disconnect()
|
userClient.disconnect()
|
||||||
}
|
}
|
||||||
|
@ -504,10 +543,10 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.passcode
|
* @param {string} req.passcode
|
||||||
* @param {string} req.uuid
|
* @param {string} req.uuid
|
||||||
* @param {Object} req.form
|
* @param {object} req.form
|
||||||
*/
|
*/
|
||||||
registerDevice = async (req) => {
|
registerDevice = async (req) => {
|
||||||
// TODO Look for a deregister API call
|
// TODO Look for a deregister API call
|
||||||
|
@ -517,16 +556,15 @@ export default class PeerClient {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtain login tokens. If this fails due to not having a device, also request a device passcode.
|
* Obtain login tokens. If this fails due to not having a device, also request a device passcode.
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.uuid
|
* @param {string} req.uuid
|
||||||
* @param {Object} req.form
|
* @param {object} req.form
|
||||||
* @param {boolean} req.forced
|
* @param {boolean} req.forced
|
||||||
* @returns The response of the login attempt, including obtained
|
* @returns The response of the login attempt, including obtained
|
||||||
* credentials for subsequent token-based login. If a required device passcode
|
* credentials for subsequent token-based login. If a required device passcode
|
||||||
* request failed, its status is stored here.
|
* request failed, its status is stored here.
|
||||||
*/
|
*/
|
||||||
handleLogin = async (req) => {
|
handleLogin = async (req) => {
|
||||||
// TODO Look for a logout API call
|
|
||||||
const authClient = await this.#createAuthClient(req.uuid)
|
const authClient = await this.#createAuthClient(req.uuid)
|
||||||
const loginRes = await authClient.login(req.form, req.forced)
|
const loginRes = await authClient.login(req.form, req.forced)
|
||||||
if (loginRes.status === KnownAuthStatusCode.DEVICE_NOT_REGISTERED) {
|
if (loginRes.status === KnownAuthStatusCode.DEVICE_NOT_REGISTERED) {
|
||||||
|
@ -574,23 +612,41 @@ export default class PeerClient {
|
||||||
* @param {string} mxid
|
* @param {string} mxid
|
||||||
* @param {ChannelProps} channelProps
|
* @param {ChannelProps} channelProps
|
||||||
* @param {?OpenChannelUserPerm[]} permNeeded If set, throw if the user's permission level matches none of the values in this list.
|
* @param {?OpenChannelUserPerm[]} permNeeded If set, throw if the user's permission level matches none of the values in this list.
|
||||||
* @param {?string} action The action requiring permission, to be used in an error message if throwing..
|
* @param {?string} action The action requiring permission, to be used in an error message if throwing.
|
||||||
* @throws {PermError} if the user does not have the specified permission level.
|
* @throws {PermError} if the user does not have the specified permission level.
|
||||||
*/
|
*/
|
||||||
async #getUserChannel(mxid, channelProps, permNeeded, action) {
|
async #getUserChannel(mxid, channelProps, permNeeded, action) {
|
||||||
const userClient = this.#getUser(mxid)
|
const userClient = this.#getUser(mxid)
|
||||||
const talkChannel = await userClient.getChannel(channelProps)
|
const talkChannel = await userClient.getChannel(channelProps)
|
||||||
if (permNeeded) {
|
if (permNeeded) {
|
||||||
const permActual = talkChannel.getUserInfo({ userId: userClient.userId }).perm
|
await this.#requireChannelPerm(talkChannel, permNeeded, action)
|
||||||
if (permNeeded.indexOf(permActual) == -1) {
|
|
||||||
throw new PermError(permNeeded, permActual, action)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return talkChannel
|
return talkChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {TalkOpenChannel} talkChannel
|
||||||
|
* @param {OpenChannelUserPerm[]} permNeeded Throw if the user's permission level matches none of the values in this list.
|
||||||
|
* @param {string} action The action requiring permission
|
||||||
|
* @throws {PermError} if the user does not have the specified permission level.
|
||||||
|
*/
|
||||||
|
async #requireChannelPerm(talkChannel, permNeeded, action) {
|
||||||
|
const permActual = talkChannel.getUserInfo({ userId: talkChannel.clientUser.userId }).perm
|
||||||
|
if (permNeeded.indexOf(permActual) == -1) {
|
||||||
|
throw new PermError(permNeeded, permActual, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} mxid
|
||||||
|
* @param {Long} channelId
|
||||||
|
*/
|
||||||
|
async #getUserNormalChannel(mxid, channelId) {
|
||||||
|
return await this.#getUser(mxid).getNormalChannel(channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {OAuthCredential} req.oauth_credential
|
* @param {OAuthCredential} req.oauth_credential
|
||||||
*/
|
*/
|
||||||
|
@ -605,7 +661,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {OAuthCredential} req.oauth_credential
|
* @param {OAuthCredential} req.oauth_credential
|
||||||
*/
|
*/
|
||||||
|
@ -624,7 +680,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
*/
|
*/
|
||||||
userStop = async (req) => {
|
userStop = async (req) => {
|
||||||
|
@ -633,7 +689,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {?OAuthCredential} req.oauth_credential
|
* @param {?OAuthCredential} req.oauth_credential
|
||||||
*/
|
*/
|
||||||
|
@ -642,7 +698,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
*/
|
*/
|
||||||
handleDisconnect = (req) => {
|
handleDisconnect = (req) => {
|
||||||
|
@ -650,7 +706,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
*/
|
*/
|
||||||
isConnected = (req) => {
|
isConnected = (req) => {
|
||||||
|
@ -658,7 +714,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
*/
|
*/
|
||||||
getSettings = async (req) => {
|
getSettings = async (req) => {
|
||||||
|
@ -682,7 +738,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
*/
|
*/
|
||||||
getOwnProfile = async (req) => {
|
getOwnProfile = async (req) => {
|
||||||
|
@ -690,7 +746,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {Long} req.user_id
|
* @param {Long} req.user_id
|
||||||
*/
|
*/
|
||||||
|
@ -699,7 +755,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
*/
|
*/
|
||||||
|
@ -723,7 +779,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
*/
|
*/
|
||||||
|
@ -757,7 +813,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
*/
|
*/
|
||||||
|
@ -767,7 +823,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {?Long} req.sync_from
|
* @param {?Long} req.sync_from
|
||||||
|
@ -794,7 +850,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {[Long]} req.unread_chat_ids Must be in DECREASING order
|
* @param {[Long]} req.unread_chat_ids Must be in DECREASING order
|
||||||
|
@ -824,7 +880,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Receipt
|
* @typedef {object} Receipt
|
||||||
* @property {Long} userId
|
* @property {Long} userId
|
||||||
* @property {Long} chatId
|
* @property {Long} chatId
|
||||||
*/
|
*/
|
||||||
|
@ -835,7 +891,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {string} req.uuid
|
* @param {string} req.uuid
|
||||||
*/
|
*/
|
||||||
|
@ -844,7 +900,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {string} req.uuid
|
* @param {string} req.uuid
|
||||||
*/
|
*/
|
||||||
|
@ -858,7 +914,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {boolean} req.searchable
|
* @param {boolean} req.searchable
|
||||||
*/
|
*/
|
||||||
|
@ -880,7 +936,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
*/
|
*/
|
||||||
listFriends = async (req) => {
|
listFriends = async (req) => {
|
||||||
|
@ -888,7 +944,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {Long} req.user_id
|
* @param {Long} req.user_id
|
||||||
* @param {boolean} req.add
|
* @param {boolean} req.add
|
||||||
|
@ -902,7 +958,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {string} req.uuid
|
* @param {string} req.uuid
|
||||||
* @param {boolean} req.add
|
* @param {boolean} req.add
|
||||||
|
@ -939,7 +995,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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.
|
||||||
* @param {string} req.friend_id The friend to search for.
|
* @param {string} req.friend_id The friend to search for.
|
||||||
* @param {string} propertyName The property to retrieve from the specified friend.
|
* @param {string} propertyName The property to retrieve from the specified friend.
|
||||||
|
@ -952,7 +1008,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
*/
|
*/
|
||||||
getMemoIds = (req) => {
|
getMemoIds = (req) => {
|
||||||
|
@ -969,7 +1025,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {string} req.key
|
* @param {string} req.key
|
||||||
|
@ -984,7 +1040,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {string} req.text
|
* @param {string} req.text
|
||||||
|
@ -1003,7 +1059,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {int} req.type
|
* @param {int} req.type
|
||||||
|
@ -1027,7 +1083,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {Long} req.chat_id
|
* @param {Long} req.chat_id
|
||||||
|
@ -1041,7 +1097,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {Long} req.read_until_chat_id
|
* @param {Long} req.read_until_chat_id
|
||||||
|
@ -1055,7 +1111,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {Long} req.user_id
|
* @param {Long} req.user_id
|
||||||
|
@ -1072,50 +1128,69 @@ export default class PeerClient {
|
||||||
if (!talkChannel.getUserInfo(user)) {
|
if (!talkChannel.getUserInfo(user)) {
|
||||||
throw new ProtocolError("Cannot set permission level of a user that is not a channel participant")
|
throw new ProtocolError("Cannot set permission level of a user that is not a channel participant")
|
||||||
}
|
}
|
||||||
if (req.user_id == talkChannel.clientUser.userId) {
|
if (req.user_id.equals(talkChannel.clientUser.userId)) {
|
||||||
throw new ProtocolError("Cannot change own permission level")
|
throw new ProtocolError("Cannot change own permission level")
|
||||||
}
|
}
|
||||||
return await talkChannel.setUserPerm(user, req.perm)
|
return await talkChannel.setUserPerm(user, req.perm)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {string} req.name
|
* @param {string} req.name
|
||||||
*/
|
*/
|
||||||
setChannelName = async (req) => {
|
setChannelName = async (req) => {
|
||||||
const talkChannel = await this.#getUserChannel(
|
if (!isChannelTypeOpen(req.channel_props.type)) {
|
||||||
req.mxid,
|
const talkChannel = await this.#getUserNormalChannel(req.mxid, req.channel_props.id)
|
||||||
req.channel_props,
|
return await talkChannel.setTitleMeta(req.name)
|
||||||
[OpenChannelUserPerm.OWNER],
|
} else {
|
||||||
"change channel name"
|
return await this.#setOpenChannelProperty(req.mxid, req.channel_props, "linkName", req.name)
|
||||||
)
|
}
|
||||||
return await talkChannel.setTitleMeta(req.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {string} req.description
|
* @param {string} req.description
|
||||||
*/
|
*/
|
||||||
setChannelDescription = async (req) => {
|
setChannelDescription = async (req) => {
|
||||||
const talkChannel = await this.#getUserChannel(
|
return await this.#setOpenChannelProperty(req.mxid, req.channel_props, "description", req.description)
|
||||||
req.mxid,
|
|
||||||
req.channel_props,
|
|
||||||
[OpenChannelUserPerm.OWNER],
|
|
||||||
"change channel description"
|
|
||||||
)
|
|
||||||
return await talkChannel.setNoticeMeta(req.description)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {string} mxid
|
||||||
|
* @param {ChannelProps} channelProps
|
||||||
|
* @param {string} propertyName
|
||||||
|
* @param {any} propertyValue
|
||||||
|
*/
|
||||||
|
async #setOpenChannelProperty(mxid, channelProps, propertyName, propertyValue) {
|
||||||
|
if (isChannelTypeOpen(channelProps)) {
|
||||||
|
throw ProtocolError(`Cannot set ${propertyName} of non-open channel ${channelProps.id} (type = ${channelProps.type})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userClient = this.#getUser(mxid)
|
||||||
|
/** @type {TalkOpenChannel} */
|
||||||
|
const talkChannel = await userClient.getChannel(channelProps)
|
||||||
|
this.#requireChannelPerm(talkChannel, [OpenChannelUserPerm.OWNER], `change channel ${propertyName}`)
|
||||||
|
|
||||||
|
const linkRes = await talkChannel.getLatestOpenLink()
|
||||||
|
if (!linkRes.success) throw linkRes
|
||||||
|
|
||||||
|
const link = linkRes.result
|
||||||
|
link[propertyName] = propertyValue
|
||||||
|
return await userClient.talkClient.channelList.open.updateOpenLink(
|
||||||
|
{ linkId: link.linkId }, link
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO
|
||||||
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
* @param {string} req.photo_url
|
* @param {string} req.photo_url
|
||||||
*/
|
|
||||||
setChannelPhoto = async (req) => {
|
setChannelPhoto = async (req) => {
|
||||||
const talkChannel = await this.#getUserChannel(
|
const talkChannel = await this.#getUserChannel(
|
||||||
req.mxid,
|
req.mxid,
|
||||||
|
@ -1128,23 +1203,59 @@ export default class PeerClient {
|
||||||
fullImageUrl: req.photo_url,
|
fullImageUrl: req.photo_url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {Long} req.user_id
|
* @param {Long} req.user_id
|
||||||
*/
|
*/
|
||||||
createDirectChat = async (req) => {
|
createDirectChat = async (req) => {
|
||||||
const channelList = this.#getUser(req.mxid).talkClient.channelList.normal
|
const userClient = this.#getUser(req.mxid)
|
||||||
const res = await channelList.createChannel({
|
await this.#requireInFriendsList(userClient.serviceClient, req.user_id)
|
||||||
userList: [{ userId: req.user_id }],
|
|
||||||
})
|
const channelList = userClient.talkClient.channelList.normal
|
||||||
if (!res.success) return res
|
const createChannel =
|
||||||
return makeCommandResult(res.result.channelId)
|
!req.user_id.equals(userClient.userId)
|
||||||
|
? channelList.createChannel.bind(channelList, {
|
||||||
|
userList: [{ userId: req.user_id }],
|
||||||
|
})
|
||||||
|
: channelList.createMemoChannel.bind(channelList)
|
||||||
|
|
||||||
|
const retry_delay = 2000
|
||||||
|
let retries_left = 1
|
||||||
|
let res
|
||||||
|
do {
|
||||||
|
res = await createChannel()
|
||||||
|
if (res.success) {
|
||||||
|
return makeCommandResult(res.result.channelId)
|
||||||
|
}
|
||||||
|
this.error(`Failed to create direct chat, try again in ${retry_delay} ms (${retries_left - 1} tries remaining)`)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retry_delay))
|
||||||
|
} while (retries_left--)
|
||||||
|
|
||||||
|
this.error(`Failed to create direct chat, not retrying`)
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {ServiceApiClient} serviceClient
|
||||||
|
* @param {Long} id
|
||||||
|
*/
|
||||||
|
async #requireInFriendsList(serviceClient, id) {
|
||||||
|
let listRes = await serviceClient.requestFriendList()
|
||||||
|
if (!listRes.success) {
|
||||||
|
this.error("Failed to check friends list")
|
||||||
|
throw listRes
|
||||||
|
}
|
||||||
|
const isFriend = -1 != listRes.result.friends.findIndex(friend => id.equals(friend.userId))
|
||||||
|
if (!isFriend) {
|
||||||
|
throw new ProtocolError("This user is not in your friends list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} req
|
||||||
* @param {string} req.mxid
|
* @param {string} req.mxid
|
||||||
* @param {ChannelProps} req.channel_props
|
* @param {ChannelProps} req.channel_props
|
||||||
*/
|
*/
|
||||||
|
@ -1162,9 +1273,9 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} req
|
* @param {object} req
|
||||||
* @param {string} req.peer_id
|
* @param {string} req.peer_id
|
||||||
* @param {Object} req.register_config
|
* @param {object} req.register_config
|
||||||
* @param {string} req.register_config.device_name
|
* @param {string} req.register_config.device_name
|
||||||
*/
|
*/
|
||||||
handleRegister = async (req) => {
|
handleRegister = async (req) => {
|
||||||
|
@ -1200,7 +1311,7 @@ export default class PeerClient {
|
||||||
this.log("Ignoring old request", req.id)
|
this.log("Ignoring old request", req.id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.log("Received request", req.id, "with command", req.command)
|
this.log(`Request ${req.id}:`, this.#logObj(req, req.command, this.loggingKeys.request))
|
||||||
this.maxCommandID = req.id
|
this.maxCommandID = req.id
|
||||||
let handler
|
let handler
|
||||||
if (!this.peerID) {
|
if (!this.peerID) {
|
||||||
|
@ -1250,7 +1361,7 @@ export default class PeerClient {
|
||||||
send_perm: this.sendPerm,
|
send_perm: this.sendPerm,
|
||||||
set_channel_name: this.setChannelName,
|
set_channel_name: this.setChannelName,
|
||||||
set_channel_description: this.setChannelDescription,
|
set_channel_description: this.setChannelDescription,
|
||||||
set_channel_photo: this.setChannelPhoto,
|
//set_channel_photo: this.setChannelPhoto,
|
||||||
create_direct_chat: this.createDirectChat,
|
create_direct_chat: this.createDirectChat,
|
||||||
leave_channel: this.leaveChannel,
|
leave_channel: this.leaveChannel,
|
||||||
}[req.command] || this.handleUnknownCommand
|
}[req.command] || this.handleUnknownCommand
|
||||||
|
@ -1267,21 +1378,59 @@ export default class PeerClient {
|
||||||
success: false,
|
success: false,
|
||||||
status: err.response.status,
|
status: err.response.status,
|
||||||
}
|
}
|
||||||
|
} else if ("status" in err) {
|
||||||
|
resp.response = err
|
||||||
} else {
|
} else {
|
||||||
resp.command = "error"
|
resp.command = "error"
|
||||||
|
let errorDetails
|
||||||
if (err instanceof ProtocolError) {
|
if (err instanceof ProtocolError) {
|
||||||
resp.error = err.message
|
resp.error = err.message
|
||||||
} else {
|
errorDetails = err.message
|
||||||
|
} else if (err instanceof Error) {
|
||||||
resp.error = err.toString()
|
resp.error = err.toString()
|
||||||
this.log(`Error handling request ${resp.id} ${err.stack}`)
|
errorDetails = err.stack
|
||||||
|
} else {
|
||||||
|
resp.error = JSON.stringify(err)
|
||||||
|
errorDetails = `throwed ${resp.error}`
|
||||||
}
|
}
|
||||||
|
this.error(`Response ${resp.id}: ${errorDetails}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO Check if session is broken. If it is, close the PeerClient
|
if (resp.response) {
|
||||||
|
const success = resp.response.success !== false
|
||||||
|
const logger = (success ? this.log : this.error).bind(this)
|
||||||
|
logger(
|
||||||
|
`Response ${resp.id}:`,
|
||||||
|
this.#logObj(
|
||||||
|
resp.response instanceof Object ? resp.response : {value: resp.response},
|
||||||
|
success ? "success" : "failure",
|
||||||
|
this.loggingKeys.response
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
await this.write(resp)
|
await this.write(resp)
|
||||||
|
if ("mxid" in req) {
|
||||||
|
const userClient = this.#tryGetUser(req.mxid)
|
||||||
|
if (userClient && userClient.isUnexpectedlyDisconnected()) {
|
||||||
|
this.error("Unexpected disconnect for user", req.mxid)
|
||||||
|
this.userClients.delete(req.mxid)
|
||||||
|
await userClient.write("unexpected_disconnect")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#writeReplacer = function(key, value) {
|
/**
|
||||||
|
* @param {object} obj
|
||||||
|
* @param {string} desc
|
||||||
|
* @param {[string]} keys
|
||||||
|
*/
|
||||||
|
#logObj(obj, desc, keys) {
|
||||||
|
return [desc].concat(
|
||||||
|
keys.filter(key => key in obj).map(key => `${key}: ${JSON.stringify(obj[key], this.#writeReplacer)}`)
|
||||||
|
).join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
#writeReplacer(key, value) {
|
||||||
if (value instanceof Long) {
|
if (value instanceof Long) {
|
||||||
return value.toString()
|
return value.toString()
|
||||||
} else {
|
} else {
|
||||||
|
@ -1289,7 +1438,7 @@ export default class PeerClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#readReviver = function(key, value) {
|
#readReviver(key, value) {
|
||||||
if (value instanceof Object) {
|
if (value instanceof Object) {
|
||||||
// TODO Use a type map if there will be many possible types
|
// TODO Use a type map if there will be many possible types
|
||||||
if (value.__type__ == "Long") {
|
if (value.__type__ == "Long") {
|
||||||
|
|
|
@ -20,10 +20,33 @@ import path from "path"
|
||||||
import PeerClient from "./client.js"
|
import PeerClient from "./client.js"
|
||||||
import { promisify } from "./util.js"
|
import { promisify } from "./util.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ListenConfig
|
||||||
|
* @property {string} type
|
||||||
|
* @property {string} path
|
||||||
|
* @property {boolean} force
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} LoggingKeys
|
||||||
|
* @property {[string]} request
|
||||||
|
* @property {[string]} response
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
export default class ClientManager {
|
export default class ClientManager {
|
||||||
constructor(listenConfig) {
|
/**
|
||||||
|
* @param {ListenConfig} listenConfig
|
||||||
|
* @param {number} registerTimeout
|
||||||
|
* @param {?LoggingKeys} loggingKeys
|
||||||
|
*/
|
||||||
|
constructor(listenConfig, registerTimeout, loggingKeys) {
|
||||||
|
if (!listenConfig) {
|
||||||
|
throw new Error("Listen config missing")
|
||||||
|
}
|
||||||
this.listenConfig = listenConfig
|
this.listenConfig = listenConfig
|
||||||
|
this.registerTimeout = registerTimeout || 3000
|
||||||
|
this.loggingKeys = loggingKeys || {request: [], response: []}
|
||||||
this.server = net.createServer(this.acceptConnection)
|
this.server = net.createServer(this.acceptConnection)
|
||||||
this.connections = []
|
this.connections = []
|
||||||
this.clients = new Map()
|
this.clients = new Map()
|
||||||
|
@ -42,7 +65,7 @@ export default class ClientManager {
|
||||||
} else {
|
} else {
|
||||||
const connID = this.connIDSequence++
|
const connID = this.connIDSequence++
|
||||||
this.connections[connID] = sock
|
this.connections[connID] = sock
|
||||||
new PeerClient(this, sock, connID).start()
|
new PeerClient(this, sock, connID, this.loggingKeys).start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,11 @@ const configPath = args["--config"] || "config.json"
|
||||||
console.log("[Main] Reading config from", configPath)
|
console.log("[Main] Reading config from", configPath)
|
||||||
const config = JSON.parse(fs.readFileSync(configPath).toString())
|
const config = JSON.parse(fs.readFileSync(configPath).toString())
|
||||||
|
|
||||||
const manager = new ClientManager(config.listen)
|
const manager = new ClientManager(
|
||||||
|
config.listen,
|
||||||
|
config.register_timeout,
|
||||||
|
config.logging_keys
|
||||||
|
)
|
||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
manager.stop().then(() => {
|
manager.stop().then(() => {
|
||||||
|
|
Loading…
Reference in New Issue