Use More/LessSettings instead of profile on login; add whoami command

This commit is contained in:
Andrew Ferrazzutti 2022-04-10 22:56:51 -04:00
parent 370865c2c1
commit 9a33f3dcf2
9 changed files with 282 additions and 32 deletions

View File

@ -14,6 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.bridge.commands import HelpSection, command_handler from mautrix.bridge.commands import HelpSection, command_handler
from mautrix.types import SerializerError
from .typehint import CommandEvent from .typehint import CommandEvent
@ -53,24 +54,36 @@ async def disconnect(evt: CommandEvent) -> None:
needs_auth=True, needs_auth=True,
management_only=True, management_only=True,
help_section=SECTION_CONNECTION, 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: async def whoami(evt: CommandEvent) -> None:
if not await evt.sender.is_logged_in():
await evt.reply("You're not logged into KakaoTalk")
return
await evt.mark_read() await evt.mark_read()
try: try:
own_info = await evt.sender.get_own_info() own_info = await evt.sender.get_own_info()
await evt.reply( await evt.reply(
f"You're logged in as {own_info.nickname} (user ID {evt.sender.ktid})." f"You're logged in as `{own_info.more.uuid}` (nickname: {own_info.more.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" 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: except CommandException as e:
await evt.reply(f"Error from KakaoTalk: {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( @command_handler(
needs_auth=True, needs_auth=True,
management_only=True, management_only=True,

View File

@ -32,6 +32,7 @@ from aiohttp import ClientSession
from aiohttp.client import _RequestContextManager from aiohttp.client import _RequestContextManager
from yarl import URL from yarl import URL
from mautrix.types import SerializerError
from mautrix.util.logging import TraceLogger from mautrix.util.logging import TraceLogger
from ...config import Config from ...config import Config
@ -55,7 +56,12 @@ from ..types.request import (
CommandResultDoneValue CommandResultDoneValue
) )
from .types import PortalChannelInfo, UserInfoUnion, ChannelProps from .types import (
ChannelProps,
PortalChannelInfo,
SettingsStruct,
UserInfoUnion,
)
from .errors import InvalidAccessToken, CommandException from .errors import InvalidAccessToken, CommandException
from .error_helper import raise_unsuccessful_response from .error_helper import raise_unsuccessful_response
@ -197,17 +203,21 @@ class Client:
# region post-token commands # 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. Initialize user-specific bridging & state by providing a token obtained from a prior login.
Receive the user's profile info in response. 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: if not self._rpc_disconnection_task:
self._rpc_disconnection_task = asyncio.create_task(self._rpc_disconnection_handler()) self._rpc_disconnection_task = asyncio.create_task(self._rpc_disconnection_handler())
else: else:
self.log.warning("Called \"start\" on an already-started client") self.log.warning("Called \"start\" on an already-started client")
return profile_req_struct.profile return settings_struct
async def stop(self) -> None: async def stop(self) -> None:
"""Immediately stop bridging this user.""" """Immediately stop bridging this user."""
@ -246,6 +256,9 @@ class Client:
await self._rpc_client.request("disconnect", mxid=self.user.mxid) await self._rpc_client.request("disconnect", mxid=self.user.mxid)
await self._on_disconnect(None) 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: async def get_own_profile(self) -> ProfileStruct:
profile_req_struct = await self._api_user_request_result(ProfileReqStruct, "get_own_profile") profile_req_struct = await self._api_user_request_result(ProfileReqStruct, "get_own_profile")
return profile_req_struct.profile return profile_req_struct.profile

View File

@ -26,6 +26,7 @@ from mautrix.types import (
MessageType, MessageType,
) )
from ..types.api.struct import MoreSettingsStruct, LessSettingsStruct
from ..types.bson import Long from ..types.bson import Long
from ..types.channel.channel_info import NormalChannelInfo from ..types.channel.channel_info import NormalChannelInfo
from ..types.channel.channel_type import ChannelType 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 from ..types.user.channel_user_info import NormalChannelUserInfo, OpenChannelUserInfo
@dataclass
class SettingsStruct(SerializableAttrs):
more: MoreSettingsStruct
less: LessSettingsStruct
ChannelInfoUnion = NewType("ChannelInfoUnion", Union[NormalChannelInfo, OpenChannelInfo]) ChannelInfoUnion = NewType("ChannelInfoUnion", Union[NormalChannelInfo, OpenChannelInfo])
@deserializer(ChannelInfoUnion) @deserializer(ChannelInfoUnion)

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
##from .login import * ##from .login import *
##from .account import * from .account import *
from .profile import * from .profile import *
from .friends import * from .friends import *
##from .openlink import * ##from .openlink import *

View File

@ -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 <https://www.gnu.org/licenses/>.
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",
]

View File

