Use More/LessSettings instead of profile on login; add whoami command
This commit is contained in:
parent
370865c2c1
commit
9a33f3dcf2
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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",
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue