Better error handling for permission setting and privileged actions

This commit is contained in:
Andrew Ferrazzutti 2022-04-14 00:55:58 -04:00
parent 746756cc3f
commit abf3114203
3 changed files with 65 additions and 17 deletions

View File

@ -31,10 +31,7 @@ from mautrix.types import (
UserID, UserID,
) )
from .kt.client.errors import CommandException from . import portal as po, user as u
from .kt.client.types import TO_PERM_MAP
from . import portal as po, puppet as pu, user as u
from .db import Message as DBMessage from .db import Message as DBMessage
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@ -1007,6 +1007,8 @@ class Portal(DBPortal, BasePortal):
sender, _ = await self.get_relay_sender(sender, f"redaction {event_id}") sender, _ = await self.get_relay_sender(sender, f"redaction {event_id}")
if not sender: if not sender:
raise Exception("not logged in") raise Exception("not logged in")
elif not sender.is_connected:
raise Exception("not connected to KakaoTalk chats")
message = await DBMessage.get_by_mxid(event_id, self.mxid) message = await DBMessage.get_by_mxid(event_id, self.mxid)
if message: if message:
if not message.ktid: if not message.ktid:
@ -1050,7 +1052,7 @@ class Portal(DBPortal, BasePortal):
event_id: EventID, event_id: EventID,
) -> None: ) -> None:
try: try:
await self._handle_matrix_power_level(sender, prev_content, content) await self._handle_matrix_power_level(sender, prev_content, content, event_id)
except Exception as e: except Exception as e:
self.log.error( self.log.error(
f"Failed to handle Matrix power level {event_id}: {e}", f"Failed to handle Matrix power level {event_id}: {e}",
@ -1084,17 +1086,21 @@ class Portal(DBPortal, BasePortal):
sender: u.User, sender: u.User,
prev_content: PowerLevelStateEventContent, prev_content: PowerLevelStateEventContent,
content: PowerLevelStateEventContent, content: PowerLevelStateEventContent,
event_id: EventID,
) -> None: ) -> None:
sender, _ = await self.get_relay_sender(sender, f"power level {event_id}")
for target_mxid, power_level in content.users.items(): for target_mxid, power_level in content.users.items():
if power_level == prev_content.get_user_level(target_mxid): if power_level == prev_content.get_user_level(target_mxid):
continue continue
puppet = await p.Puppet.get_by_mxid(target_mxid) puppet = await p.Puppet.get_by_mxid(target_mxid)
if puppet: if puppet:
if sender.is_connected: if sender and sender.is_connected:
perm = TO_PERM_MAP.get(power_level) perm = TO_PERM_MAP.get(power_level)
await sender.client.send_perm(self.channel_props, puppet.ktid, perm) await sender.client.send_perm(self.channel_props, puppet.ktid, perm)
else: else:
raise Exception("Disconnected users cannot set power levels of KakaoTalk users") raise Exception(
"Only users connected to KakaoTalk can set power levels of KakaoTalk users"
)
async def handle_matrix_leave(self, user: u.User) -> None: async def handle_matrix_leave(self, user: u.User) -> None:
if self.is_direct: if self.is_direct:

View File