@ -25,6 +25,7 @@ from mautrix.types import (
JSON, JSON,
MessageType, MessageType,
RoomID, RoomID,
SerializerError,
TextMessageEventContent, TextMessageEventContent,
UserID, UserID,
) )
@ -38,7 +39,7 @@ from .db import User as DBUser
from .kt.client import Client from .kt.client import Client
from .kt.client.errors import AuthenticationRequired, ResponseError 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.bson import Long
from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData from .kt.types.channel.channel_info import ChannelInfo, NormalChannelInfo, NormalChannelData
from .kt.types.channel.channel_type import ChannelType, KnownChannelType from .kt.types.channel.channel_type import ChannelType, KnownChannelType
@ -96,7 +97,7 @@ class User(DBUser, BaseUser):
_db_instance: DBUser | None _db_instance: DBUser | None
_sync_lock: SimpleLock _sync_lock: SimpleLock
_is_rpc_reconnecting: bool _is_rpc_reconnecting: bool
_logged_in_info: ProfileStruct | None _logged_in_info: SettingsStruct | None
_logged_in_info_time: float _logged_in_info_time: float
def __init__( def __init__(
@ -253,9 +254,9 @@ class User(DBUser, BaseUser):
self.log.warning(f"UUID mismatch: expected {self.uuid}, got {oauth_credential.deviceUUID}") self.log.warning(f"UUID mismatch: expected {self.uuid}, got {oauth_credential.deviceUUID}")
self.uuid = 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(): 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() self._logged_in_info_time = time.monotonic()
return self._logged_in_info return self._logged_in_info
@ -272,7 +273,7 @@ class User(DBUser, BaseUser):
return False return False
client = Client(self, log=self.log.getChild("ktclient")) client = Client(self, log=self.log.getChild("ktclient"))
user_info = await client.start() 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.log.info("Loaded session successfully")
self.client = client self.client = client
self._logged_in_info = user_info self._logged_in_info = user_info
@ -303,6 +304,12 @@ class User(DBUser, BaseUser):
if self._is_logged_in is None or _override: if self._is_logged_in is None or _override:
try: try:
self._is_logged_in = bool(await self.get_own_info()) 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: except Exception:
self.log.exception("Exception checking login status") self.log.exception("Exception checking login status")
self._is_logged_in = False self._is_logged_in = False

View File

@ -460,8 +460,7 @@ export default class PeerClient {
*/ */
userStart = async (req) => { userStart = async (req) => {
const userClient = this.#tryGetUser(req.mxid) || await UserClient.create(req.mxid, req.oauth_credential, this) const userClient = this.#tryGetUser(req.mxid) || await UserClient.create(req.mxid, req.oauth_credential, this)
// TODO Should call requestMore/LessSettings instead const res = await this.#getSettings(userClient.serviceClient)
const res = await userClient.serviceClient.requestMyProfile()
if (res.success) { if (res.success) {
this.userClients.set(req.mxid, userClient) this.userClients.set(req.mxid, userClient)
} }
@ -496,7 +495,31 @@ export default class PeerClient {
/** /**
* @param {Object} req * @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) => { getOwnProfile = async (req) => {
return await this.#getUser(req.mxid).serviceClient.requestMyProfile() return await this.#getUser(req.mxid).serviceClient.requestMyProfile()
@ -504,7 +527,15 @@ export default class PeerClient {
/** /**
* @param {Object} req * @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 * @param {Long} req.user_id
*/ */
getProfile = async (req) => { getProfile = async (req) => {
@ -522,7 +553,7 @@ export default class PeerClient {
const res = await talkChannel.updateAll() const res = await talkChannel.updateAll()
if (!res.success) return res if (!res.success) return res
return this.#makeCommandResult({ return makeCommandResult({
name: talkChannel.getDisplayName(), name: talkChannel.getDisplayName(),
participants: Array.from(talkChannel.getAllUserInfo()), participants: Array.from(talkChannel.getAllUserInfo()),
// TODO Image // TODO Image
@ -574,7 +605,7 @@ export default class PeerClient {
const res = await this.#getUser(req.mxid).serviceClient.findFriendById(req.friend_id) const res = await this.#getUser(req.mxid).serviceClient.findFriendById(req.friend_id)
if (!res.success) return res 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 = () => { handleUnknownCommand = () => {
throw new Error("Unknown command") throw new Error("Unknown command")
} }
@ -735,6 +758,7 @@ export default class PeerClient {
stop: this.userStop, stop: this.userStop,
connect: this.handleConnect, connect: this.handleConnect,
disconnect: this.handleDisconnect, disconnect: this.handleDisconnect,
get_settings: this.getSettings,
get_own_profile: this.getOwnProfile, get_own_profile: this.getOwnProfile,
get_profile: this.getProfile, get_profile: this.getProfile,
get_portal_channel_info: this.getPortalChannelInfo, 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 {TalkChannelList} channelList
* @param {ChannelType} channelType * @param {ChannelType} channelType

View File

@ -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

View File

@ -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