2022-02-25 02:22:50 -05:00
|
|
|
// 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/>.
|
|
|
|
import { Long } from "bson"
|
2022-03-21 01:10:31 -04:00
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
import {
|
|
|
|
AuthApiClient,
|
|
|
|
OAuthApiClient,
|
|
|
|
ServiceApiClient,
|
|
|
|
TalkClient,
|
|
|
|
KnownAuthStatusCode,
|
|
|
|
util,
|
|
|
|
} from "node-kakao"
|
2022-04-28 01:36:15 -04:00
|
|
|
import { ReadStreamUtil } from "node-kakao/stream"
|
2022-03-21 01:10:31 -04:00
|
|
|
/** @typedef {import("node-kakao").OAuthCredential} OAuthCredential */
|
2022-03-21 01:33:22 -04:00
|
|
|
/** @typedef {import("node-kakao").ChannelType} ChannelType */
|
2022-04-05 15:44:02 -04:00
|
|
|
/** @typedef {import("node-kakao").ReplyAttachment} ReplyAttachment */
|
2022-04-06 12:49:23 -04:00
|
|
|
/** @typedef {import("node-kakao").MentionStruct} MentionStruct */
|
2022-04-28 01:32:12 -04:00
|
|
|
/** @typedef {import("node-kakao").TalkNormalChannel} TalkNormalChannel */
|
|
|
|
/** @typedef {import("node-kakao").TalkOpenChannel} TalkOpenChannel */
|
2022-04-05 15:44:02 -04:00
|
|
|
/** @typedef {import("node-kakao/dist/talk").TalkChannelList} TalkChannelList */
|
2022-04-28 01:32:12 -04:00
|
|
|
// TODO Remove once/if some helper type hints are upstreamed
|
|
|
|
/** @typedef {import("node-kakao").OpenChannelUserInfo} OpenChannelUserInfo */
|
2022-03-21 01:10:31 -04:00
|
|
|
|
2022-04-23 16:48:23 -04:00
|
|
|
import openlink from "node-kakao/openlink"
|
|
|
|
const { OpenChannelUserPerm } = openlink
|
2022-04-14 00:55:58 -04:00
|
|
|
|
2022-03-09 20:26:39 -05:00
|
|
|
import chat from "node-kakao/chat"
|
|
|
|
const { KnownChatType } = chat
|
2022-02-25 02:22:50 -05:00
|
|
|
|
2022-09-05 23:55:27 -04:00
|
|
|
import * as modutil from "./modutil.js"
|
2022-03-18 03:52:55 -04:00
|
|
|
import { emitLines, promisify } from "./util.js"
|
|
|
|
|
2022-04-08 19:01:32 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @typedef {object} ChannelProps
|
2022-04-08 19:01:32 -04:00
|
|
|
* @property {Long} id
|
|
|
|
* @property {ChannelType} type
|
|
|
|
*/
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
|
2022-03-23 03:09:30 -04:00
|
|
|
ServiceApiClient.prototype.requestFriendList = async function() {
|
|
|
|
const res = await this._client.requestData(
|
|
|
|
"POST",
|
|
|
|
`${this.getFriendsApiPath("update.json")}`,
|
|
|
|
{
|
|
|
|
phone_number_type: 1,
|
|
|
|
}
|
2022-04-10 02:19:59 -04:00
|
|
|
)
|
2022-03-23 03:09:30 -04:00
|
|
|
|
|
|
|
return {
|
|
|
|
status: res.status,
|
|
|
|
success: res.status === 0,
|
|
|
|
result: res,
|
2022-04-10 02:19:59 -04:00
|
|
|
}
|
2022-03-23 03:09:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-04-28 01:32:12 -04:00
|
|
|
class ProtocolError extends Error {}
|
2022-04-14 00:55:58 -04:00
|
|
|
|
2022-04-28 01:32:12 -04:00
|
|
|
class PermError extends ProtocolError {
|
2022-04-14 00:55:58 -04:00
|
|
|
/** @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
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {string} action
|
2022-04-14 00:55:58 -04:00
|
|
|
*/
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
class UserClient {
|
2022-03-06 03:14:59 -05:00
|
|
|
static #initializing = false
|
2022-02-25 02:22:50 -05:00
|
|
|
|
2022-05-16 00:08:00 -04:00
|
|
|
#connected = false
|
2022-02-25 02:22:50 -05:00
|
|
|
#talkClient = new TalkClient()
|
|
|
|
get talkClient() { return this.#talkClient }
|
|
|
|
|
|
|
|
/** @type {ServiceApiClient} */
|
2022-03-18 03:52:55 -04:00
|
|
|
#serviceClient
|
2022-02-25 02:22:50 -05:00
|
|
|
get serviceClient() { return this.#serviceClient }
|
|
|
|
|
2022-04-23 13:51:39 -04:00
|
|
|
/** @type {OAuthCredential} */
|
|
|
|
#credential
|
|
|
|
get userId() { return this.#credential.userId }
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
/**
|
2022-03-06 03:14:59 -05:00
|
|
|
* DO NOT CONSTRUCT DIRECTLY. Callers should use {@link UserClient#create} instead.
|
|
|
|
* @param {string} mxid
|
2022-04-01 05:07:05 -04:00
|
|
|
* @param {PeerClient} peerClient TODO Make RPC user-specific instead of needing this
|
2022-02-25 02:22:50 -05:00
|
|
|
*/
|
2022-04-23 13:51:39 -04:00
|
|
|
constructor(mxid, peerClient) {
|
2022-03-06 03:14:59 -05:00
|
|
|
if (!UserClient.#initializing) {
|
|
|
|
throw new Error("Private constructor")
|
|
|
|
}
|
|
|
|
UserClient.#initializing = false
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
this.mxid = mxid
|
2022-04-01 05:07:05 -04:00
|
|
|
this.peerClient = peerClient
|
2022-04-05 02:05:43 -04:00
|
|
|
|
|
|
|
this.#talkClient.on("chat", (data, channel) => {
|
2022-04-23 17:01:39 -04:00
|
|
|
this.log(`Chat ${data.chat.logId} received in channel ${channel.channelId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("chat", {
|
2022-04-05 02:05:43 -04:00
|
|
|
//is_sequential: true, // TODO Make sequential per user & channel (if it isn't already)
|
|
|
|
chatlog: data.chat,
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
2022-04-09 03:07:31 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("chat_deleted", (feedChatlog, channel, feed) => {
|
2022-04-23 17:01:39 -04:00
|
|
|
this.log(`Chat ${feed.logId} deleted in channel ${channel.channelId} by user ${feedChatlog.sender.userId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("chat_deleted", {
|
2022-04-09 03:07:31 -04:00
|
|
|
chatId: feed.logId,
|
|
|
|
senderId: feedChatlog.sender.userId,
|
|
|
|
timestamp: feedChatlog.sendAt,
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("message_hidden", (hideLog, channel, feed) => {
|
2022-04-10 04:30:26 -04:00
|
|
|
this.log(`Message ${feed.logId} hid from channel ${channel.channelId} by user ${hideLog.sender.userId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("chat_deleted", {
|
2022-04-09 03:07:31 -04:00
|
|
|
chatId: feed.logId,
|
|
|
|
senderId: hideLog.sender.userId,
|
|
|
|
timestamp: hideLog.sendAt,
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
2022-04-05 02:05:43 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("chat_read", (chat, channel, reader) => {
|
2022-05-10 21:36:22 -04:00
|
|
|
let senderId
|
|
|
|
if (reader) {
|
|
|
|
senderId = reader.userId
|
|
|
|
} else if (channel.info.type == "MemoChat") {
|
|
|
|
senderId = channel.clientUser.userId
|
|
|
|
} else {
|
|
|
|
this.error(`Chat ${chat.logId} read in channel ${channel.channelId} by unknown reader (channel type: ${channel.info.type || "none"})`)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.log(`Chat ${chat.logId} read in channel ${channel.channelId} by ${senderId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("chat_read", {
|
2022-04-10 02:25:36 -04:00
|
|
|
chatId: chat.logId,
|
2022-05-10 21:36:22 -04:00
|
|
|
senderId: senderId,
|
2022-04-10 02:25:36 -04:00
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
2022-04-05 02:05:43 -04:00
|
|
|
})
|
|
|
|
|
2022-04-23 17:01:39 -04:00
|
|
|
this.#talkClient.on("chat_event", (channel, author, type, count, chat) => {
|
|
|
|
// TODO Figure out if this is can ever be for anything other than hearts on Shouts
|
|
|
|
this.log(`Event ${type} (count = ${count}) on chat ${chat.logId} in channel ${channel.channelId} sent by user ${author.userId}`)
|
|
|
|
})
|
|
|
|
|
2022-04-10 02:23:50 -04:00
|
|
|
this.#talkClient.on("profile_changed", (channel, lastInfo, user) => {
|
2022-04-23 17:01:39 -04:00
|
|
|
this.log(`Profile of ${user.userId} changed (in channel ${channel ? channel.channelId : "None"})`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("profile_changed", {
|
2022-04-10 02:23:50 -04:00
|
|
|
info: user,
|
|
|
|
/* TODO Is this ever a per-channel profile change?
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
*/
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2022-04-28 01:32:12 -04:00
|
|
|
this.#talkClient.on("perm_changed",
|
|
|
|
/**
|
|
|
|
* TODO Upstream these type hints
|
|
|
|
* @param {TalkOpenChannel} channel
|
|
|
|
* @param {OpenChannelUserInfo} lastInfo
|
|
|
|
* @param {OpenChannelUserInfo} user
|
|
|
|
*/
|
|
|
|
(channel, lastInfo, user) => {
|
2022-04-13 01:12:56 -04:00
|
|
|
this.log(`Perms of user ${user.userId} in channel ${channel.channelId} changed from ${lastInfo.perm} to ${user.perm}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("perm_changed", {
|
2022-04-13 01:12:56 -04:00
|
|
|
is_sequential: true,
|
|
|
|
userId: user.userId,
|
|
|
|
perm: user.perm,
|
2022-04-28 01:32:12 -04:00
|
|
|
senderId: getChannelOwner().userId,
|
2022-04-13 01:12:56 -04:00
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2022-04-23 17:01:39 -04:00
|
|
|
this.#talkClient.on("host_handover", (channel, lastLink, link) => {
|
|
|
|
// TODO Find how or if this relates to permissions
|
|
|
|
this.log(`Host of channel ${channel.channelId} changed from ${lastLink.linkOwner.nickname} to ${link.linkOwner.nickname}`)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("channel_added", channel => {
|
|
|
|
this.log(`Added channel ${channel.channelId}`)
|
|
|
|
this.write("channel_added", {
|
|
|
|
channelInfo: channel.info,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2022-04-10 04:30:26 -04:00
|
|
|
this.#talkClient.on("channel_join", channel => {
|
|
|
|
this.log(`Joined channel ${channel.channelId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("channel_join", {
|
2022-04-10 04:30:26 -04:00
|
|
|
channelInfo: channel.info,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("channel_left", channel => {
|
|
|
|
this.log(`Left channel ${channel.channelId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("channel_left", {
|
2022-04-10 04:30:26 -04:00
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("channel_kicked", (kickedLog, channel, feed) => {
|
2022-04-23 17:01:39 -04:00
|
|
|
// TODO Confirm whether this can refer to any user that was kicked, or only to the current user
|
|
|
|
this.log(`Kicked from channel ${channel.channelId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("channel_kicked", {
|
2022-04-10 04:30:26 -04:00
|
|
|
userId: feed.member.userId,
|
|
|
|
senderId: kickedLog.sender.userId,
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2022-04-23 17:01:39 -04:00
|
|
|
this.#talkClient.on("channel_link_deleted", (feedChatLog, channel, feed) => {
|
|
|
|
// TODO Figure out what this means
|
|
|
|
this.log(`Channel link deleted in channel ${channel.channelId}: feed=${JSON.stringify(feed)}, feedChatLog=${JSON.stringify(feedChatLog)}`)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("link_created", link => {
|
|
|
|
// TODO Figure out what this means
|
|
|
|
this.log(`Link created: ${JSON.stringify(link)}`)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("link_deleted", link => {
|
|
|
|
// TODO Figure out what this means
|
|
|
|
this.log(`Link deleted: ${JSON.stringify(link)}`)
|
|
|
|
})
|
|
|
|
|
2022-04-10 04:30:26 -04:00
|
|
|
this.#talkClient.on("user_join", (joinLog, channel, user, feed) => {
|
|
|
|
this.log(`User ${user.userId} joined channel ${channel.channelId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("user_join", {
|
2022-04-10 04:30:26 -04:00
|
|
|
userId: user.userId,
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("user_left", (leftLog, channel, user, feed) => {
|
|
|
|
this.log(`User ${user.userId} left channel ${channel.channelId}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("user_left", {
|
2022-04-10 04:30:26 -04:00
|
|
|
userId: user.userId,
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2022-04-13 13:02:55 -04:00
|
|
|
this.#talkClient.on("meta_change", (channel, type, newMeta) => {
|
2022-04-23 17:01:39 -04:00
|
|
|
// TODO Handle announcements as pinned messages
|
2022-04-13 13:02:55 -04:00
|
|
|
this.log(`Channel ${channel.channelId} metadata changed`)
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("push_packet", (method, data) => {
|
|
|
|
// TODO Find a better way to do this...but data doesn't have much.
|
|
|
|
if (method == "SYNCLINKUP") {
|
|
|
|
if (!data?.ol) return
|
|
|
|
const linkURL = data.ol?.lu
|
|
|
|
if (!linkURL) return
|
|
|
|
for (const channel of this.#talkClient.channelList.open.all()) {
|
|
|
|
if (channel.info.openLink?.linkURL == linkURL) {
|
|
|
|
this.write("channel_meta_change", {
|
|
|
|
info: {
|
|
|
|
name: data.ol?.ln,
|
|
|
|
description: data.ol?.desc || null,
|
|
|
|
photoURL: data.ol?.liu || null,
|
|
|
|
},
|
|
|
|
channelId: channel.channelId,
|
|
|
|
channelType: channel.info.type,
|
|
|
|
})
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2022-05-10 21:39:18 -04:00
|
|
|
this.#talkClient.on("disconnected", reason => {
|
2022-04-05 02:05:43 -04:00
|
|
|
this.log(`Disconnected (reason=${reason})`)
|
|
|
|
this.disconnect()
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("disconnected", {
|
2022-04-05 02:05:43 -04:00
|
|
|
reason: reason,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
this.#talkClient.on("switch_server", () => {
|
|
|
|
this.log(`Server switch requested`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("switch_server", {
|
2022-04-05 02:05:43 -04:00
|
|
|
is_sequential: true,
|
|
|
|
})
|
|
|
|
})
|
2022-04-10 02:18:53 -04:00
|
|
|
|
2022-05-10 21:39:18 -04:00
|
|
|
this.#talkClient.on("error", err => {
|
2022-04-10 02:18:53 -04:00
|
|
|
this.log(`Client error: ${err}`)
|
2022-04-13 03:15:28 -04:00
|
|
|
this.write("error", {
|
2022-04-10 02:18:53 -04:00
|
|
|
error: err,
|
|
|
|
})
|
|
|
|
})
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
2022-03-06 03:14:59 -05:00
|
|
|
/**
|
|
|
|
* @param {string} mxid The ID of the associated Matrix user
|
2022-04-01 05:07:05 -04:00
|
|
|
* @param {OAuthCredential} credential The token to log in with, obtained from prior login
|
|
|
|
* @param {PeerClient} peerClient What handles RPC
|
2022-03-06 03:14:59 -05:00
|
|
|
*/
|
2022-04-01 05:07:05 -04:00
|
|
|
static async create(mxid, credential, peerClient) {
|
2022-03-06 03:14:59 -05:00
|
|
|
this.#initializing = true
|
2022-04-23 13:51:39 -04:00
|
|
|
const userClient = new UserClient(mxid, peerClient)
|
2022-03-06 03:14:59 -05:00
|
|
|
|
2022-04-23 13:51:39 -04:00
|
|
|
await userClient.setCredential(credential)
|
2022-02-25 02:22:50 -05:00
|
|
|
return userClient
|
|
|
|
}
|
|
|
|
|
2022-04-01 05:07:05 -04:00
|
|
|
|
|
|
|
log(...text) {
|
|
|
|
console.log(`[API/${this.mxid}]`, ...text)
|
|
|
|
}
|
|
|
|
|
|
|
|
error(...text) {
|
|
|
|
console.error(`[API/${this.mxid}]`, ...text)
|
|
|
|
}
|
|
|
|
|
2022-04-23 13:51:39 -04:00
|
|
|
/**
|
|
|
|
* @param {OAuthCredential} credential
|
|
|
|
*/
|
|
|
|
async setCredential(credential) {
|
|
|
|
this.#serviceClient = await ServiceApiClient.create(credential)
|
|
|
|
this.#credential = credential
|
|
|
|
}
|
|
|
|
|
2022-03-21 01:33:22 -04:00
|
|
|
/**
|
2022-04-08 19:01:32 -04:00
|
|
|
* @param {ChannelProps} channelProps
|
2022-03-21 01:33:22 -04:00
|
|
|
*/
|
2022-04-08 19:01:32 -04:00
|
|
|
async getChannel(channelProps) {
|
2022-05-16 01:53:21 -04:00
|
|
|
const talkChannel = this.#talkClient.channelList.get(channelProps.id)
|
|
|
|
if (talkChannel) {
|
|
|
|
return talkChannel
|
|
|
|
}
|
|
|
|
|
|
|
|
const channelList = getChannelListForType(
|
|
|
|
this.#talkClient.channelList,
|
|
|
|
channelProps.type
|
|
|
|
)
|
|
|
|
const res = await channelList.addChannel({ channelId: channelProps.id })
|
|
|
|
if (!res.success) {
|
|
|
|
this.error(`Unable to add ${channelProps.type} channel ${channelProps.id}`)
|
|
|
|
throw res
|
|
|
|
}
|
|
|
|
return res.result
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Long} channelId
|
|
|
|
*/
|
|
|
|
async getNormalChannel(channelId) {
|
|
|
|
const channelList = this.#talkClient.channelList.normal
|
|
|
|
const talkChannel = channelList.get(channelId)
|
|
|
|
if (talkChannel) {
|
|
|
|
return talkChannel
|
|
|
|
}
|
|
|
|
|
|
|
|
const res = await channelList.addChannel({ channelId: channelId })
|
|
|
|
if (!res.success) {
|
|
|
|
this.error(`Unable to add normal channel ${channelProps.id}`)
|
|
|
|
throw res
|
2022-03-21 01:33:22 -04:00
|
|
|
}
|
2022-05-16 01:53:21 -04:00
|
|
|
return res.result
|
2022-03-21 01:33:22 -04:00
|
|
|
}
|
|
|
|
|
2022-04-01 05:07:05 -04:00
|
|
|
/**
|
2022-04-23 13:51:39 -04:00
|
|
|
* @param {?OAuthCredential} credential The token to log in with, obtained from prior login
|
2022-04-01 05:07:05 -04:00
|
|
|
*/
|
|
|
|
async connect(credential) {
|
|
|
|
// TODO Don't re-login if possible. But must still return a LoginResult!
|
|
|
|
this.disconnect()
|
2022-04-23 13:51:39 -04:00
|
|
|
if (credential && this.#credential != credential) {
|
|
|
|
await this.setCredential(credential)
|
|
|
|
}
|
2022-05-16 00:08:00 -04:00
|
|
|
const res = await this.#talkClient.login(this.#credential)
|
|
|
|
this.#connected = res.success
|
|
|
|
return res
|
2022-04-01 05:07:05 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
disconnect() {
|
2022-05-16 00:08:00 -04:00
|
|
|
if (this.isConnected()) {
|
2022-04-01 05:07:05 -04:00
|
|
|
this.#talkClient.close()
|
|
|
|
}
|
2022-05-16 00:08:00 -04:00
|
|
|
this.#connected = false
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
2022-04-28 01:42:50 -04:00
|
|
|
isConnected() {
|
|
|
|
return this.#talkClient?.logon || false
|
|
|
|
}
|
|
|
|
|
2022-05-16 00:08:00 -04:00
|
|
|
isUnexpectedlyDisconnected() {
|
|
|
|
return this.#connected && !this.isConnected()
|
|
|
|
}
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
/**
|
2022-04-01 05:07:05 -04:00
|
|
|
* Send a user-specific command with (optional) data to the socket.
|
|
|
|
*
|
|
|
|
* @param {string} command - The data to write.
|
|
|
|
* @param {?object} data - The data to write.
|
2022-02-25 02:22:50 -05:00
|
|
|
*/
|
2022-04-01 05:07:05 -04:00
|
|
|
write(command, data) {
|
|
|
|
return this.peerClient.write({
|
|
|
|
id: --this.peerClient.notificationID,
|
|
|
|
command: `${command}:${this.mxid}`,
|
|
|
|
...data
|
|
|
|
})
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class PeerClient {
|
|
|
|
/**
|
2022-03-21 01:10:31 -04:00
|
|
|
* @param {import("./clientmanager.js").default} manager
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {import("net").Socket} socket
|
|
|
|
* @param {number} connID
|
|
|
|
*/
|
|
|
|
constructor(manager, socket, connID) {
|
|
|
|
this.manager = manager
|
2022-05-15 22:17:28 -04:00
|
|
|
this.registerTimeout = manager.registerTimeout
|
|
|
|
this.loggingKeys = manager.loggingKeys
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
this.socket = socket
|
|
|
|
this.connID = connID
|
2022-03-21 01:10:31 -04:00
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
this.stopped = false
|
|
|
|
this.notificationID = 0
|
|
|
|
this.maxCommandID = 0
|
2022-05-10 01:16:03 -04:00
|
|
|
this.peerID = ""
|
2022-04-20 23:47:02 -04:00
|
|
|
this.deviceName = "KakaoTalk Bridge"
|
2022-03-21 01:10:31 -04:00
|
|
|
/** @type {Map<string, UserClient>} */
|
2022-02-25 02:22:50 -05:00
|
|
|
this.userClients = new Map()
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log(...text) {
|
|
|
|
if (this.peerID) {
|
|
|
|
console.log(`[API/${this.peerID}/${this.connID}]`, ...text)
|
|
|
|
} else {
|
|
|
|
console.log(`[API/${this.connID}]`, ...text)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
error(...text) {
|
|
|
|
if (this.peerID) {
|
|
|
|
console.error(`[API/${this.peerID}/${this.connID}]`, ...text)
|
|
|
|
} else {
|
|
|
|
console.error(`[API/${this.connID}]`, ...text)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
start() {
|
|
|
|
this.log("Received connection", this.connID)
|
|
|
|
emitLines(this.socket)
|
|
|
|
this.socket.on("line", line => this.handleLine(line)
|
|
|
|
.catch(err => this.log("Error handling line:", err)))
|
|
|
|
this.socket.on("end", this.handleEnd)
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!this.peerID && !this.stopped) {
|
2022-05-15 22:17:28 -04:00
|
|
|
this.log(`Didn't receive register request within ${this.registerTimeout/1000} seconds, terminating`)
|
2022-02-25 02:22:50 -05:00
|
|
|
this.stop("Register request timeout")
|
|
|
|
}
|
2022-05-15 22:17:28 -04:00
|
|
|
}, this.registerTimeout)
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
async stop(error = null) {
|
|
|
|
if (this.stopped) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.stopped = true
|
2022-03-18 03:52:55 -04:00
|
|
|
this.#closeUsers()
|
2022-02-25 02:22:50 -05:00
|
|
|
try {
|
2022-04-01 05:07:05 -04:00
|
|
|
await this.write({ id: --this.notificationID, command: "quit", error })
|
2022-02-25 02:22:50 -05:00
|
|
|
await promisify(cb => this.socket.end(cb))
|
|
|
|
} catch (err) {
|
|
|
|
this.error("Failed to end connection:", err)
|
|
|
|
this.socket.destroy(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-18 03:52:55 -04:00
|
|
|
handleEnd = () => {
|
2022-02-25 02:22:50 -05:00
|
|
|
this.stopped = true
|
2022-03-18 03:52:55 -04:00
|
|
|
this.#closeUsers()
|
2022-02-25 02:22:50 -05:00
|
|
|
if (this.peerID && this.manager.clients.get(this.peerID) === this) {
|
|
|
|
this.manager.clients.delete(this.peerID)
|
|
|
|
}
|
2022-05-10 01:16:03 -04:00
|
|
|
this.log(`Connection closed (peer: ${this.peerID || "unknown peer"})`)
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
2022-03-18 03:52:55 -04:00
|
|
|
#closeUsers() {
|
2022-05-10 01:16:03 -04:00
|
|
|
this.log(`Closing all API clients for ${this.peerID || "unknown peer"}`)
|
2022-03-18 03:52:55 -04:00
|
|
|
for (const userClient of this.userClients.values()) {
|
2022-04-01 05:07:05 -04:00
|
|
|
userClient.disconnect()
|
2022-03-18 03:52:55 -04:00
|
|
|
}
|
|
|
|
this.userClients.clear()
|
|
|
|
}
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
/**
|
|
|
|
* Write JSON data to the socket.
|
|
|
|
*
|
|
|
|
* @param {object} data - The data to write.
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
*/
|
2022-04-01 05:07:05 -04:00
|
|
|
write(data) {
|
2022-03-11 03:43:00 -05:00
|
|
|
return promisify(cb => this.socket.write(JSON.stringify(data, this.#writeReplacer) + "\n", cb))
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {string} req.passcode
|
|
|
|
* @param {string} req.uuid
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req.form
|
2022-02-25 02:22:50 -05:00
|
|
|
*/
|
|
|
|
registerDevice = async (req) => {
|
2022-03-18 03:52:55 -04:00
|
|
|
// TODO Look for a deregister API call
|
2022-02-25 02:22:50 -05:00
|
|
|
const authClient = await this.#createAuthClient(req.uuid)
|
|
|
|
return await authClient.registerDevice(req.form, req.passcode, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-04-01 05:07:05 -04:00
|
|
|
* Obtain login tokens. If this fails due to not having a device, also request a device passcode.
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {string} req.uuid
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req.form
|
2022-05-04 02:50:59 -04:00
|
|
|
* @param {boolean} req.forced
|
2022-02-25 02:22:50 -05:00
|
|
|
* @returns The response of the login attempt, including obtained
|
|
|
|
* credentials for subsequent token-based login. If a required device passcode
|
|
|
|
* request failed, its status is stored here.
|
|
|
|
*/
|
|
|
|
handleLogin = async (req) => {
|
|
|
|
const authClient = await this.#createAuthClient(req.uuid)
|
2022-05-04 02:50:59 -04:00
|
|
|
const loginRes = await authClient.login(req.form, req.forced)
|
2022-02-25 02:22:50 -05:00
|
|
|
if (loginRes.status === KnownAuthStatusCode.DEVICE_NOT_REGISTERED) {
|
|
|
|
const passcodeRes = await authClient.requestPasscode(req.form)
|
|
|
|
if (!passcodeRes.success) {
|
|
|
|
loginRes.status = passcodeRes.status
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return loginRes
|
|
|
|
}
|
|
|
|
|
2022-03-09 02:25:28 -05:00
|
|
|
/**
|
|
|
|
* TODO Consider caching per-user
|
|
|
|
* @param {string} uuid
|
|
|
|
*/
|
|
|
|
async #createAuthClient(uuid) {
|
2022-09-05 23:55:27 -04:00
|
|
|
return await AuthApiClient.create(this.deviceName, uuid, {}, modutil.ModXVCProvider)
|
2022-03-09 02:25:28 -05:00
|
|
|
}
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
// TODO Wrapper for per-user commands
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checked lookup of a UserClient for a given mxid.
|
|
|
|
* @param {string} mxid
|
2022-03-06 03:14:59 -05:00
|
|
|
* @returns {UserClient}
|
2022-02-25 02:22:50 -05:00
|
|
|
*/
|
|
|
|
#getUser(mxid) {
|
|
|
|
const userClient = this.userClients.get(mxid)
|
|
|
|
if (userClient === undefined) {
|
|
|
|
throw new Error(`Could not find user ${mxid}`)
|
|
|
|
}
|
|
|
|
return userClient
|
|
|
|
}
|
2022-03-06 03:14:59 -05:00
|
|
|
|
2022-03-18 03:52:55 -04:00
|
|
|
/**
|
|
|
|
* Unchecked lookup of a UserClient for a given mxid.
|
|
|
|
* @param {string} mxid
|
|
|
|
* @returns {UserClient | undefined}
|
|
|
|
*/
|
|
|
|
#tryGetUser(mxid) {
|
|
|
|
return this.userClients.get(mxid)
|
|
|
|
}
|
|
|
|
|
2022-04-05 00:59:22 -04:00
|
|
|
/**
|
|
|
|
* @param {string} mxid
|
2022-04-08 19:01:32 -04:00
|
|
|
* @param {ChannelProps} channelProps
|
2022-04-14 00:55:58 -04:00
|
|
|
* @param {?OpenChannelUserPerm[]} permNeeded If set, throw if the user's permission level matches none of the values in this list.
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {?string} action The action requiring permission, to be used in an error message if throwing.
|
2022-04-14 00:55:58 -04:00
|
|
|
* @throws {PermError} if the user does not have the specified permission level.
|
|
|
|
*/
|
|
|
|
async #getUserChannel(mxid, channelProps, permNeeded, action) {
|
|
|
|
const userClient = this.#getUser(mxid)
|
|
|
|
const talkChannel = await userClient.getChannel(channelProps)
|
|
|
|
if (permNeeded) {
|
2022-05-16 01:53:21 -04:00
|
|
|
await this.#requireChannelPerm(talkChannel, permNeeded, action)
|
2022-04-14 00:55:58 -04:00
|
|
|
}
|
|
|
|
return talkChannel
|
2022-04-05 00:59:22 -04:00
|
|
|
}
|
|
|
|
|
2022-05-16 01:53:21 -04:00
|
|
|
/**
|
|
|
|
* @param {TalkOpenChannel} talkChannel
|
|
|
|
* @param {OpenChannelUserPerm[]} permNeeded Throw if the user's permission level matches none of the values in this list.
|
|
|
|
* @param {string} action The action requiring permission
|
|
|
|
* @throws {PermError} if the user does not have the specified permission level.
|
|
|
|
*/
|
|
|
|
async #requireChannelPerm(talkChannel, permNeeded, action) {
|
|
|
|
const permActual = talkChannel.getUserInfo({ userId: talkChannel.clientUser.userId }).perm
|
|
|
|
if (permNeeded.indexOf(permActual) == -1) {
|
|
|
|
throw new PermError(permNeeded, permActual, action)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} mxid
|
|
|
|
* @param {Long} channelId
|
|
|
|
*/
|
|
|
|
async #getUserNormalChannel(mxid, channelId) {
|
|
|
|
return await this.#getUser(mxid).getNormalChannel(channelId)
|
|
|
|
}
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-23 13:51:39 -04:00
|
|
|
* @param {string} req.mxid
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {OAuthCredential} req.oauth_credential
|
|
|
|
*/
|
|
|
|
handleRenew = async (req) => {
|
2022-04-23 13:51:39 -04:00
|
|
|
const userClient = this.#tryGetUser(req.mxid)
|
2022-02-25 02:22:50 -05:00
|
|
|
const oAuthClient = await OAuthApiClient.create()
|
2022-04-23 13:51:39 -04:00
|
|
|
const res = await oAuthClient.renew(req.oauth_credential)
|
|
|
|
if (res.success && userClient) {
|
2022-05-02 02:50:49 -04:00
|
|
|
await userClient.setCredential(res.result.credential)
|
2022-04-23 13:51:39 -04:00
|
|
|
}
|
|
|
|
return res
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {OAuthCredential} req.oauth_credential
|
|
|
|
*/
|
2022-04-01 05:07:05 -04:00
|
|
|
userStart = async (req) => {
|
2022-04-23 13:51:39 -04:00
|
|
|
let userClient = this.#tryGetUser(req.mxid)
|
|
|
|
if (!userClient) {
|
|
|
|
userClient = await UserClient.create(req.mxid, req.oauth_credential, this)
|
|
|
|
} else {
|
|
|
|
await userClient.setCredential(req.oauth_credential)
|
|
|
|
}
|
2022-04-10 22:56:51 -04:00
|
|
|
const res = await this.#getSettings(userClient.serviceClient)
|
2022-04-01 05:07:05 -04:00
|
|
|
if (res.success) {
|
|
|
|
this.userClients.set(req.mxid, userClient)
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
2022-02-25 02:22:50 -05:00
|
|
|
|
2022-04-01 05:07:05 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-01 05:07:05 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
*/
|
|
|
|
userStop = async (req) => {
|
|
|
|
this.handleDisconnect(req)
|
|
|
|
this.userClients.delete(req.mxid)
|
|
|
|
}
|
2022-02-25 02:22:50 -05:00
|
|
|
|
2022-04-01 05:07:05 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-01 05:07:05 -04:00
|
|
|
* @param {string} req.mxid
|
2022-04-23 13:51:39 -04:00
|
|
|
* @param {?OAuthCredential} req.oauth_credential
|
2022-04-01 05:07:05 -04:00
|
|
|
*/
|
|
|
|
handleConnect = async (req) => {
|
|
|
|
return await this.#getUser(req.mxid).connect(req.oauth_credential)
|
2022-03-18 03:52:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-03-18 03:52:55 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
*/
|
|
|
|
handleDisconnect = (req) => {
|
2022-04-01 05:07:05 -04:00
|
|
|
this.#tryGetUser(req.mxid)?.disconnect()
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
2022-04-28 01:42:50 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-28 01:42:50 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
*/
|
|
|
|
isConnected = (req) => {
|
|
|
|
return this.#tryGetUser(req.mxid)?.isConnected()
|
|
|
|
}
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-10 22:56:51 -04:00
|
|
|
* @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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-10 22:56:51 -04:00
|
|
|
* @param {string} req.mxid
|
2022-02-25 02:22:50 -05:00
|
|
|
*/
|
|
|
|
getOwnProfile = async (req) => {
|
2022-04-01 05:07:05 -04:00
|
|
|
return await this.#getUser(req.mxid).serviceClient.requestMyProfile()
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-10 22:56:51 -04:00
|
|
|
* @param {string} req.mxid
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {Long} req.user_id
|
|
|
|
*/
|
|
|
|
getProfile = async (req) => {
|
2022-04-01 05:07:05 -04:00
|
|
|
return await this.#getUser(req.mxid).serviceClient.requestProfile(req.user_id)
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {string} req.mxid
|
2022-04-08 19:01:32 -04:00
|
|
|
* @param {ChannelProps} req.channel_props
|
2022-02-25 02:22:50 -05:00
|
|
|
*/
|
2022-03-09 02:25:28 -05:00
|
|
|
getPortalChannelInfo = async (req) => {
|
2022-04-05 00:59:22 -04:00
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
2022-02-25 02:22:50 -05:00
|
|
|
|
|
|
|
const res = await talkChannel.updateAll()
|
|
|
|
if (!res.success) return res
|
|
|
|
|
2022-04-10 22:56:51 -04:00
|
|
|
return makeCommandResult({
|
2022-02-25 02:22:50 -05:00
|
|
|
name: talkChannel.getDisplayName(),
|
2022-04-13 13:02:55 -04:00
|
|
|
description: talkChannel.info.openLink?.description,
|
|
|
|
// TODO Find out why linkCoverURL is blank, despite having updated the channel!
|
|
|
|
photoURL: talkChannel.info.openLink?.linkCoverURL || null,
|
2022-04-28 01:32:12 -04:00
|
|
|
participantInfo: {
|
|
|
|
// TODO Get members from chatON?
|
|
|
|
participants: Array.from(talkChannel.getAllUserInfo()),
|
|
|
|
kickedUserIds: await this.#getKickedUserIds(talkChannel),
|
|
|
|
},
|
2022-02-25 02:22:50 -05:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-28 01:32:12 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-28 01:32:12 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
*/
|
|
|
|
getPortalChannelParticipantInfo = async (req) => {
|
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
|
|
|
|
|
|
|
// TODO Get members from chatON?
|
|
|
|
const participantRes = await talkChannel.getAllLatestUserInfo()
|
|
|
|
if (!participantRes.success) return participantRes
|
|
|
|
|
|
|
|
return {
|
|
|
|
participants: participantRes.result,
|
|
|
|
kickedUserIds: await this.#getKickedUserIds(talkChannel),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {TalkNormalChannel | TalkOpenChannel} talkChannel
|
|
|
|
*/
|
|
|
|
async #getKickedUserIds(talkChannel) {
|
|
|
|
if (!isChannelTypeOpen(talkChannel.info.type)) {
|
|
|
|
return []
|
|
|
|
} else {
|
|
|
|
const kickListRes = await talkChannel.getKickList()
|
|
|
|
if (!kickListRes.success) {
|
|
|
|
return []
|
|
|
|
} else {
|
|
|
|
return kickListRes.result.map(kickUser => kickUser.userId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-20 03:12:17 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-03-20 03:12:17 -04:00
|
|
|
* @param {string} req.mxid
|
2022-04-08 19:01:32 -04:00
|
|
|
* @param {ChannelProps} req.channel_props
|
2022-03-20 03:12:17 -04:00
|
|
|
*/
|
|
|
|
getParticipants = async (req) => {
|
2022-04-05 00:59:22 -04:00
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
2022-03-20 03:12:17 -04:00
|
|
|
return await talkChannel.getAllLatestUserInfo()
|
|
|
|
}
|
|
|
|
|
2022-03-09 02:25:28 -05:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-03-09 02:25:28 -05:00
|
|
|
* @param {string} req.mxid
|
2022-04-08 19:01:32 -04:00
|
|
|
* @param {ChannelProps} req.channel_props
|
2022-03-23 03:09:30 -04:00
|
|
|
* @param {?Long} req.sync_from
|
2022-04-13 01:14:10 -04:00
|
|
|
* @param {?number} req.limit
|
2022-03-09 02:25:28 -05:00
|
|
|
*/
|
|
|
|
getChats = async (req) => {
|
2022-04-05 00:59:22 -04:00
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
2022-03-09 02:25:28 -05:00
|
|
|
|
2022-03-12 23:26:23 -05:00
|
|
|
const res = await talkChannel.getChatListFrom(req.sync_from)
|
2022-04-23 13:58:18 -04:00
|
|
|
if (!res.success || !res.length) return res
|
2022-04-13 01:14:10 -04:00
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const nextRes = await talkChannel.getChatListFrom(
|
|
|
|
res.result[res.result.length - 1].logId
|
|
|
|
)
|
|
|
|
if (!nextRes.success) return nextRes
|
|
|
|
if (!nextRes.result.length) break
|
|
|
|
res.result.push(...nextRes.result)
|
|
|
|
}
|
|
|
|
if (0 < req.limit && req.limit < res.result.length) {
|
2022-03-12 23:26:23 -05:00
|
|
|
res.result.splice(0, res.result.length - req.limit)
|
|
|
|
}
|
|
|
|
return res
|
2022-03-09 02:25:28 -05:00
|
|
|
}
|
|
|
|
|
2022-05-05 03:00:41 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-05-05 03:00:41 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {[Long]} req.unread_chat_ids Must be in DECREASING order
|
|
|
|
*/
|
|
|
|
getReadReceipts = async (req) => {
|
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
|
|
|
// TODO Is any pre-syncing needed?
|
|
|
|
const userCount = talkChannel.userCount
|
|
|
|
if (userCount == 1) return makeCommandResult([])
|
|
|
|
/** @type {Map<Long, Long> */
|
|
|
|
const latestReceiptByUser = new Map()
|
|
|
|
let fullyRead = false
|
|
|
|
for (const chatId of req.unread_chat_ids) {
|
|
|
|
const chatReaders = talkChannel.getReaders({ logId: chatId })
|
|
|
|
for (const chatReader of chatReaders) {
|
|
|
|
if (!latestReceiptByUser.has(chatReader.userId)) {
|
|
|
|
latestReceiptByUser.set(chatReader.userId, chatId)
|
|
|
|
if (latestReceiptByUser.size == userCount) {
|
|
|
|
fullyRead = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (fullyRead) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @typedef {object} Receipt
|
2022-05-05 03:00:41 -04:00
|
|
|
* @property {Long} userId
|
|
|
|
* @property {Long} chatId
|
|
|
|
*/
|
|
|
|
/** @type {[Receipt]} */
|
|
|
|
const receipts = []
|
|
|
|
latestReceiptByUser.forEach((value, key) => receipts.push({ "userId": key, "chatId": value }))
|
|
|
|
return makeCommandResult(receipts)
|
|
|
|
}
|
|
|
|
|
2022-05-06 02:45:00 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-05-06 02:45:00 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {string} req.uuid
|
|
|
|
*/
|
|
|
|
canChangeUUID = async (req) => {
|
|
|
|
return await this.#getUser(req.mxid).serviceClient.canChangeUUID(req.uuid)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-05-06 02:45:00 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {string} req.uuid
|
|
|
|
*/
|
|
|
|
changeUUID = async (req) => {
|
|
|
|
const serviceClient = this.#getUser(req.mxid).serviceClient
|
|
|
|
|
|
|
|
const checkRes = await serviceClient.canChangeUUID(req.uuid)
|
|
|
|
if (!checkRes.success) return checkRes
|
|
|
|
|
|
|
|
return await serviceClient.changeUUID(req.uuid)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-05-06 02:45:00 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {boolean} req.searchable
|
|
|
|
*/
|
|
|
|
setUUIDSearchable = async (req) => {
|
|
|
|
const serviceClient = this.#getUser(req.mxid).serviceClient
|
|
|
|
const moreRes = await serviceClient.requestMoreSettings()
|
|
|
|
if (!moreRes.success) {
|
|
|
|
throw new ProtocolError("Error checking status of KakaoTalk ID")
|
|
|
|
}
|
|
|
|
if (!moreRes.result.uuid) {
|
|
|
|
throw new ProtocolError("You do not yet have a KakaoTalk ID")
|
|
|
|
}
|
|
|
|
if (req.searchable == moreRes.result.uuidSearchable) {
|
|
|
|
throw new ProtocolError(`Your KakaoTalk ID is already ${req.searchable ? "searchable" : "hidden"}`)
|
|
|
|
}
|
|
|
|
return await serviceClient.updateSettings({
|
|
|
|
uuid_searchable: req.searchable,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-23 03:09:30 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-04 20:07:30 -04:00
|
|
|
* @param {string} req.mxid
|
2022-03-23 03:09:30 -04:00
|
|
|
*/
|
|
|
|
listFriends = async (req) => {
|
2022-04-01 05:07:05 -04:00
|
|
|
return await this.#getUser(req.mxid).serviceClient.requestFriendList()
|
2022-03-23 03:09:30 -04:00
|
|
|
}
|
|
|
|
|
2022-05-06 00:57:01 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-05-06 00:57:01 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {Long} req.user_id
|
|
|
|
* @param {boolean} req.add
|
|
|
|
*/
|
|
|
|
editFriend = async (req) => {
|
|
|
|
return await this.#editFriend(
|
|
|
|
this.#getUser(req.mxid).serviceClient,
|
|
|
|
req.user_id,
|
|
|
|
req.add
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-05-06 00:57:01 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {string} req.uuid
|
|
|
|
* @param {boolean} req.add
|
|
|
|
*/
|
|
|
|
editFriendByUUID = async (req) => {
|
|
|
|
const serviceClient = this.#getUser(req.mxid).serviceClient
|
|
|
|
|
|
|
|
const res = await serviceClient.findFriendByUUID(req.uuid)
|
|
|
|
if (!res.success) return res
|
|
|
|
|
|
|
|
return await this.#editFriend(
|
|
|
|
serviceClient,
|
|
|
|
res.result.member.userId instanceof Long
|
|
|
|
? res.result.member.userId
|
|
|
|
: Long.fromNumber(res.result.member.userId),
|
|
|
|
req.add
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {ServiceApiClient} serviceClient
|
|
|
|
* @param {Long} id
|
|
|
|
* @param {boolean} add
|
|
|
|
*/
|
|
|
|
async #editFriend(serviceClient, id, add) {
|
|
|
|
const listRes = await serviceClient.requestFriendList()
|
|
|
|
if (listRes.success) {
|
|
|
|
const isFriend = -1 != listRes.result.friends.findIndex(friend => id.equals(friend.userId))
|
|
|
|
if (isFriend == add) {
|
|
|
|
throw new ProtocolError(`User is already ${add ? "in" : "absent from"} friends list`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return add ? await serviceClient.addFriend(id) : await serviceClient.removeFriend(id)
|
|
|
|
}
|
|
|
|
|
2022-04-04 20:07:30 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-04 20:07:30 -04:00
|
|
|
* @param {string} req.mxid The user whose friend is being looked up.
|
|
|
|
* @param {string} req.friend_id The friend to search for.
|
|
|
|
* @param {string} propertyName The property to retrieve from the specified friend.
|
|
|
|
*/
|
|
|
|
getFriendProperty = async (req, propertyName) => {
|
|
|
|
const res = await this.#getUser(req.mxid).serviceClient.findFriendById(req.friend_id)
|
|
|
|
if (!res.success) return res
|
|
|
|
|
2022-04-10 22:56:51 -04:00
|
|
|
return makeCommandResult(res.result.friend[propertyName])
|
2022-04-04 20:07:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-04 20:07:30 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
*/
|
|
|
|
getMemoIds = (req) => {
|
|
|
|
/** @type Long[] */
|
|
|
|
const channelIds = []
|
|
|
|
const channelList = this.#getUser(req.mxid).talkClient.channelList
|
2022-05-06 04:25:08 -04:00
|
|
|
// TODO channelList.all() doesn't really return *all* channels...
|
2022-04-04 20:07:30 -04:00
|
|
|
for (const channel of channelList.all()) {
|
|
|
|
if (channel.info.type == "MemoChat") {
|
|
|
|
channelIds.push(channel.channelId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return channelIds
|
|
|
|
}
|
|
|
|
|
2022-04-28 01:36:15 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-28 01:36:15 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {string} req.key
|
|
|
|
*/
|
|
|
|
downloadFile = async (req) => {
|
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
|
|
|
const res = await talkChannel.downloadMedia({ key: req.key }, KnownChatType.FILE)
|
|
|
|
if (!res.success) return res
|
|
|
|
|
|
|
|
const data = await ReadStreamUtil.all(res.result)
|
|
|
|
return makeCommandResult(Array.from(data))
|
|
|
|
}
|
|
|
|
|
2022-03-09 20:26:39 -05:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-03-09 20:26:39 -05:00
|
|
|
* @param {string} req.mxid
|
2022-04-08 19:01:32 -04:00
|
|
|
* @param {ChannelProps} req.channel_props
|
2022-03-09 20:26:39 -05:00
|
|
|
* @param {string} req.text
|
2022-04-05 15:44:02 -04:00
|
|
|
* @param {?ReplyAttachment} req.reply_to
|
2022-04-06 12:49:23 -04:00
|
|
|
* @param {?MentionStruct[]} req.mentions
|
2022-03-09 20:26:39 -05:00
|
|
|
*/
|
2022-04-05 00:59:22 -04:00
|
|
|
sendChat = async (req) => {
|
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
2022-04-12 04:34:16 -04:00
|
|
|
const res = await talkChannel.sendChat({
|
2022-03-09 20:26:39 -05:00
|
|
|
text: req.text,
|
2022-04-05 15:44:02 -04:00
|
|
|
type: !!req.reply_to ? KnownChatType.REPLY : KnownChatType.TEXT,
|
2022-04-06 12:49:23 -04:00
|
|
|
attachment: !req.mentions ? req.reply_to : {...req.reply_to, mentions: req.mentions},
|
2022-03-09 20:26:39 -05:00
|
|
|
})
|
2022-04-12 04:34:16 -04:00
|
|
|
if (res.success) res.result = res.result.logId
|
|
|
|
return res
|
2022-03-09 20:26:39 -05:00
|
|
|
}
|
|
|
|
|
2022-03-26 03:37:53 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-03-26 03:37:53 -04:00
|
|
|
* @param {string} req.mxid
|
2022-04-08 19:01:32 -04:00
|
|
|
* @param {ChannelProps} req.channel_props
|
2022-03-26 03:37:53 -04:00
|
|
|
* @param {int} req.type
|
2022-04-06 12:49:23 -04:00
|
|
|
* @param {number[]} req.data
|
2022-03-26 03:37:53 -04:00
|
|
|
* @param {string} req.name
|
|
|
|
* @param {?int} req.width
|
|
|
|
* @param {?int} req.height
|
|
|
|
* @param {?string} req.ext
|
|
|
|
*/
|
|
|
|
sendMedia = async (req) => {
|
2022-04-05 00:59:22 -04:00
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
2022-04-12 04:34:16 -04:00
|
|
|
const res = await talkChannel.sendMedia(req.type, {
|
2022-03-26 03:37:53 -04:00
|
|
|
data: Uint8Array.from(req.data),
|
|
|
|
name: req.name,
|
|
|
|
width: req.width,
|
|
|
|
height: req.height,
|
|
|
|
ext: req.ext,
|
|
|
|
})
|
2022-04-12 04:34:16 -04:00
|
|
|
if (res.success) res.result = res.result.logId
|
|
|
|
return res
|
2022-03-26 03:37:53 -04:00
|
|
|
}
|
|
|
|
|
2022-04-09 04:02:51 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-09 04:02:51 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {Long} req.chat_id
|
|
|
|
*/
|
|
|
|
deleteChat = async (req) => {
|
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
|
|
|
|
|
|
|
return await talkChannel.deleteChat({
|
|
|
|
logId: req.chat_id,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-10 04:26:09 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-10 04:26:09 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {Long} req.read_until_chat_id
|
|
|
|
*/
|
|
|
|
markRead = async (req) => {
|
|
|
|
const talkChannel = await this.#getUserChannel(req.mxid, req.channel_props)
|
|
|
|
|
|
|
|
return await talkChannel.markRead({
|
|
|
|
logId: req.read_until_chat_id,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-13 01:12:56 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-13 01:12:56 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {Long} req.user_id
|
|
|
|
* @param {OpenChannelUserPerm} req.perm
|
|
|
|
*/
|
|
|
|
sendPerm = async (req) => {
|
2022-04-14 00:55:58 -04:00
|
|
|
const talkChannel = await this.#getUserChannel(
|
|
|
|
req.mxid,
|
|
|
|
req.channel_props,
|
|
|
|
[OpenChannelUserPerm.OWNER],
|
|
|
|
"change user permissions"
|
|
|
|
)
|
2022-04-28 01:32:12 -04:00
|
|
|
const user = { userId: req.user_id }
|
|
|
|
if (!talkChannel.getUserInfo(user)) {
|
|
|
|
throw new ProtocolError("Cannot set permission level of a user that is not a channel participant")
|
|
|
|
}
|
2022-05-10 21:37:15 -04:00
|
|
|
if (req.user_id.equals(talkChannel.clientUser.userId)) {
|
2022-04-28 01:32:12 -04:00
|
|
|
throw new ProtocolError("Cannot change own permission level")
|
|
|
|
}
|
|
|
|
return await talkChannel.setUserPerm(user, req.perm)
|
2022-04-13 01:12:56 -04:00
|
|
|
}
|
|
|
|
|
2022-04-14 03:45:01 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-14 03:45:01 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {string} req.name
|
|
|
|
*/
|
|
|
|
setChannelName = async (req) => {
|
2022-05-16 01:53:21 -04:00
|
|
|
if (!isChannelTypeOpen(req.channel_props.type)) {
|
|
|
|
const talkChannel = await this.#getUserNormalChannel(req.mxid, req.channel_props.id)
|
|
|
|
return await talkChannel.setTitleMeta(req.name)
|
|
|
|
} else {
|
|
|
|
return await this.#setOpenChannelProperty(req.mxid, req.channel_props, "linkName", req.name)
|
|
|
|
}
|
2022-04-14 03:45:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-14 03:45:01 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {string} req.description
|
|
|
|
*/
|
|
|
|
setChannelDescription = async (req) => {
|
2022-05-16 01:53:21 -04:00
|
|
|
return await this.#setOpenChannelProperty(req.mxid, req.channel_props, "description", req.description)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} mxid
|
|
|
|
* @param {ChannelProps} channelProps
|
|
|
|
* @param {string} propertyName
|
|
|
|
* @param {any} propertyValue
|
|
|
|
*/
|
|
|
|
async #setOpenChannelProperty(mxid, channelProps, propertyName, propertyValue) {
|
|
|
|
if (isChannelTypeOpen(channelProps)) {
|
|
|
|
throw ProtocolError(`Cannot set ${propertyName} of non-open channel ${channelProps.id} (type = ${channelProps.type})`)
|
|
|
|
}
|
|
|
|
|
|
|
|
const userClient = this.#getUser(mxid)
|
|
|
|
/** @type {TalkOpenChannel} */
|
|
|
|
const talkChannel = await userClient.getChannel(channelProps)
|
|
|
|
this.#requireChannelPerm(talkChannel, [OpenChannelUserPerm.OWNER], `change channel ${propertyName}`)
|
|
|
|
|
|
|
|
const linkRes = await talkChannel.getLatestOpenLink()
|
|
|
|
if (!linkRes.success) throw linkRes
|
|
|
|
|
|
|
|
const link = linkRes.result
|
|
|
|
link[propertyName] = propertyValue
|
|
|
|
return await userClient.talkClient.channelList.open.updateOpenLink(
|
|
|
|
{ linkId: link.linkId }, link
|
2022-04-14 03:45:01 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-05-12 03:44:57 -04:00
|
|
|
/*
|
|
|
|
* TODO
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-14 03:45:01 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
* @param {string} req.photo_url
|
|
|
|
setChannelPhoto = async (req) => {
|
|
|
|
const talkChannel = await this.#getUserChannel(
|
|
|
|
req.mxid,
|
|
|
|
req.channel_props,
|
|
|
|
[OpenChannelUserPerm.OWNER],
|
|
|
|
"change channel photo"
|
|
|
|
)
|
|
|
|
return await talkChannel.setProfileMeta({
|
|
|
|
imageUrl: req.photo_url,
|
|
|
|
fullImageUrl: req.photo_url,
|
|
|
|
})
|
|
|
|
}
|
2022-05-12 03:44:57 -04:00
|
|
|
*/
|
2022-04-14 03:45:01 -04:00
|
|
|
|
2022-05-06 04:25:08 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-05-06 04:25:08 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {Long} req.user_id
|
|
|
|
*/
|
|
|
|
createDirectChat = async (req) => {
|
2022-05-10 01:06:42 -04:00
|
|
|
const userClient = this.#getUser(req.mxid)
|
2022-05-16 02:00:30 -04:00
|
|
|
await this.#requireInFriendsList(userClient.serviceClient, req.user_id)
|
2022-05-10 01:06:42 -04:00
|
|
|
|
2022-05-16 02:00:30 -04:00
|
|
|
const channelList = userClient.talkClient.channelList.normal
|
2022-05-10 21:33:11 -04:00
|
|
|
const createChannel =
|
|
|
|
!req.user_id.equals(userClient.userId)
|
|
|
|
? channelList.createChannel.bind(channelList, {
|
2022-05-10 01:06:42 -04:00
|
|
|
userList: [{ userId: req.user_id }],
|
|
|
|
})
|
2022-05-10 21:33:11 -04:00
|
|
|
: channelList.createMemoChannel.bind(channelList)
|
|
|
|
|
|
|
|
const retry_delay = 2000
|
|
|
|
let retries_left = 1
|
|
|
|
let res
|
|
|
|
do {
|
|
|
|
res = await createChannel()
|
|
|
|
if (res.success) {
|
|
|
|
return makeCommandResult(res.result.channelId)
|
|
|
|
}
|
|
|
|
this.error(`Failed to create direct chat, try again in ${retry_delay} ms (${retries_left - 1} tries remaining)`)
|
|
|
|
await new Promise(resolve => setTimeout(resolve, retry_delay))
|
|
|
|
} while (retries_left--)
|
2022-05-10 01:06:42 -04:00
|
|
|
|
2022-05-10 21:33:11 -04:00
|
|
|
this.error(`Failed to create direct chat, not retrying`)
|
|
|
|
return res
|
2022-05-06 04:25:08 -04:00
|
|
|
}
|
|
|
|
|
2022-05-16 02:00:30 -04:00
|
|
|
/**
|
|
|
|
* @param {ServiceApiClient} serviceClient
|
|
|
|
* @param {Long} id
|
|
|
|
*/
|
|
|
|
async #requireInFriendsList(serviceClient, id) {
|
|
|
|
let listRes = await serviceClient.requestFriendList()
|
|
|
|
if (!listRes.success) {
|
|
|
|
this.error("Failed to check friends list")
|
|
|
|
throw listRes
|
|
|
|
}
|
|
|
|
const isFriend = -1 != listRes.result.friends.findIndex(friend => id.equals(friend.userId))
|
|
|
|
if (!isFriend) {
|
|
|
|
throw new ProtocolError("This user is not in your friends list")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-28 03:22:14 -04:00
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-04-28 03:22:14 -04:00
|
|
|
* @param {string} req.mxid
|
|
|
|
* @param {ChannelProps} req.channel_props
|
|
|
|
*/
|
|
|
|
leaveChannel = async (req) => {
|
|
|
|
const userClient = this.#getUser(req.mxid)
|
|
|
|
const channelList = getChannelListForType(
|
|
|
|
userClient.talkClient.channelList,
|
|
|
|
req.channel_props.type
|
|
|
|
)
|
2022-04-28 22:28:53 -04:00
|
|
|
return await channelList.leaveChannel({ channelId: req.channel_props.id })
|
2022-04-28 03:22:14 -04:00
|
|
|
}
|
|
|
|
|
2022-02-25 02:22:50 -05:00
|
|
|
handleUnknownCommand = () => {
|
|
|
|
throw new Error("Unknown command")
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req
|
2022-02-25 02:22:50 -05:00
|
|
|
* @param {string} req.peer_id
|
2022-05-15 22:21:49 -04:00
|
|
|
* @param {object} req.register_config
|
2022-04-20 23:47:02 -04:00
|
|
|
* @param {string} req.register_config.device_name
|
2022-02-25 02:22:50 -05:00
|
|
|
*/
|
|
|
|
handleRegister = async (req) => {
|
|
|
|
this.peerID = req.peer_id
|
2022-04-20 23:47:02 -04:00
|
|
|
this.deviceName = req.register_config.device_name || this.deviceName
|
2022-02-25 02:22:50 -05:00
|
|
|
this.log(`Registered socket ${this.connID} -> ${this.peerID}`)
|
|
|
|
if (this.manager.clients.has(this.peerID)) {
|
|
|
|
const oldClient = this.manager.clients.get(this.peerID)
|
|
|
|
this.log(`Terminating previous socket ${oldClient.connID} for ${this.peerID}`)
|
|
|
|
await oldClient.stop("Socket replaced by new connection")
|
|
|
|
}
|
|
|
|
this.manager.clients.set(this.peerID, this)
|
|
|
|
return { client_exists: this.authClient !== null }
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleLine(line) {
|
|
|
|
if (this.stopped) {
|
|
|
|
this.log("Ignoring line, client is stopped")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let req
|
|
|
|
try {
|
2022-03-11 03:43:00 -05:00
|
|
|
req = JSON.parse(line, this.#readReviver)
|
2022-02-25 02:22:50 -05:00
|
|
|
} catch (err) {
|
|
|
|
this.log("Non-JSON request:", line)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (!req.command || !req.id) {
|
|
|
|
this.log("Invalid request:", line)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (req.id <= this.maxCommandID) {
|
|
|
|
this.log("Ignoring old request", req.id)
|
|
|
|
return
|
|
|
|
}
|
2022-05-15 22:17:28 -04:00
|
|
|
this.log(`Request ${req.id}:`, this.#logObj(req, req.command, this.loggingKeys.request))
|
2022-02-25 02:22:50 -05:00
|
|
|
this.maxCommandID = req.id
|
|
|
|
let handler
|
|
|
|
if (!this.peerID) {
|
|
|
|
if (req.command !== "register") {
|
|
|
|
this.log("First request wasn't a register request, terminating")
|
|
|
|
await this.stop("Invalid first request")
|
|
|
|
return
|
|
|
|
} else if (!req.peer_id) {
|
|
|
|
this.log("Register request didn't contain ID, terminating")
|
|
|
|
await this.stop("Invalid register request")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
handler = this.handleRegister
|
|
|
|
} else {
|
|
|
|
handler = {
|
2022-03-18 03:52:55 -04:00
|
|
|
// TODO Wrapper for per-user commands
|
2022-02-25 02:22:50 -05:00
|
|
|
generate_uuid: util.randomAndroidSubDeviceUUID,
|
|
|
|
register_device: this.registerDevice,
|
2022-03-18 03:52:55 -04:00
|
|
|
login: this.handleLogin,
|
|
|
|
renew: this.handleRenew,
|
2022-04-01 05:07:05 -04:00
|
|
|
start: this.userStart,
|
|
|
|
stop: this.userStop,
|
2022-03-18 03:52:55 -04:00
|
|
|
connect: this.handleConnect,
|
|
|
|
disconnect: this.handleDisconnect,
|
2022-04-28 01:42:50 -04:00
|
|
|
is_connected: this.isConnected,
|
2022-04-10 22:56:51 -04:00
|
|
|
get_settings: this.getSettings,
|
2022-02-25 02:22:50 -05:00
|
|
|
get_own_profile: this.getOwnProfile,
|
2022-03-18 03:52:55 -04:00
|
|
|
get_profile: this.getProfile,
|
2022-02-25 02:22:50 -05:00
|
|
|
get_portal_channel_info: this.getPortalChannelInfo,
|
2022-04-28 01:32:12 -04:00
|
|
|
get_portal_channel_participant_info: this.getPortalChannelParticipantInfo,
|
2022-03-20 03:12:17 -04:00
|
|
|
get_participants: this.getParticipants,
|
2022-03-09 02:25:28 -05:00
|
|
|
get_chats: this.getChats,
|
2022-05-05 03:00:41 -04:00
|
|
|
get_read_receipts: this.getReadReceipts,
|
2022-05-06 02:45:00 -04:00
|
|
|
can_change_uuid: this.canChangeUUID,
|
|
|
|
change_uuid: this.changeUUID,
|
|
|
|
set_uuid_searchable: this.setUUIDSearchable,
|
2022-03-23 03:09:30 -04:00
|
|
|
list_friends: this.listFriends,
|
2022-05-06 00:57:01 -04:00
|
|
|
edit_friend: this.editFriend,
|
|
|
|
edit_friend_by_uuid: this.editFriendByUUID,
|
2022-04-04 20:07:30 -04:00
|
|
|
get_friend_dm_id: req => this.getFriendProperty(req, "directChatId"),
|
|
|
|
get_memo_ids: this.getMemoIds,
|
2022-04-28 01:36:15 -04:00
|
|
|
download_file: this.downloadFile,
|
2022-04-05 00:59:22 -04:00
|
|
|
send_chat: this.sendChat,
|
2022-03-26 03:37:53 -04:00
|
|
|
send_media: this.sendMedia,
|
2022-04-09 04:02:51 -04:00
|
|
|
delete_chat: this.deleteChat,
|
2022-04-12 01:03:18 -04:00
|
|
|
mark_read: this.markRead,
|
2022-04-13 01:12:56 -04:00
|
|
|
send_perm: this.sendPerm,
|
2022-04-14 03:45:01 -04:00
|
|
|
set_channel_name: this.setChannelName,
|
|
|
|
set_channel_description: this.setChannelDescription,
|
2022-05-12 03:44:57 -04:00
|
|
|
//set_channel_photo: this.setChannelPhoto,
|
2022-05-06 04:25:08 -04:00
|
|
|
create_direct_chat: this.createDirectChat,
|
2022-04-28 03:22:14 -04:00
|
|
|
leave_channel: this.leaveChannel,
|
2022-02-25 02:22:50 -05:00
|
|
|
}[req.command] || this.handleUnknownCommand
|
|
|
|
}
|
|
|
|
const resp = { id: req.id }
|
|
|
|
delete req.id
|
|
|
|
delete req.command
|
|
|
|
resp.command = "response"
|
|
|
|
try {
|
|
|
|
resp.response = await handler(req)
|
|
|
|
} catch (err) {
|
|
|
|
if (err.isAxiosError) {
|
|
|
|
resp.response = {
|
|
|
|
success: false,
|
|
|
|
status: err.response.status,
|
|
|
|
}
|
2022-05-16 00:11:53 -04:00
|
|
|
} else if ("status" in err) {
|
|
|
|
resp.response = err
|
2022-02-25 02:22:50 -05:00
|
|
|
} else {
|
|
|
|
resp.command = "error"
|
2022-05-16 00:11:53 -04:00
|
|
|
let errorDetails
|
2022-05-06 02:45:00 -04:00
|
|
|
if (err instanceof ProtocolError) {
|
|
|
|
resp.error = err.message
|
2022-05-16 00:11:53 -04:00
|
|
|
errorDetails = err.message
|
|
|
|
} else if (err instanceof Error) {
|
2022-05-06 02:45:00 -04:00
|
|
|
resp.error = err.toString()
|
2022-05-16 00:11:53 -04:00
|
|
|
errorDetails = err.stack
|
|
|
|
} else {
|
|
|
|
resp.error = JSON.stringify(err)
|
|
|
|
errorDetails = `throwed ${resp.error}`
|
2022-05-06 02:45:00 -04:00
|
|
|
}
|
2022-05-16 00:11:53 -04:00
|
|
|
this.error(`Response ${resp.id}: ${errorDetails}`)
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
}
|
2022-05-15 22:17:28 -04:00
|
|
|
if (resp.response) {
|
|
|
|
const success = resp.response.success !== false
|
|
|
|
const logger = (success ? this.log : this.error).bind(this)
|
|
|
|
logger(
|
|
|
|
`Response ${resp.id}:`,
|
|
|
|
this.#logObj(
|
2022-05-16 00:10:08 -04:00
|
|
|
resp.response instanceof Object ? resp.response : {value: resp.response},
|
2022-05-15 22:17:28 -04:00
|
|
|
success ? "success" : "failure",
|
|
|
|
this.loggingKeys.response
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2022-04-01 05:07:05 -04:00
|
|
|
await this.write(resp)
|
2022-05-16 00:08:00 -04:00
|
|
|
if ("mxid" in req) {
|
|
|
|
const userClient = this.#tryGetUser(req.mxid)
|
|
|
|
if (userClient && userClient.isUnexpectedlyDisconnected()) {
|
|
|
|
this.error("Unexpected disconnect for user", req.mxid)
|
|
|
|
this.userClients.delete(req.mxid)
|
|
|
|
await userClient.write("unexpected_disconnect")
|
|
|
|
}
|
|
|
|
}
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
|
2022-05-15 22:17:28 -04:00
|
|
|
/**
|
|
|
|
* @param {object} obj
|
|
|
|
* @param {string} desc
|
|
|
|
* @param {[string]} keys
|
|
|
|
*/
|
|
|
|
#logObj(obj, desc, keys) {
|
|
|
|
return [desc].concat(
|
|
|
|
keys.filter(key => key in obj).map(key => `${key}: ${JSON.stringify(obj[key], this.#writeReplacer)}`)
|
|
|
|
).join(', ')
|
|
|
|
}
|
|
|
|
|
|
|
|
#writeReplacer(key, value) {
|
2022-03-11 03:43:00 -05:00
|
|
|
if (value instanceof Long) {
|
|
|
|
return value.toString()
|
|
|
|
} else {
|
|
|
|
return value
|
|
|
|
}
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
2022-03-11 03:43:00 -05:00
|
|
|
|
2022-05-15 22:17:28 -04:00
|
|
|
#readReviver(key, value) {
|
2022-03-11 03:43:00 -05:00
|
|
|
if (value instanceof Object) {
|
|
|
|
// TODO Use a type map if there will be many possible types
|
|
|
|
if (value.__type__ == "Long") {
|
|
|
|
return Long.fromString(value.str)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return value
|
2022-02-25 02:22:50 -05:00
|
|
|
}
|
|
|
|
}
|
2022-03-21 01:33:22 -04:00
|
|
|
|
|
|
|
|
2022-04-10 22:56:51 -04:00
|
|
|
/**
|
|
|
|
* @param {object} result
|
|
|
|
*/
|
|
|
|
function makeCommandResult(result) {
|
|
|
|
return {
|
|
|
|
success: true,
|
|
|
|
status: 0,
|
|
|
|
result: result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-21 01:33:22 -04:00
|
|
|
/**
|
|
|
|
* @param {TalkChannelList} channelList
|
|
|
|
* @param {ChannelType} channelType
|
|
|
|
*/
|
|
|
|
function getChannelListForType(channelList, channelType) {
|
2022-04-08 19:01:32 -04:00
|
|
|
return isChannelTypeOpen(channelType) ? channelList.open : channelList.normal
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {ChannelType} channelType
|
|
|
|
*/
|
|
|
|
function isChannelTypeOpen(channelType) {
|
2022-03-21 01:33:22 -04:00
|
|
|
switch (channelType) {
|
|
|
|
case "OM":
|
|
|
|
case "OD":
|
2022-04-08 19:01:32 -04:00
|
|
|
return true
|
2022-03-21 01:33:22 -04:00
|
|
|
default:
|
2022-04-08 19:01:32 -04:00
|
|
|
return false
|
2022-03-21 01:33:22 -04:00
|
|
|
}
|
|
|
|
}
|
2022-04-28 01:32:12 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {TalkOpenChannel} channel
|
|
|
|
*/
|
|
|
|
function getChannelOwner(channel) {
|
|
|
|
for (const userInfo of channel.getAllUserInfo()) {
|
|
|
|
if (userInfo.perm == OpenChannelUserPerm.OWNER) {
|
|
|
|
return userInfo
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|