@ -27,9 +27,11 @@ import {
/** @typedef {import("node-kakao").ChannelType} ChannelType */ /** @typedef {import("node-kakao").ChannelType} ChannelType */
/** @typedef {import("node-kakao").ReplyAttachment} ReplyAttachment */ /** @typedef {import("node-kakao").ReplyAttachment} ReplyAttachment */
/** @typedef {import("node-kakao").MentionStruct} MentionStruct */ /** @typedef {import("node-kakao").MentionStruct} MentionStruct */
/** @typedef {import("node-kakao").OpenChannelUserPerm} OpenChannelUserPerm */
/** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */ /** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */
import pkg from "node-kakao"
const { OpenChannelUserPerm } = pkg
import chat from "node-kakao/chat" import chat from "node-kakao/chat"
const { KnownChatType } = chat const { KnownChatType } = chat
@ -59,6 +61,34 @@ ServiceApiClient.prototype.requestFriendList = async function() {
} }
class CustomError extends Error {}
class PermError extends CustomError {
/** @type {Map<OpenChannelUserPerm, string> */
static #PERM_NAMES = new Map([
[OpenChannelUserPerm.OWNER, "the channel owner"],
[OpenChannelUserPerm.MANAGER, "channel admininstrators"],
[OpenChannelUserPerm.BOT, "bots"],
[OpenChannelUserPerm.NONE, "registered KakaoTalk users"],
])
/**
* @param {?OpenChannelUserPerm[]} permNeeded
* @param {?OpenChannelUserPerm} permActual
*/
constructor(permNeeded, permActual, action) {
const who =
!permActual
? "In this channel, no one"
: "Only " + permNeeded
.map(v => PermError.#PERM_NAMES.get(v))
.reduce((prev, curr) => prev += ` and ${curr}`)
super(`${who} can ${action}`)
this.name = this.constructor.name
}
}
class UserClient { class UserClient {
static #initializing = false static #initializing = false
@ -71,15 +101,17 @@ class UserClient {
/** /**
* DO NOT CONSTRUCT DIRECTLY. Callers should use {@link UserClient#create} instead. * DO NOT CONSTRUCT DIRECTLY. Callers should use {@link UserClient#create} instead.
* @param {Long} userId
* @param {string} mxid * @param {string} mxid
* @param {PeerClient} peerClient TODO Make RPC user-specific instead of needing this * @param {PeerClient} peerClient TODO Make RPC user-specific instead of needing this
*/ */
constructor(mxid, peerClient) { constructor(userId, mxid, peerClient) {
if (!UserClient.#initializing) { if (!UserClient.#initializing) {
throw new Error("Private constructor") throw new Error("Private constructor")
} }
UserClient.#initializing = false UserClient.#initializing = false
this.userId = userId
this.mxid = mxid this.mxid = mxid
this.peerClient = peerClient this.peerClient = peerClient
@ -248,7 +280,7 @@ class UserClient {
*/ */
static async create(mxid, credential, peerClient) { static async create(mxid, credential, peerClient) {
this.#initializing = true this.#initializing = true
const userClient = new UserClient(mxid, peerClient) const userClient = new UserClient(credential.userId, mxid, peerClient)
userClient.#serviceClient = await ServiceApiClient.create(credential) userClient.#serviceClient = await ServiceApiClient.create(credential)
return userClient return userClient
@ -478,9 +510,20 @@ 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 {?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.
*/ */
async #getUserChannel(mxid, channelProps) { async #getUserChannel(mxid, channelProps, permNeeded, action) {
return await this.#getUser(mxid).getChannel(channelProps) const userClient = this.#getUser(mxid)
const talkChannel = await userClient.getChannel(channelProps)
if (permNeeded) {
const permActual = talkChannel.getUserInfo({ userId: userClient.userId }).perm
if (!(permActual in permNeeded)) {
throw new PermError(permNeeded, permActual, action)
}
}
return talkChannel
} }
/** /**
@ -755,10 +798,12 @@ export default class PeerClient {
* @param {OpenChannelUserPerm} req.perm * @param {OpenChannelUserPerm} req.perm
*/ */
sendPerm = async (req) => { sendPerm = async (req) => {
if (!isChannelTypeOpen(req.channel_props.type)) { const talkChannel = await this.#getUserChannel(
throw Error("Can't send perm on non-open channel") req.mxid,
} req.channel_props,
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props) [OpenChannelUserPerm.OWNER],
"change user permissions"
)
return await talkChannel.setUserPerm({ userId: req.user_id }, req.perm) return await talkChannel.setUserPerm({ userId: req.user_id }, req.perm)
} }
@ -857,7 +902,7 @@ export default class PeerClient {
} }
} else { } else {
resp.command = "error" resp.command = "error"
resp.error = err.toString() resp.error = err instanceof CustomError ? err.message : err.toString()
this.log(`Error handling request ${resp.id} ${err.stack}`) this.log(`Error handling request ${resp.id} ${err.stack}`)
// TODO Check if session is broken. If it is, close the PeerClient // TODO Check if session is broken. If it is, close the PeerClient
} }