diff --git a/matrix_appservice_kakaotalk/commands/conn.py b/matrix_appservice_kakaotalk/commands/conn.py
index db78e12..2799c19 100644
--- a/matrix_appservice_kakaotalk/commands/conn.py
+++ b/matrix_appservice_kakaotalk/commands/conn.py
@@ -14,6 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
from mautrix.bridge.commands import HelpSection, command_handler
+from mautrix.types import SerializerError
from .typehint import CommandEvent
@@ -53,24 +54,36 @@ async def disconnect(evt: CommandEvent) -> None:
needs_auth=True,
management_only=True,
help_section=SECTION_CONNECTION,
- help_text="Check if you're logged into KakaoTalk & connected to chats",
+ help_text="Check if you're logged into KakaoTalk and retrieve your account information",
)
-async def ping(evt: CommandEvent) -> None:
- if not await evt.sender.is_logged_in():
- await evt.reply("You're not logged into KakaoTalk")
- return
+async def whoami(evt: CommandEvent) -> None:
await evt.mark_read()
try:
own_info = await evt.sender.get_own_info()
await evt.reply(
- f"You're logged in as {own_info.nickname} (user ID {evt.sender.ktid})."
- "\n\n"
- f"You are {'connected to' if evt.sender.is_connected else '**disconnected** from'} KakaoTalk chats.\n\n"
+ f"You're logged in as `{own_info.more.uuid}` (nickname: {own_info.more.nickName}, user ID: {evt.sender.ktid})."
+ )
+ except SerializerError:
+ evt.sender.log.exception("Failed to deserialize settings struct")
+ await evt.reply(
+ f"You're logged in, but the bridge is unable to retrieve your profile information (user ID: {evt.sender.ktid})."
)
except CommandException as e:
await evt.reply(f"Error from KakaoTalk: {e}")
+@command_handler(
+ needs_auth=True,
+ management_only=True,
+ help_section=SECTION_CONNECTION,
+ help_text="Check if you're connected to KakaoTalk chats",
+)
+async def ping(evt: CommandEvent) -> None:
+ await evt.reply(
+ f"You are {'connected to' if evt.sender.is_connected else '**disconnected** from'} KakaoTalk chats."
+ )
+
+
@command_handler(
needs_auth=True,
management_only=True,
diff --git a/matrix_appservice_kakaotalk/kt/client/client.py b/matrix_appservice_kakaotalk/kt/client/client.py
index b1511e9..0267cc5 100644
--- a/matrix_appservice_kakaotalk/kt/client/client.py
+++ b/matrix_appservice_kakaotalk/kt/client/client.py
@@ -32,6 +32,7 @@ from aiohttp import ClientSession
from aiohttp.client import _RequestContextManager
from yarl import URL
+from mautrix.types import SerializerError
from mautrix.util.logging import TraceLogger
from ...config import Config
@@ -55,7 +56,12 @@ from ..types.request import (
CommandResultDoneValue
)
-from .types import PortalChannelInfo, UserInfoUnion, ChannelProps
+from .types import (
+ ChannelProps,
+ PortalChannelInfo,
+ SettingsStruct,
+ UserInfoUnion,
+)
from .errors import InvalidAccessToken, CommandException
from .error_helper import raise_unsuccessful_response
@@ -197,17 +203,21 @@ class Client:
# region post-token commands
- async def start(self) -> ProfileStruct:
+ async def start(self) -> SettingsStruct | None:
"""
Initialize user-specific bridging & state by providing a token obtained from a prior login.
Receive the user's profile info in response.
"""
- profile_req_struct = await self._api_user_request_result(ProfileReqStruct, "start")
+ try:
+ settings_struct = await self._api_user_request_result(SettingsStruct, "start")
+ except SerializerError:
+ self.log.exception("Unable to deserialize settings struct, but starting client anyways")
+ settings_struct = None
if not self._rpc_disconnection_task:
self._rpc_disconnection_task = asyncio.create_task(self._rpc_disconnection_handler())
else:
self.log.warning("Called \"start\" on an already-started client")
- return profile_req_struct.profile
+ return settings_struct
async def stop(self) -> None:
"""Immediately stop bridging this user."""
@@ -246,6 +256,9 @@ class Client:
await self._rpc_client.request("disconnect", mxid=self.user.mxid)
await self._on_disconnect(None)
+ async def get_settings(self) -> SettingsStruct:
+ return await self._api_user_request_result(SettingsStruct, "get_settings")
+
async def get_own_profile(self) -> ProfileStruct:
profile_req_struct = await self._api_user_request_result(ProfileReqStruct, "get_own_profile")
return profile_req_struct.profile
diff --git a/matrix_appservice_kakaotalk/kt/client/types.py b/matrix_appservice_kakaotalk/kt/client/types.py
index b00ba94..6b2cb91 100644
--- a/matrix_appservice_kakaotalk/kt/client/types.py
+++ b/matrix_appservice_kakaotalk/kt/client/types.py
@@ -26,6 +26,7 @@ from mautrix.types import (
MessageType,
)
+from ..types.api.struct import MoreSettingsStruct, LessSettingsStruct
from ..types.bson import Long
from ..types.channel.channel_info import NormalChannelInfo
from ..types.channel.channel_type import ChannelType
@@ -34,6 +35,12 @@ from ..types.openlink.open_channel_info import OpenChannelInfo
from ..types.user.channel_user_info import NormalChannelUserInfo, OpenChannelUserInfo
+@dataclass
+class SettingsStruct(SerializableAttrs):
+ more: MoreSettingsStruct
+ less: LessSettingsStruct
+
+
ChannelInfoUnion = NewType("ChannelInfoUnion", Union[NormalChannelInfo, OpenChannelInfo])
@deserializer(ChannelInfoUnion)
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py b/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py
index eb92a23..7ae6988 100644
--- a/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/__init__.py
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
##from .login import *
-##from .account import *
+from .account import *
from .profile import *
from .friends import *
##from .openlink import *
diff --git a/matrix_appservice_kakaotalk/kt/types/api/struct/account.py b/matrix_appservice_kakaotalk/kt/types/api/struct/account.py
new file mode 100644
index 0000000..3e879e0
--- /dev/null
+++ b/matrix_appservice_kakaotalk/kt/types/api/struct/account.py
@@ -0,0 +1,141 @@
+# matrix-appservice-kakaotalk - A Matrix-KakaoTalk puppeting bridge.
+# Copyright (C) 2022 Tulir Asokan, Andrew Ferrazzutti
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+from typing import Optional, Union
+
+from attr import dataclass
+
+from mautrix.types import SerializableAttrs
+
+from ...bson import Long
+
+
+@dataclass
+class OpenChatSettingsStruct(SerializableAttrs):
+ chatMemberMaxJoin: int
+ chatRoomMaxJoin: int
+ createLinkLimit: 10;
+ createCardLinkLimit: 3;
+ numOfStaffLimit: 5;
+ rewritable: bool
+ handoverEnabled: bool
+
+
+@dataclass(kw_only=True)
+class MoreSettingsStruct(SerializableAttrs):
+ since: int
+
+ @dataclass
+ class ClientConf(SerializableAttrs):
+ osVersion: str
+ clientConf: ClientConf
+
+ available: int
+ available2: int
+ friendsPollingInterval: Optional[int] = None # NOTE Made optional
+ settingsPollingInterval: Optional[int] = None # NOTE Made optional
+ profilePollingInterval: Optional[int] = None # NOTE Made optional
+ moreListPollingInterval: Optional[int] = None # NOTE Made optional
+ morePayPollingInterval: Optional[int] = None # NOTE Made optional
+ daumMediaPollingInterval: Optional[int] = None # NOTE Made optional
+ lessSettingsPollingInterval: Optional[int] = None # NOTE Made optional
+
+ @dataclass
+ class MoreApps(SerializableAttrs):
+ recommend: Optional[list[str]] = None # NOTE From unknown[]
+ all: Optional[list[str]] = None # NOTE From unknown[]
+ moreApps: MoreApps
+
+ shortcuts: Optional[dict[str, int]] = None # NOTE Made optional
+ seasonProfileRev: int
+ seasonNoticeRev: int
+ serviceUserId: Union[Long, int]
+ accountId: int
+ accountDisplayId: str
+ hashedAccountId: str
+ pstnNumber: str
+ formattedPstnNumber: str
+ nsnNumber: str
+ formattedNsnNumber: str
+ contactNameSync: int
+ allowMigration: bool
+ emailStatus: int
+ emailAddress: str
+ emailVerified: bool
+ uuid: str
+ uuidSearchable: bool
+ nickName: str
+ openChat: OpenChatSettingsStruct
+ profileImageUrl: str
+ fullProfileImageUrl: str
+ originalProfileImageUrl: str
+ statusMessage: str
+
+
+@dataclass(kw_only=True)
+class LessSettingsStruct(SerializableAttrs):
+ kakaoAutoLoginDomain: list[str]
+ daumSsoDomain: list[str]
+
+ @dataclass
+ class GoogleMapsApi(SerializableAttrs):
+ key: str
+ signature: str
+ googleMapsApi: GoogleMapsApi
+
+ @dataclass
+ class ChatReportLimit(SerializableAttrs):
+ chat: int
+ open_chat: int
+ plus_chat: int
+ chat_report_limit: ChatReportLimit
+
+ externalApiList: str # NOTE From unknown
+
+ @dataclass
+ class BirthdayFriends(SerializableAttrs):
+ landing_url: str
+ birthday_friends: BirthdayFriends
+
+ messageDeleteTime: Optional[int] = None # NOTE Made optional
+
+ @dataclass
+ class VoiceTalk(SerializableAttrs):
+ groupCallMaxParticipants: int
+ voiceTalk: VoiceTalk
+
+ profileActions: bool
+
+ @dataclass
+ class PostExpirationSetting(SerializableAttrs):
+ flagOn: bool
+ newPostTerm: int
+ postExpirationSetting: PostExpirationSetting
+
+ kakaoAlertIds: list[int]
+
+
+@dataclass
+class LoginTokenStruct(SerializableAttrs):
+ token: str
+ expires: int
+
+
+___all___ = [
+ "OpenChatSettingsStruct",
+ "MoreSettingsStruct",
+ "LessSettingsStruct",
+ "LoginTokenStruct",
+]
\ No newline at end of file
diff --git a/matrix_appservice_kakaotalk/user.py b/matrix_appservice_kakaotalk/user.py
index 54e4368..2dfb4dc 100644
--- a/matrix_appservice_kakaotalk/user.py
+++ b/matrix_appservice_kakaotalk/user.py
@@ -25,6 +25,7 @@ from mautrix.types import (
JSON,
MessageType,
RoomID,
+ SerializerError,
TextMessageEventContent,
UserID,
)
@@ -38,7 +39,7 @@ from .db import User as DBUser
from .kt.client import Client
from .kt.client.errors import AuthenticationRequired, ResponseError
-from .kt.types.api.struct.profile import ProfileStruct
+from .kt.client.types import SettingsStruct
from .kt.types.bson import Long
from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData
from .kt.types.channel.channel_type import ChannelType, KnownChannelType
@@ -96,7 +97,7 @@ class User(DBUser, BaseUser):
_db_instance: DBUser | None
_sync_lock: SimpleLock
_is_rpc_reconnecting: bool
- _logged_in_info: ProfileStruct | None
+ _logged_in_info: SettingsStruct | None
_logged_in_info_time: float
def __init__(
@@ -253,9 +254,9 @@ class User(DBUser, BaseUser):
self.log.warning(f"UUID mismatch: expected {self.uuid}, got {oauth_credential.deviceUUID}")
self.uuid = oauth_credential.deviceUUID
- async def get_own_info(self) -> ProfileStruct:
+ async def get_own_info(self) -> SettingsStruct:
if not self._logged_in_info or self._logged_in_info_time + 60 * 60 < time.monotonic():
- self._logged_in_info = await self.client.get_own_profile()
+ self._logged_in_info = await self.client.get_settings()
self._logged_in_info_time = time.monotonic()
return self._logged_in_info
@@ -272,7 +273,7 @@ class User(DBUser, BaseUser):
return False
client = Client(self, log=self.log.getChild("ktclient"))
user_info = await client.start()
- # NOTE On failure, client.start throws instead of returning False
+ # NOTE On failure, client.start throws instead of returning something falsy
self.log.info("Loaded session successfully")
self.client = client
self._logged_in_info = user_info
@@ -303,6 +304,12 @@ class User(DBUser, BaseUser):
if self._is_logged_in is None or _override:
try:
self._is_logged_in = bool(await self.get_own_info())
+ except SerializerError:
+ self.log.exception(
+ "Unable to deserialize settings struct, "
+ "but didn't get auth error, so treating user as logged in"
+ )
+ self._is_logged_in = True
except Exception:
self.log.exception("Exception checking login status")
self._is_logged_in = False
diff --git a/node/src/client.js b/node/src/client.js
index b0f128f..164fa8e 100644
--- a/node/src/client.js
+++ b/node/src/client.js
@@ -460,8 +460,7 @@ export default class PeerClient {
*/
userStart = async (req) => {
const userClient = this.#tryGetUser(req.mxid) || await UserClient.create(req.mxid, req.oauth_credential, this)
- // TODO Should call requestMore/LessSettings instead
- const res = await userClient.serviceClient.requestMyProfile()
+ const res = await this.#getSettings(userClient.serviceClient)
if (res.success) {
this.userClients.set(req.mxid, userClient)
}
@@ -496,7 +495,31 @@ export default class PeerClient {
/**
* @param {Object} req
- * @param {?string} req.mxid
+ * @param {string} req.mxid
+ */
+ getSettings = async (req) => {
+ return await this.#getSettings(this.#getUser(req.mxid).serviceClient)
+ }
+
+ /**
+ * @param {ServiceApiClient} serviceClient
+ */
+ #getSettings = async (serviceClient) => {
+ const moreRes = await serviceClient.requestMoreSettings()
+ if (!moreRes.success) return moreRes
+
+ const lessRes = await serviceClient.requestLessSettings()
+ if (!lessRes.success) return lessRes
+
+ return makeCommandResult({
+ more: moreRes.result,
+ less: lessRes.result,
+ })
+ }
+
+ /**
+ * @param {Object} req
+ * @param {string} req.mxid
*/
getOwnProfile = async (req) => {
return await this.#getUser(req.mxid).serviceClient.requestMyProfile()
@@ -504,7 +527,15 @@ export default class PeerClient {
/**
* @param {Object} req
- * @param {?string} req.mxid
+ * @param {string} req.mxid
+ */
+ getOwnProfile = async (req) => {
+ return await this.#getUser(req.mxid).serviceClient.requestMyProfile()
+ }
+
+ /**
+ * @param {Object} req
+ * @param {string} req.mxid
* @param {Long} req.user_id
*/
getProfile = async (req) => {
@@ -522,7 +553,7 @@ export default class PeerClient {
const res = await talkChannel.updateAll()
if (!res.success) return res
- return this.#makeCommandResult({
+ return makeCommandResult({
name: talkChannel.getDisplayName(),
participants: Array.from(talkChannel.getAllUserInfo()),
// TODO Image
@@ -574,7 +605,7 @@ export default class PeerClient {
const res = await this.#getUser(req.mxid).serviceClient.findFriendById(req.friend_id)
if (!res.success) return res
- return this.#makeCommandResult(res.result.friend[propertyName])
+ return makeCommandResult(res.result.friend[propertyName])
}
/**
@@ -662,14 +693,6 @@ export default class PeerClient {
})
}
- #makeCommandResult(result) {
- return {
- success: true,
- status: 0,
- result: result
- }
- }
-
handleUnknownCommand = () => {
throw new Error("Unknown command")
}
@@ -735,6 +758,7 @@ export default class PeerClient {
stop: this.userStop,
connect: this.handleConnect,
disconnect: this.handleDisconnect,
+ get_settings: this.getSettings,
get_own_profile: this.getOwnProfile,
get_profile: this.getProfile,
get_portal_channel_info: this.getPortalChannelInfo,
@@ -790,6 +814,17 @@ export default class PeerClient {
}
+/**
+ * @param {object} result
+ */
+function makeCommandResult(result) {
+ return {
+ success: true,
+ status: 0,
+ result: result
+ }
+}
+
/**
* @param {TalkChannelList} channelList
* @param {ChannelType} channelType
diff --git a/systemd/matrix-appservice-kakaotalk-node.service b/systemd/matrix-appservice-kakaotalk-node.service
new file mode 100644
index 0000000..474638c
--- /dev/null
+++ b/systemd/matrix-appservice-kakaotalk-node.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=Node backend for matrix-appservice-kakaotalk
+After=multi-user.target network.target
+
+[Service]
+; User=matrix-appservice-kakaotalk
+; Group=matrix-appservice-kakaotalk
+Type=notify
+NotifyAccess=all
+WorkingDirectory=/opt/matrix-appservice-kakaotalk/node
+ConfigurationDirectory=matrix-appservice-kakaotalk
+RuntimeDirectory=matrix-appservice-kakaotalk
+ExecStart=/usr/bin/env node src/main.js --config ${CONFIGURATION_DIRECTORY}/config.json
+Restart=on-failure
+RestartSec=3
+
+[Install]
+WantedBy=multi-user.target
diff --git a/systemd/matrix-appservice-kakaotalk.service b/systemd/matrix-appservice-kakaotalk.service
new file mode 100644
index 0000000..d50b498
--- /dev/null
+++ b/systemd/matrix-appservice-kakaotalk.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=matrix-appservice-kakaotalk bridge
+After=multi-user.target network.target
+
+[Service]
+; User=matrix-appservice-kakaotalk
+; Group=matrix-appservice-kakaotalk
+WorkingDirectory=/opt/matrix-appservice-kakaotalk
+ConfigurationDirectory=matrix-appservice-kakaotalk
+RuntimeDirectory=matrix-appservice-kakaotalk
+ExecStart=/opt/matrix-appservice-kakaotalk/.venv/bin/python -m matrix_appservice_kakaotalk -c ${CONFIGURATION_DIRECTORY}/config.yaml
+Restart=on-failure
+RestartSec=3
+
+[Install]
+WantedBy=multi-user.target