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
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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,

View File

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

View File

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

View File

@ -14,7 +14,7 @@
# 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 .login import *
##from .account import *
from .account import *
from .profile import *
from .friends 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,
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

View File

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

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