mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-15 07:00:51 +00:00
Adds keepActiveUser to NewUser. When set, the new user is created but the
current active user is preserved.
This is needed to create a profile for an invitation: the prepared chat is
resolved under the active user by APIChangePreparedContactUser, so the profile
that owns the invitation has to stay active until the chat has been moved to
the new one. Creating and activating in one step makes that reassignment
impossible without switching back and forth.
BoolDef gives the field omittedField = False, so clients that do not send it -
iOS, the CLI, and any older caller - keep the current behaviour of activating
the new user.
keepActiveUser is ignored when there is no active user to keep, which would
otherwise leave the app with none at all.
Note the response is still CRActiveUser: it carries the created user, which is
not the active one on this path. Documented at the field.
Regenerates the API docs and the generated TypeScript and Python client types,
which are checked by the Bot API docs tests.
The generator emits BoolDef fields as required in the client types, so the two
hand-maintained clients that build a NewUser literal - simplex-chat-python and
simplex-chat-nodejs - stop type-checking until the field is added there too. No
test covers those; a4e3a1ea1 did the same when clientService was added.
960 lines
33 KiB
TypeScript
960 lines
33 KiB
TypeScript
import {CC, CEvt, ChatEvent, ChatResponse, T} from "@simplex-chat/types"
|
|
import * as core from "./core"
|
|
import * as util from "./util"
|
|
|
|
export class ChatCommandError extends Error {
|
|
constructor(public message: string, public response: ChatResponse) {
|
|
super(message)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Connection request types.
|
|
* @enum {string}
|
|
*/
|
|
export enum ConnReqType {
|
|
Invitation = "invitation",
|
|
Contact = "contact",
|
|
}
|
|
|
|
/**
|
|
* Bot address settings.
|
|
*/
|
|
export interface BotAddressSettings {
|
|
/**
|
|
* Automatically accept contact requests.
|
|
* @default true
|
|
*/
|
|
autoAccept?: boolean
|
|
|
|
/**
|
|
* Optional welcome message to show before connection to the users.
|
|
* @default undefined (no welcome message)
|
|
*/
|
|
welcomeMessage?: T.MsgContent | string | undefined
|
|
|
|
/**
|
|
* Business contact address.
|
|
* For all requests business chats will be created where other participants can be added.
|
|
* @default false
|
|
*/
|
|
businessAddress?: boolean
|
|
}
|
|
|
|
export const defaultBotAddressSettings: BotAddressSettings = {
|
|
autoAccept: true,
|
|
welcomeMessage: undefined,
|
|
businessAddress: false
|
|
}
|
|
|
|
export type EventSubscriberFunc<K extends CEvt.Tag> = (event: ChatEvent & {type: K}) => void | Promise<void>
|
|
|
|
export type EventSubscribers = {[K in CEvt.Tag]?: EventSubscriberFunc<K>}
|
|
|
|
interface EventSubscriber<K extends CEvt.Tag> {
|
|
subscriber: EventSubscriberFunc<K>
|
|
once: boolean
|
|
}
|
|
|
|
/**
|
|
* Database configuration. The native library is built against exactly one
|
|
* backend (see `simplex_backend` / `SIMPLEX_BACKEND` at install time); this
|
|
* type makes the caller state which one they are targeting so field names
|
|
* can't lie about their meaning.
|
|
*/
|
|
export type DbConfig =
|
|
| {
|
|
/** SQLite backend (default). */
|
|
type: "sqlite"
|
|
/** File prefix — two schema files are named `<prefix>_chat.db` and `<prefix>_agent.db`. */
|
|
filePrefix: string
|
|
/** Optional SQLCipher encryption key. Empty/omitted = unencrypted. */
|
|
encryptionKey?: string
|
|
}
|
|
| {
|
|
/** PostgreSQL backend (Linux x86_64 only, libpq5 required). */
|
|
type: "postgres"
|
|
/** Schema prefix used to namespace tables. Defaults to `"simplex_v1"` when omitted. */
|
|
schemaPrefix?: string
|
|
/** PostgreSQL connection string (e.g. `postgres://user:pass@host/db`). */
|
|
connectionString: string
|
|
}
|
|
|
|
function dbConfigToMigrateArgs(db: DbConfig): [string, string] {
|
|
switch (db.type) {
|
|
case "sqlite":
|
|
return [db.filePrefix, db.encryptionKey ?? ""]
|
|
case "postgres":
|
|
return [db.schemaPrefix ?? "", db.connectionString]
|
|
default:
|
|
throw new Error(`Invalid DbConfig: ${JSON.stringify(db satisfies never)}`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main API class for interacting with the chat core library.
|
|
*/
|
|
export class ChatApi {
|
|
private receiveEvents = false
|
|
private eventsLoop: Promise<void> | undefined = undefined
|
|
private subscribers: {[K in CEvt.Tag]?: EventSubscriber<K>[]} = {}
|
|
private receivers: EventSubscriberFunc<CEvt.Tag>[] = []
|
|
|
|
private constructor(protected ctrl_: bigint | undefined) {}
|
|
|
|
/**
|
|
* Initializes the ChatApi.
|
|
* @param {DbConfig} db - Database configuration (sqlite or postgres).
|
|
* @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode.
|
|
*/
|
|
static async init(
|
|
db: DbConfig,
|
|
confirm = core.MigrationConfirmation.YesUp
|
|
): Promise<ChatApi> {
|
|
const [path, key] = dbConfigToMigrateArgs(db)
|
|
const ctrl = await core.chatMigrateInit(path, key, confirm)
|
|
return new ChatApi(ctrl)
|
|
}
|
|
|
|
/**
|
|
* Start chat controller. Must be called with the existing user profile.
|
|
*/
|
|
async startChat(): Promise<void> {
|
|
this.receiveEvents = true
|
|
this.eventsLoop = this.runEventsLoop()
|
|
const r = await this.sendChatCmd(CC.StartChat.cmdString({mainApp: true, enableSndFiles: true}))
|
|
if (r.type !== "chatStarted" && r.type !== "chatRunning") {
|
|
throw new ChatCommandError("error starting chat", r)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop chat controller.
|
|
* Must be called before closing the database.
|
|
* Usually doesn't need to be called in chat bots.
|
|
*/
|
|
async stopChat(): Promise<void> {
|
|
const r = await this.sendChatCmd("/_stop")
|
|
if (r.type !== "chatStopped") throw new ChatCommandError("error starting chat", r)
|
|
this.receiveEvents = false
|
|
if (this.eventsLoop) await this.eventsLoop
|
|
this.eventsLoop = undefined
|
|
}
|
|
|
|
/**
|
|
* Close chat database.
|
|
* Usually doesn't need to be called in chat bots.
|
|
*/
|
|
async close(): Promise<void> {
|
|
this.receiveEvents = false
|
|
if (this.eventsLoop) await this.eventsLoop
|
|
this.eventsLoop = undefined
|
|
await core.chatCloseStore(this.ctrl)
|
|
this.ctrl_ = undefined
|
|
}
|
|
|
|
private async runEventsLoop(): Promise<void> {
|
|
while (this.receiveEvents) {
|
|
try {
|
|
const event = await this.recvChatEvent()
|
|
if (!event) continue
|
|
const subs = this.subscribers[event.type]
|
|
if (subs) {
|
|
for (const {subscriber, once} of [...subs]) {
|
|
try {
|
|
const p = (subscriber as EventSubscriberFunc<typeof event.type>)(event)
|
|
if (p instanceof Promise) await p
|
|
} catch(e) {
|
|
console.log(`${event.type} event processing error`, e)
|
|
}
|
|
if (once) this.off(event.type, subscriber as EventSubscriberFunc<typeof event.type>)
|
|
}
|
|
}
|
|
for (const r of [...this.receivers]) {
|
|
try {
|
|
const p = r(event)
|
|
if (p instanceof Promise) await p
|
|
} catch(e) {
|
|
console.log(`${event.type} event processing error`, e)
|
|
}
|
|
}
|
|
} catch(err) {
|
|
const e = err as core.ChatAPIError
|
|
if ("chatError" in e) {
|
|
console.log("Chat error", e.chatError)
|
|
} else {
|
|
console.log("Invalid event", e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subscribe multiple event handlers at once.
|
|
* @param subscribers - An object mapping event types (CEvt.Tag) to their subscriber functions.
|
|
* @throws {Error} If the same function is subscribed to event.
|
|
*/
|
|
on<K extends CEvt.Tag>(subscribers: EventSubscribers): void
|
|
|
|
/**
|
|
* Subscribe a handler to a specific event.
|
|
* @param {CEvt.Tag} event - The event type to subscribe to.
|
|
* @param subscriber - The subscriber function for the event.
|
|
* @throws {Error} If the same function is subscribed to event.
|
|
*/
|
|
on<K extends CEvt.Tag>(event: K, subscriber: EventSubscriberFunc<K>): void
|
|
on<K extends CEvt.Tag>(events: K | EventSubscribers, subscriber?: EventSubscriberFunc<K>): void {
|
|
if (typeof events === "string" && subscriber) {
|
|
this.on_(events, subscriber)
|
|
} else {
|
|
const eventEntries = Object.entries(events) as [CEvt.Tag, EventSubscriberFunc<CEvt.Tag> | undefined][]
|
|
for (const [event, subscriber] of eventEntries) {
|
|
if (subscriber) this.on_(event, subscriber)
|
|
}
|
|
}
|
|
}
|
|
|
|
private on_<K extends CEvt.Tag>(event: K, subscriber: EventSubscriberFunc<K>, once: boolean = false): void {
|
|
const subs: EventSubscriber<K>[] = this.subscribers[event] || (this.subscribers[event] = [])
|
|
if (subs.some(s => s.subscriber === subscriber)) throw Error(`this function is already subscribed to ${event}`)
|
|
subs.push({subscriber, once})
|
|
}
|
|
|
|
/**
|
|
* Subscribe a handler to any event.
|
|
* @param receiver - The receiver function for any event.
|
|
* @throws {Error} If the same function is subscribed to event.
|
|
*/
|
|
onAny(receiver: EventSubscriberFunc<CEvt.Tag>): void {
|
|
if (this.receivers.some(s => s === receiver)) throw Error("this function is already subscribed")
|
|
this.receivers.push(receiver)
|
|
}
|
|
|
|
/**
|
|
* Subscribe a handler to a specific event to be delivered one time.
|
|
* @param {CEvt.Tag} event - The event type to subscribe to.
|
|
* @param subscriber - The subscriber function for the event.
|
|
* @throws {Error} If the same function is subscribed to event.
|
|
*/
|
|
once<K extends CEvt.Tag>(event: K, subscriber: EventSubscriberFunc<K>): void {
|
|
this.on_(event, subscriber, true)
|
|
}
|
|
|
|
/**
|
|
* Waits for specific event, with an optional predicate.
|
|
* Returns `undefined` on timeout if specified.
|
|
*/
|
|
wait<K extends CEvt.Tag>(event: K): Promise<ChatEvent & {type: K}>
|
|
wait<K extends CEvt.Tag>(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined): Promise<ChatEvent & {type: K}>
|
|
wait<K extends CEvt.Tag>(event: K, timeout: number): Promise<ChatEvent & {type: K} | undefined>
|
|
wait<K extends CEvt.Tag>(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined, timeout: number): Promise<ChatEvent & {type: K} | undefined>
|
|
wait<K extends CEvt.Tag>(
|
|
event: K,
|
|
predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined | number = undefined, // number for timeout
|
|
timeout: number = 0 // milliseconds, default - indefinite
|
|
): Promise<ChatEvent & {type: K} | undefined> {
|
|
if (typeof predicate === "number") {
|
|
timeout = predicate
|
|
predicate = undefined
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
let done = false
|
|
const cleanup = () => {
|
|
done = true
|
|
this.off(event, subscriber)
|
|
}
|
|
const subscriber: EventSubscriberFunc<K> = async (evt: ChatEvent & {type: K}) => {
|
|
if (done) return
|
|
if (predicate) {
|
|
try { if (!predicate(evt)) return }
|
|
catch(e) { cleanup(); reject(e); return }
|
|
}
|
|
cleanup()
|
|
resolve(evt)
|
|
}
|
|
this.on(event, subscriber)
|
|
if (timeout > 0) {
|
|
setTimeout(() => { if (!done) { cleanup(); resolve(undefined) } }, timeout)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe all or a specific handler from a specific event.
|
|
* @param {CEvt.Tag} event - The event type to unsubscribe from.
|
|
* @param subscriber - An optional subscriber function for the event.
|
|
*/
|
|
off<K extends CEvt.Tag>(event: K, subscriber: EventSubscriberFunc<K> | undefined = undefined): void {
|
|
if (subscriber) {
|
|
const subs = this.subscribers[event]
|
|
if (subs) {
|
|
const i = subs.findIndex(s => s.subscriber === subscriber)
|
|
if (i >= 0) subs.splice(i, 1)
|
|
}
|
|
} else {
|
|
delete this.subscribers[event]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe all or a specific handler from any events.
|
|
* @param receiver - An optional subscriber function for the event.
|
|
*/
|
|
offAny(receiver: EventSubscriberFunc<CEvt.Tag> | undefined = undefined): void {
|
|
if (receiver) {
|
|
const i = this.receivers.findIndex(r => r === receiver)
|
|
if (i >= 0) this.receivers.splice(i, 1)
|
|
} else {
|
|
this.receivers = []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Chat controller is initialized
|
|
*/
|
|
get initialized(): boolean {
|
|
return typeof this.ctrl_ === "bigint"
|
|
}
|
|
|
|
/**
|
|
* Chat controller is started
|
|
*/
|
|
get started(): boolean {
|
|
return this.receiveEvents && this.eventsLoop !== undefined
|
|
}
|
|
|
|
/**
|
|
* Chat controller reference
|
|
*/
|
|
get ctrl(): bigint {
|
|
if (typeof this.ctrl_ === "bigint") return this.ctrl_
|
|
else throw Error("chat api controller not initialized")
|
|
}
|
|
|
|
async sendChatCmd(cmd: string): Promise<ChatResponse> {
|
|
return await core.chatSendCmd(this.ctrl, cmd)
|
|
}
|
|
|
|
async recvChatEvent(wait: number = 5_000_000): Promise<ChatEvent | undefined> {
|
|
return await core.chatRecvMsgWait(this.ctrl, wait)
|
|
}
|
|
|
|
/**
|
|
* Create bot address.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiCreateUserAddress(userId: number): Promise<T.CreatedConnLink> {
|
|
const r = await this.sendChatCmd(CC.APICreateMyAddress.cmdString({userId}))
|
|
if (r.type === "userContactLinkCreated") return r.connLinkContact
|
|
throw new ChatCommandError("error creating user address", r)
|
|
}
|
|
|
|
/**
|
|
* Deletes a user address.
|
|
* Network usage: background.
|
|
*/
|
|
async apiDeleteUserAddress(userId: number): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIDeleteMyAddress.cmdString({userId}))
|
|
if (r.type === "userContactLinkDeleted") return
|
|
throw new ChatCommandError("error deleting user address", r)
|
|
}
|
|
|
|
/**
|
|
* Get bot address and settings.
|
|
* Network usage: no.
|
|
*/
|
|
async apiGetUserAddress(userId: number): Promise<T.UserContactLink | undefined> {
|
|
try {
|
|
const r = await this.sendChatCmd(CC.APIShowMyAddress.cmdString({userId}))
|
|
switch (r.type) {
|
|
case "userContactLink": return r.contactLink
|
|
default: throw new ChatCommandError("error loading user address", r)
|
|
}
|
|
} catch (err) {
|
|
const e = err as any
|
|
if (e.chatError?.type === "errorStore" && e.chatError.storeError?.type === "userContactLinkNotFound") return undefined
|
|
throw e
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add address to bot profile.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiSetProfileAddress(userId: number, enable: boolean): Promise<T.UserProfileUpdateSummary> {
|
|
const r = await this.sendChatCmd(CC.APISetProfileAddress.cmdString({userId, enable}))
|
|
switch (r.type) {
|
|
case "userProfileUpdated":
|
|
return r.updateSummary
|
|
default:
|
|
throw new ChatCommandError("error loading user address", r)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set bot address settings.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiSetAddressSettings(userId: number, {autoAccept, welcomeMessage, businessAddress}: BotAddressSettings): Promise<void> {
|
|
const autoReply = welcomeMessage || defaultBotAddressSettings.welcomeMessage
|
|
const settings: T.AddressSettings = {
|
|
autoAccept: (autoAccept === undefined ? defaultBotAddressSettings.autoAccept : autoAccept) ? {acceptIncognito: false} : undefined,
|
|
autoReply: typeof autoReply === "string" ? {type: "text", text: autoReply} : autoReply,
|
|
businessAddress: businessAddress || defaultBotAddressSettings.businessAddress || false
|
|
}
|
|
const r = await this.sendChatCmd(CC.APISetAddressSettings.cmdString({userId, settings}))
|
|
if (r.type !== "userContactLinkUpdated") {
|
|
throw new ChatCommandError("error changing user contact address settings", r)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send messages.
|
|
* Network usage: background.
|
|
*/
|
|
async apiSendMessages(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, messages: T.ComposedMessage[], liveMessage = false): Promise<T.AChatItem[]> {
|
|
const sendRef = Array.isArray(chat)
|
|
? {chatType: chat[0], chatId: chat[1]}
|
|
: "chatType" in chat
|
|
? chat
|
|
: util.chatInfoRef(chat)
|
|
if (!sendRef) throw Error("apiSendMessages: can't send messages to this chat")
|
|
const r = await this.sendChatCmd(
|
|
CC.APISendMessages.cmdString({
|
|
sendRef,
|
|
composedMessages: messages,
|
|
liveMessage,
|
|
signMessages: false
|
|
})
|
|
)
|
|
if (r.type === "newChatItems") return r.chatItems
|
|
throw new ChatCommandError("unexpected response", r)
|
|
}
|
|
|
|
/**
|
|
* Send text message.
|
|
* Network usage: background.
|
|
*/
|
|
async apiSendTextMessage(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, text: string, inReplyTo?: number): Promise<T.AChatItem[]> {
|
|
return this.apiSendMessages(chat, [{msgContent: {type: "text", text}, mentions: {}, quotedItemId: inReplyTo}])
|
|
}
|
|
|
|
/**
|
|
* Send text message in reply to received message.
|
|
* Network usage: background.
|
|
*/
|
|
async apiSendTextReply(chatItem: T.AChatItem, text: string): Promise<T.AChatItem[]> {
|
|
return this.apiSendTextMessage(chatItem.chatInfo, text, chatItem.chatItem.meta.itemId)
|
|
}
|
|
|
|
/**
|
|
* Update message.
|
|
* Network usage: background.
|
|
*/
|
|
async apiUpdateChatItem(chatType: T.ChatType, chatId: number, chatItemId: number, msgContent: T.MsgContent, liveMessage: false): Promise<T.ChatItem> {
|
|
const r = await this.sendChatCmd(
|
|
CC.APIUpdateChatItem.cmdString({
|
|
chatRef: {chatType, chatId},
|
|
chatItemId,
|
|
liveMessage,
|
|
updatedMessage: {msgContent, mentions: {}},
|
|
})
|
|
)
|
|
if (r.type === "chatItemUpdated") return r.chatItem.chatItem
|
|
throw new ChatCommandError("error updating chat item", r)
|
|
}
|
|
|
|
/**
|
|
* Delete message.
|
|
* Network usage: background.
|
|
*/
|
|
async apiDeleteChatItems(
|
|
chatType: T.ChatType,
|
|
chatId: number,
|
|
chatItemIds: number[],
|
|
deleteMode: T.CIDeleteMode
|
|
): Promise<T.ChatItemDeletion[]> {
|
|
const r = await this.sendChatCmd(CC.APIDeleteChatItem.cmdString({chatRef: {chatType, chatId}, chatItemIds, deleteMode}))
|
|
if (r.type === "chatItemsDeleted") return r.chatItemDeletions
|
|
throw new ChatCommandError("error deleting chat item", r)
|
|
}
|
|
|
|
/**
|
|
* Moderate message. Requires Moderator role (and higher than message author's).
|
|
* Network usage: background.
|
|
*/
|
|
async apiDeleteMemberChatItem(groupId: number, chatItemIds: number[]): Promise<T.ChatItemDeletion[]> {
|
|
const r = await this.sendChatCmd(CC.APIDeleteMemberChatItem.cmdString({groupId, chatItemIds}))
|
|
if (r.type === "chatItemsDeleted") return r.chatItemDeletions
|
|
throw new ChatCommandError("error deleting member chat item", r)
|
|
}
|
|
|
|
/**
|
|
* Add/remove message reaction.
|
|
* Network usage: background.
|
|
*/
|
|
async apiChatItemReaction(
|
|
chatType: T.ChatType,
|
|
chatId: number,
|
|
chatItemId: number,
|
|
add: boolean,
|
|
reaction: T.MsgReaction
|
|
) {
|
|
const r = await this.sendChatCmd(CC.APIChatItemReaction.cmdString({chatRef: {chatType, chatId}, chatItemId, add, reaction}))
|
|
if (r.type === "chatItemsDeleted") return r.chatItemDeletions
|
|
throw new ChatCommandError("error setting item reaction", r)
|
|
}
|
|
|
|
/**
|
|
* Receive file.
|
|
* Network usage: no.
|
|
*/
|
|
async apiReceiveFile(fileId: number): Promise<T.AChatItem> {
|
|
const r = await this.sendChatCmd(CC.ReceiveFile.cmdString({fileId, userApprovedRelays: true}))
|
|
if (r.type === "rcvFileAccepted") return r.chatItem
|
|
throw new ChatCommandError("error receiving file", r)
|
|
}
|
|
|
|
/**
|
|
* Cancel file.
|
|
* Network usage: background.
|
|
*/
|
|
async apiCancelFile(fileId: number): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.CancelFile.cmdString({fileId}))
|
|
if (r.type === "sndFileCancelled" || r.type === "rcvFileCancelled") return
|
|
throw new ChatCommandError("error canceling file", r)
|
|
}
|
|
|
|
/**
|
|
* Add contact to group. Requires bot to have Admin role.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiAddMember(groupId: number, contactId: number, memberRole: T.GroupMemberRole): Promise<T.GroupMember> {
|
|
const r = await this.sendChatCmd(CC.APIAddMember.cmdString({groupId, contactId, memberRole}))
|
|
if (r.type === "sentGroupInvitation") return r.member
|
|
throw new ChatCommandError("error adding member", r)
|
|
}
|
|
|
|
/**
|
|
* Join group.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiJoinGroup(groupId: number): Promise<T.GroupInfo> {
|
|
const r = await this.sendChatCmd(CC.APIJoinGroup.cmdString({groupId}))
|
|
if (r.type === "userAcceptedGroupSent") return r.groupInfo
|
|
throw new ChatCommandError("error joining group", r)
|
|
}
|
|
|
|
/**
|
|
* Accept group member. Requires Admin role.
|
|
* Network usage: background.
|
|
*/
|
|
async apiAcceptMember(groupId: number, groupMemberId: number, memberRole: T.GroupMemberRole): Promise<T.GroupMember> {
|
|
const r = await this.sendChatCmd(CC.APIAcceptMember.cmdString({groupId, groupMemberId, memberRole}))
|
|
if (r.type === "memberAccepted") return r.member
|
|
throw new ChatCommandError("error accepting member", r)
|
|
}
|
|
|
|
/**
|
|
* Set members role. Requires Admin role.
|
|
* Network usage: background.
|
|
*/
|
|
async apiSetMembersRole(groupId: number, groupMemberIds: number[], memberRole: T.GroupMemberRole): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIMembersRole.cmdString({groupId, groupMemberIds, memberRole}))
|
|
if (r.type === "membersRoleUser") return
|
|
throw new ChatCommandError("error setting members role", r)
|
|
}
|
|
|
|
/**
|
|
* Block members. Requires Moderator role.
|
|
* Network usage: background.
|
|
*/
|
|
async apiBlockMembersForAll(groupId: number, groupMemberIds: number[], blocked: boolean): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIBlockMembersForAll.cmdString({groupId, groupMemberIds, blocked}))
|
|
if (r.type === "membersBlockedForAllUser") return
|
|
throw new ChatCommandError("error blocking members", r)
|
|
}
|
|
|
|
/**
|
|
* Remove members. Requires Admin role.
|
|
* Network usage: background.
|
|
*/
|
|
async apiRemoveMembers(groupId: number, memberIds: number[], withMessages = false): Promise<T.GroupMember[]> {
|
|
const r = await this.sendChatCmd(CC.APIRemoveMembers.cmdString({groupId, groupMemberIds: memberIds, withMessages}))
|
|
if (r.type === "userDeletedMembers") return r.members
|
|
throw new ChatCommandError("error removing member", r)
|
|
}
|
|
|
|
/**
|
|
* Leave group.
|
|
* Network usage: background.
|
|
*/
|
|
async apiLeaveGroup(groupId: number): Promise<T.GroupInfo> {
|
|
const r = await this.sendChatCmd(CC.APILeaveGroup.cmdString({groupId}))
|
|
if (r.type === "leftMemberUser") return r.groupInfo
|
|
throw new ChatCommandError("error leaving group", r)
|
|
}
|
|
|
|
/**
|
|
* Get group members.
|
|
* Network usage: no.
|
|
*/
|
|
async apiListMembers(groupId: number): Promise<T.GroupMember[]> {
|
|
const r = await this.sendChatCmd(CC.APIListMembers.cmdString({groupId}))
|
|
if (r.type === "groupMembers") return r.group.members
|
|
throw new ChatCommandError("error getting group members", r)
|
|
}
|
|
|
|
/**
|
|
* Create group.
|
|
* Network usage: no.
|
|
*/
|
|
async apiNewGroup(userId: number, groupProfile: T.GroupProfile): Promise<T.GroupInfo> {
|
|
const r = await this.sendChatCmd(CC.APINewGroup.cmdString({userId, groupProfile, incognito: false}))
|
|
if (r.type === "groupCreated") return r.groupInfo
|
|
throw new ChatCommandError("error creating group", r)
|
|
}
|
|
|
|
/**
|
|
* Update group profile.
|
|
* Network usage: background.
|
|
*/
|
|
async apiUpdateGroupProfile(groupId: number, groupProfile: T.GroupProfile): Promise<T.GroupInfo> {
|
|
const r = await this.sendChatCmd(CC.APIUpdateGroupProfile.cmdString({groupId, groupProfile}))
|
|
if (r.type === "groupUpdated") return r.toGroup
|
|
throw new ChatCommandError("error updating group", r)
|
|
}
|
|
|
|
/**
|
|
* Create group link.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiCreateGroupLink(groupId: number, memberRole: T.GroupMemberRole): Promise<string> {
|
|
const r = await this.sendChatCmd(CC.APICreateGroupLink.cmdString({groupId, memberRole}))
|
|
if (r.type === "groupLinkCreated") {
|
|
const link = r.groupLink.connLinkContact
|
|
return link.connShortLink || link.connFullLink
|
|
}
|
|
throw new ChatCommandError("error creating group link", r)
|
|
}
|
|
|
|
/**
|
|
* Set member role for group link.
|
|
* Network usage: no.
|
|
*/
|
|
async apiSetGroupLinkMemberRole(groupId: number, memberRole: T.GroupMemberRole): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIGroupLinkMemberRole.cmdString({groupId, memberRole}))
|
|
if (r.type !== "groupLink") throw new ChatCommandError("error setting group link member role", r)
|
|
}
|
|
|
|
/**
|
|
* Delete group link.
|
|
* Network usage: background.
|
|
*/
|
|
async apiDeleteGroupLink(groupId: number): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIDeleteGroupLink.cmdString({groupId}))
|
|
if (r.type !== "groupLinkDeleted") throw new ChatCommandError("error deleting group link", r)
|
|
}
|
|
|
|
/**
|
|
* Get group link.
|
|
* Network usage: no.
|
|
*/
|
|
async apiGetGroupLink(groupId: number): Promise<T.GroupLink> {
|
|
const r = await this.sendChatCmd(CC.APIGetGroupLink.cmdString({groupId}))
|
|
if (r.type === "groupLink") return r.groupLink
|
|
throw new ChatCommandError("error getting group link", r)
|
|
}
|
|
|
|
async apiGetGroupLinkStr(groupId: number): Promise<string> {
|
|
const link = (await this.apiGetGroupLink(groupId)).connLinkContact
|
|
return link.connShortLink || link.connFullLink
|
|
}
|
|
|
|
/**
|
|
* Create 1-time invitation link.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiCreateLink(userId: number): Promise<string> {
|
|
const r = await this.sendChatCmd(CC.APIAddContact.cmdString({userId, incognito: false}))
|
|
if (r.type === "invitation") {
|
|
const link = r.connLinkInvitation
|
|
return link.connShortLink || link.connFullLink
|
|
}
|
|
throw new ChatCommandError("error creating link", r)
|
|
}
|
|
|
|
/**
|
|
* Determine SimpleX link type and if the bot is already connected via this link.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink]> {
|
|
const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectTarget: connectionLink, resolveMode: T.PlanResolveMode.Unknown}))
|
|
if (r.type === "connectionPlan") return [r.connectionPlan, r.connLink]
|
|
throw new ChatCommandError("error getting connect plan", r)
|
|
}
|
|
|
|
/**
|
|
* Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiConnect(userId: number, incognito: boolean, preparedLink?: T.CreatedConnLink): Promise<ConnReqType> {
|
|
const r = await this.sendChatCmd(CC.APIConnect.cmdString({userId, incognito, preparedLink_: preparedLink}))
|
|
return this.handleConnectResult(r)
|
|
}
|
|
|
|
/**
|
|
* Connect via SimpleX link as string in the active user profile.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiConnectActiveUser(connLink: string): Promise<ConnReqType> {
|
|
const r = await this.sendChatCmd(CC.Connect.cmdString({incognito: false, connTarget_: connLink}))
|
|
return this.handleConnectResult(r)
|
|
}
|
|
|
|
private handleConnectResult(r: ChatResponse): ConnReqType {
|
|
switch (r.type) {
|
|
case "sentConfirmation":
|
|
return ConnReqType.Invitation
|
|
case "sentInvitation":
|
|
return ConnReqType.Contact
|
|
case "contactAlreadyExists":
|
|
throw new ChatCommandError("contact already exists", r)
|
|
default:
|
|
throw new ChatCommandError("connection error", r)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accept contact request.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiAcceptContactRequest(contactReqId: number): Promise<T.Contact> {
|
|
const r = await this.sendChatCmd(CC.APIAcceptContact.cmdString({contactReqId}))
|
|
if (r.type === "acceptingContactRequest") return r.contact
|
|
throw new ChatCommandError("error accepting contact request", r)
|
|
}
|
|
|
|
/**
|
|
* Reject contact request. The user who sent the request is **not notified**.
|
|
* Network usage: no.
|
|
*/
|
|
async apiRejectContactRequest(contactReqId: number): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIRejectContact.cmdString({contactReqId}))
|
|
if (r.type === "contactRequestRejected") return
|
|
throw new ChatCommandError("error rejecting contact request", r)
|
|
}
|
|
|
|
/**
|
|
* Get contacts.
|
|
* Network usage: no.
|
|
*/
|
|
async apiListContacts(userId: number): Promise<T.Contact[]> {
|
|
const r = await this.sendChatCmd(CC.APIListContacts.cmdString({userId}))
|
|
if (r.type === "contactsList") return r.contacts
|
|
throw new ChatCommandError("error listing contacts", r)
|
|
}
|
|
|
|
/**
|
|
* Get groups.
|
|
* Network usage: no.
|
|
*/
|
|
async apiListGroups(userId: number, contactId?: number, search?: string): Promise<T.GroupInfo[]> {
|
|
const r = await this.sendChatCmd(CC.APIListGroups.cmdString({userId, contactId_: contactId, search}))
|
|
if (r.type === "groupsList") return r.groups
|
|
throw new ChatCommandError("error listing groups", r)
|
|
}
|
|
|
|
/**
|
|
* Get chat previews (paginated).
|
|
* Network usage: no.
|
|
*
|
|
* Prefer this over apiListContacts / apiListGroups for any scan: those
|
|
* methods load every record into memory in a single response and will fail
|
|
* on large databases.
|
|
*/
|
|
async apiGetChats(
|
|
userId: number,
|
|
pagination: T.PaginationByTime,
|
|
query: T.ChatListQuery = {type: "filters", favorite: false, unread: false},
|
|
pendingConnections = false,
|
|
): Promise<T.AChat[]> {
|
|
const r = await this.sendChatCmd(CC.APIGetChats.cmdString({userId, pendingConnections, pagination, query}))
|
|
if (r.type === "apiChats") return r.chats
|
|
throw new ChatCommandError("error getting chats", r)
|
|
}
|
|
|
|
/**
|
|
* Delete chat.
|
|
* Network usage: background.
|
|
*/
|
|
async apiDeleteChat(chatType: T.ChatType, chatId: number, deleteMode: T.ChatDeleteMode = {type: "full", notify: true}): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIDeleteChat.cmdString({chatRef: {chatType, chatId}, chatDeleteMode: deleteMode}))
|
|
switch (chatType) {
|
|
case T.ChatType.Direct:
|
|
if (r.type === "contactDeleted") return
|
|
break
|
|
case T.ChatType.Group:
|
|
if (r.type === "groupDeletedUser") return
|
|
break
|
|
}
|
|
throw new ChatCommandError("error deleting chat", r)
|
|
}
|
|
|
|
/**
|
|
* Set group custom data.
|
|
* Network usage: no.
|
|
*/
|
|
async apiSetGroupCustomData(groupId: number, customData?: object): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APISetGroupCustomData.cmdString({groupId, customData}))
|
|
if (r.type === "cmdOk") return
|
|
throw new ChatCommandError("error setting group custom data", r)
|
|
}
|
|
|
|
/**
|
|
* Set contact custom data.
|
|
* Network usage: no.
|
|
*/
|
|
async apiSetContactCustomData(contactId: number, customData?: object): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APISetContactCustomData.cmdString({contactId, customData}))
|
|
if (r.type === "cmdOk") return
|
|
throw new ChatCommandError("error setting contact custom data", r)
|
|
}
|
|
|
|
/**
|
|
* Set auto-accept member contacts.
|
|
* Network usage: no.
|
|
*/
|
|
async apiSetAutoAcceptMemberContacts(userId: number, onOff: boolean): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APISetUserAutoAcceptMemberContacts.cmdString({userId, onOff}))
|
|
if (r.type === "cmdOk") return
|
|
throw new ChatCommandError("error setting auto-accept member contacts", r)
|
|
}
|
|
|
|
/**
|
|
* Get chat items.
|
|
* Network usage: no.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async apiGetChat(chatType: T.ChatType, chatId: number, count: number): Promise<any> {
|
|
const r: any = await this.sendChatCmd(`/_get chat ${T.ChatType.cmdString(chatType)}${chatId} count=${count}`)
|
|
if (r.type === "apiChat") return r.chat
|
|
throw new ChatCommandError("error getting chat", r)
|
|
}
|
|
|
|
/**
|
|
* Get active user profile
|
|
* Network usage: no.
|
|
*/
|
|
async apiGetActiveUser(): Promise<T.User | undefined> {
|
|
try {
|
|
const r = await this.sendChatCmd(CC.ShowActiveUser.cmdString({}))
|
|
switch (r.type) {
|
|
case "activeUser":
|
|
return r.user
|
|
default:
|
|
throw new ChatCommandError("unexpected response", r)
|
|
}
|
|
} catch (err) {
|
|
const e = err as core.ChatAPIError
|
|
if (e.chatError?.type === "error" && e.chatError.errorType.type === "noActiveUser") return undefined
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create new user profile
|
|
* Network usage: no.
|
|
*/
|
|
async apiCreateActiveUser(profile?: T.Profile): Promise<T.User> {
|
|
const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false, clientService: false, keepActiveUser: false}}))
|
|
if (r.type === "activeUser") return r.user
|
|
throw new ChatCommandError("unexpected response", r)
|
|
}
|
|
|
|
/**
|
|
* Get all user profiles
|
|
* Network usage: no.
|
|
*/
|
|
async apiListUsers(): Promise<T.UserInfo[]> {
|
|
const r = await this.sendChatCmd(CC.ListUsers.cmdString({}))
|
|
if (r.type === "usersList") return r.users
|
|
throw new ChatCommandError("error listing users", r)
|
|
}
|
|
|
|
/**
|
|
* Set active user profile
|
|
* Network usage: no.
|
|
*/
|
|
async apiSetActiveUser(userId: number, viewPwd?: string): Promise<T.User> {
|
|
const r = await this.sendChatCmd(CC.APISetActiveUser.cmdString({userId, viewPwd}))
|
|
if (r.type === "activeUser") return r.user
|
|
throw new ChatCommandError("error setting active user", r)
|
|
}
|
|
|
|
/**
|
|
* Delete user profile.
|
|
* Network usage: background.
|
|
*/
|
|
async apiDeleteUser(userId: number, delSMPQueues: boolean, viewPwd?: string): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APIDeleteUser.cmdString({userId, delSMPQueues, viewPwd}))
|
|
if (r.type === "cmdOk") return
|
|
throw new ChatCommandError("error deleting user", r)
|
|
}
|
|
|
|
/**
|
|
* Update user profile.
|
|
* Network usage: background.
|
|
*/
|
|
async apiUpdateProfile(userId: number, profile: T.Profile): Promise<T.UserProfileUpdateSummary | undefined> {
|
|
const r = await this.sendChatCmd(CC.APIUpdateProfile.cmdString({userId, profile}))
|
|
switch (r.type) {
|
|
case "userProfileNoChange":
|
|
return undefined
|
|
case "userProfileUpdated":
|
|
return r.updateSummary
|
|
default:
|
|
throw new ChatCommandError("error updating profile", r)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configure chat preference overrides for the contact.
|
|
* Network usage: background.
|
|
*/
|
|
async apiSetContactPrefs(contactId: number, preferences: T.Preferences): Promise<void> {
|
|
const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences}))
|
|
if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r)
|
|
}
|
|
|
|
/**
|
|
* Create a direct message contact with a group member.
|
|
* Returns the created contact.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiCreateMemberContact(groupId: number, groupMemberId: number): Promise<T.Contact> {
|
|
const r: any = await this.sendChatCmd(`/_create member contact #${groupId} ${groupMemberId}`)
|
|
if (r.type === "newMemberContact") return r.contact
|
|
throw new ChatCommandError("error creating member contact", r)
|
|
}
|
|
|
|
/**
|
|
* Send a direct message invitation to a group member contact.
|
|
* The contact must have been created with {@link apiCreateMemberContact}.
|
|
* Network usage: interactive.
|
|
*/
|
|
async apiSendMemberContactInvitation(contactId: number, message?: T.MsgContent | string): Promise<T.Contact> {
|
|
let cmd = `/_invite member contact @${contactId}`
|
|
if (message !== undefined) {
|
|
if (typeof message === "string") {
|
|
cmd += ` text ${message}`
|
|
} else {
|
|
cmd += ` json ${JSON.stringify(message)}`
|
|
}
|
|
}
|
|
const r: any = await this.sendChatCmd(cmd)
|
|
if (r.type === "newMemberContactSentInv") return r.contact
|
|
throw new ChatCommandError("error sending member contact invitation", r)
|
|
}
|
|
}
|