simplex-chat 0.3.0 → 6.5.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -22
- package/binding.gyp +50 -0
- package/cpp/simplex.cc +426 -0
- package/cpp/simplex.h +46 -0
- package/dist/api.d.ts +423 -0
- package/dist/api.js +857 -0
- package/dist/api.js.map +1 -0
- package/dist/bot.d.ts +29 -0
- package/dist/bot.js +189 -0
- package/dist/bot.js.map +1 -0
- package/dist/core.d.ts +128 -0
- package/dist/core.js +121 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +19 -2
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/simplex.d.ts +10 -0
- package/dist/simplex.js +1 -0
- package/dist/util.d.ts +15 -0
- package/dist/util.js +91 -0
- package/dist/util.js.map +1 -0
- package/docs/Namespace.api.md +33 -0
- package/docs/Namespace.bot.md +23 -0
- package/docs/Namespace.core.md +48 -0
- package/docs/Namespace.util.md +25 -0
- package/docs/README.md +12 -0
- package/docs/api.Class.ChatApi.md +1752 -0
- package/docs/api.Class.ChatCommandError.md +205 -0
- package/docs/api.Enumeration.ConnReqType.md +27 -0
- package/docs/api.Interface.BotAddressSettings.md +60 -0
- package/docs/api.TypeAlias.DbConfig.md +64 -0
- package/docs/api.TypeAlias.EventSubscriberFunc.md +27 -0
- package/docs/api.TypeAlias.EventSubscribers.md +11 -0
- package/docs/api.Variable.defaultBotAddressSettings.md +11 -0
- package/docs/bot.Function.run.md +21 -0
- package/docs/bot.Interface.BotConfig.md +75 -0
- package/docs/bot.Interface.BotOptions.md +81 -0
- package/docs/bot.TypeAlias.BotDbOpts.md +17 -0
- package/docs/core.Class.ChatAPIError.md +205 -0
- package/docs/core.Class.ChatInitError.md +205 -0
- package/docs/core.DBMigrationError.Interface.ErrorMigration.md +41 -0
- package/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md +33 -0
- package/docs/core.DBMigrationError.Interface.ErrorSQL.md +41 -0
- package/docs/core.DBMigrationError.Interface.InvalidConfirmation.md +25 -0
- package/docs/core.DBMigrationError.TypeAlias.Tag.md +11 -0
- package/docs/core.Enumeration.MigrationConfirmation.md +43 -0
- package/docs/core.Function.chatCloseStore.md +23 -0
- package/docs/core.Function.chatDecryptFile.md +31 -0
- package/docs/core.Function.chatEncryptFile.md +31 -0
- package/docs/core.Function.chatMigrateInit.md +31 -0
- package/docs/core.Function.chatReadFile.md +27 -0
- package/docs/core.Function.chatRecvMsgWait.md +27 -0
- package/docs/core.Function.chatSendCmd.md +27 -0
- package/docs/core.Function.chatWriteFile.md +31 -0
- package/docs/core.Interface.APIResult.md +31 -0
- package/docs/core.Interface.CryptoArgs.md +27 -0
- package/docs/core.Interface.UpMigration.md +25 -0
- package/docs/core.MTRError.Interface.MTREDifferent.md +33 -0
- package/docs/core.MTRError.Interface.MTRENoDown.md +33 -0
- package/docs/core.MTRError.TypeAlias.Tag.md +11 -0
- package/docs/core.MigrationError.Interface.MEDowngrade.md +33 -0
- package/docs/core.MigrationError.Interface.MEUpgrade.md +33 -0
- package/docs/core.MigrationError.Interface.MigrationError.md +33 -0
- package/docs/core.MigrationError.TypeAlias.Tag.md +11 -0
- package/docs/core.Namespace.DBMigrationError.md +18 -0
- package/docs/core.Namespace.MTRError.md +16 -0
- package/docs/core.Namespace.MigrationError.md +17 -0
- package/docs/core.TypeAlias.DBMigrationError.md +11 -0
- package/docs/core.TypeAlias.MTRError.md +11 -0
- package/docs/core.TypeAlias.MigrationError.md +11 -0
- package/docs/util.Function.botAddressSettings.md +21 -0
- package/docs/util.Function.chatInfoName.md +21 -0
- package/docs/util.Function.chatInfoRef.md +21 -0
- package/docs/util.Function.ciBotCommand.md +21 -0
- package/docs/util.Function.ciContentText.md +21 -0
- package/docs/util.Function.contactAddressStr.md +21 -0
- package/docs/util.Function.fromLocalProfile.md +21 -0
- package/docs/util.Function.reactionText.md +21 -0
- package/docs/util.Function.senderName.md +25 -0
- package/docs/util.Interface.BotCommand.md +25 -0
- package/examples/squaring-bot-pg.js +21 -0
- package/examples/squaring-bot-readme.js +17 -0
- package/examples/squaring-bot.ts +42 -0
- package/package.json +36 -48
- package/src/api.ts +939 -0
- package/src/bot.ts +214 -0
- package/src/core.ts +210 -0
- package/src/download-libs.js +239 -0
- package/src/index.ts +22 -0
- package/src/simplex.d.ts +10 -0
- package/src/simplex.js +1 -0
- package/src/util.ts +92 -0
- package/tsconfig.json +23 -0
- package/dist/client.d.ts +0 -61
- package/dist/client.js +0 -271
- package/dist/client.js.map +0 -1
- package/dist/command.d.ts +0 -408
- package/dist/command.js +0 -184
- package/dist/command.js.map +0 -1
- package/dist/index-web.d.ts +0 -1
- package/dist/index-web.js +0 -6
- package/dist/index-web.js.map +0 -1
- package/dist/index.bundle.js +0 -11
- package/dist/queue.d.ts +0 -24
- package/dist/queue.js +0 -77
- package/dist/queue.js.map +0 -1
- package/dist/response.d.ts +0 -754
- package/dist/response.js +0 -21
- package/dist/response.js.map +0 -1
- package/dist/test.html +0 -7
- package/dist/transport.d.ts +0 -54
- package/dist/transport.js +0 -131
- package/dist/transport.js.map +0 -1
- package/dist/websocket.d.ts +0 -8
- package/dist/websocket.js +0 -4
- package/dist/websocket.js.map +0 -1
package/src/bot.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {T} from "@simplex-chat/types"
|
|
2
|
+
import * as api from "./api"
|
|
3
|
+
import * as core from "./core"
|
|
4
|
+
import * as util from "./util"
|
|
5
|
+
import equal = require("fast-deep-equal")
|
|
6
|
+
|
|
7
|
+
export type BotDbOpts = api.DbConfig & {
|
|
8
|
+
confirmMigrations?: core.MigrationConfirmation
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BotOptions {
|
|
12
|
+
createAddress?: boolean
|
|
13
|
+
updateAddress?: boolean
|
|
14
|
+
updateProfile?: boolean
|
|
15
|
+
addressSettings?: api.BotAddressSettings
|
|
16
|
+
allowFiles?: boolean
|
|
17
|
+
commands?: T.ChatBotCommand[] // commands to show in client UI
|
|
18
|
+
useBotProfile?: boolean // create profile not marked as a bot, with default preferences
|
|
19
|
+
logContacts?: boolean
|
|
20
|
+
logNetwork?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaultOpts: Required<BotOptions> = {
|
|
24
|
+
createAddress: true,
|
|
25
|
+
updateAddress: true,
|
|
26
|
+
updateProfile: true,
|
|
27
|
+
addressSettings: api.defaultBotAddressSettings,
|
|
28
|
+
allowFiles: false,
|
|
29
|
+
commands: [],
|
|
30
|
+
useBotProfile: true,
|
|
31
|
+
logContacts: true,
|
|
32
|
+
logNetwork: false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BotConfig {
|
|
36
|
+
profile: T.Profile,
|
|
37
|
+
dbOpts: BotDbOpts,
|
|
38
|
+
options: BotOptions,
|
|
39
|
+
onMessage?: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise<void>,
|
|
40
|
+
// command handlers can be different from commands to be shown in client UI
|
|
41
|
+
onCommands?: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise<void>)},
|
|
42
|
+
// If you use `onMessage` and to subscribe "newChatItems" event, exclude content messages from processing
|
|
43
|
+
// If you use `onCommands` and to subscribe "newChatItems" event, exclude commands from processing
|
|
44
|
+
events?: api.EventSubscribers
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function run({profile, dbOpts, options = defaultOpts, onMessage, onCommands = {}, events = {}}: BotConfig): Promise<[api.ChatApi, T.User, T.UserContactLink | undefined]> {
|
|
48
|
+
const bot = await api.ChatApi.init(dbOpts, dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp)
|
|
49
|
+
const opts = fullOptions(options)
|
|
50
|
+
if (onMessage) subscribeMessages(bot, onMessage)
|
|
51
|
+
if (Object.keys(onCommands).length > 0) subscribeCommands(bot, onCommands)
|
|
52
|
+
if (Object.keys(events).length > 0) bot.on(events)
|
|
53
|
+
subscribeLogEvents(bot, opts)
|
|
54
|
+
const botProfile = mkBotProfile(profile, opts)
|
|
55
|
+
const user = await createBotUser(bot, botProfile)
|
|
56
|
+
await bot.startChat()
|
|
57
|
+
const address = await createOrUpdateAddress(bot, user, opts)
|
|
58
|
+
if (address) {
|
|
59
|
+
const addressLink = util.contactAddressStr(address.connLinkContact)
|
|
60
|
+
console.log(`Bot address: ${addressLink}`)
|
|
61
|
+
if (opts.useBotProfile) botProfile.contactLink = addressLink
|
|
62
|
+
}
|
|
63
|
+
await updateBotUserProfile(bot, user, botProfile, opts)
|
|
64
|
+
return [bot, user, address]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fullOptions(options: BotOptions): Required<BotOptions> {
|
|
68
|
+
const opts = {
|
|
69
|
+
createAddress: options.createAddress ?? defaultOpts.createAddress,
|
|
70
|
+
updateAddress: options.updateAddress ?? defaultOpts.updateAddress,
|
|
71
|
+
updateProfile: options.updateProfile ?? defaultOpts.updateProfile,
|
|
72
|
+
addressSettings: options.addressSettings ?? defaultOpts.addressSettings,
|
|
73
|
+
allowFiles: options.allowFiles ?? defaultOpts.allowFiles,
|
|
74
|
+
commands: options.commands ?? defaultOpts.commands,
|
|
75
|
+
useBotProfile: options.useBotProfile ?? defaultOpts.useBotProfile,
|
|
76
|
+
logContacts: options.logContacts ?? defaultOpts.logContacts,
|
|
77
|
+
logNetwork: options.logNetwork ?? defaultOpts.logNetwork
|
|
78
|
+
}
|
|
79
|
+
const welcomeMessage = opts.addressSettings.welcomeMessage ?? defaultOpts.addressSettings.welcomeMessage
|
|
80
|
+
opts.addressSettings = {
|
|
81
|
+
autoAccept: opts.addressSettings.autoAccept ?? defaultOpts.addressSettings.autoAccept,
|
|
82
|
+
welcomeMessage: typeof welcomeMessage === "string" ? {type: "text", text: welcomeMessage} : welcomeMessage,
|
|
83
|
+
businessAddress: opts.addressSettings.businessAddress ?? defaultOpts.addressSettings.businessAddress
|
|
84
|
+
}
|
|
85
|
+
return opts
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mkBotProfile(profile: T.Profile, opts: Required<BotOptions>): T.Profile {
|
|
89
|
+
if (opts.useBotProfile) {
|
|
90
|
+
const prefs = profile.preferences || {}
|
|
91
|
+
if (prefs.files || prefs.calls || prefs.voice || prefs.commands) {
|
|
92
|
+
console.log("Option useBotProfile is enabled and profile preferences used for files, calls, voice or commands, exiting")
|
|
93
|
+
process.exit()
|
|
94
|
+
}
|
|
95
|
+
prefs.files = {allow: opts.allowFiles ? T.FeatureAllowed.Yes : T.FeatureAllowed.No}
|
|
96
|
+
prefs.calls = {allow: T.FeatureAllowed.No}
|
|
97
|
+
prefs.voice = {allow: T.FeatureAllowed.No}
|
|
98
|
+
prefs.commands = opts.commands
|
|
99
|
+
profile.preferences = prefs
|
|
100
|
+
profile.peerType = T.ChatPeerType.Bot
|
|
101
|
+
} else if (opts.commands.length > 0) {
|
|
102
|
+
console.log("Option useBotProfile is disabled and commands are passed, exiting")
|
|
103
|
+
process.exit()
|
|
104
|
+
}
|
|
105
|
+
return profile
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function subscribeMessages(bot: api.ChatApi, onMessage: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise<void>) {
|
|
109
|
+
bot.on("newChatItems", async ({chatItems}) => {
|
|
110
|
+
for (const ci of chatItems) {
|
|
111
|
+
if (ci.chatItem.content.type === "rcvMsgContent") {
|
|
112
|
+
try {
|
|
113
|
+
const p = onMessage(ci, ci.chatItem.content.msgContent)
|
|
114
|
+
if (p instanceof Promise) await p
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.log("message processing error", e)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function subscribeCommands(bot: api.ChatApi, commands: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise<void>)}) {
|
|
124
|
+
bot.on("newChatItems", async (evt) => {
|
|
125
|
+
for (const ci of evt.chatItems) {
|
|
126
|
+
const cmd = util.ciBotCommand(ci.chatItem)
|
|
127
|
+
if (cmd) {
|
|
128
|
+
const cmdFunc = commands[cmd.keyword] || commands[""]
|
|
129
|
+
if (cmdFunc) {
|
|
130
|
+
try {
|
|
131
|
+
const p = cmdFunc(ci, cmd)
|
|
132
|
+
if (p instanceof Promise) await p
|
|
133
|
+
} catch(e) {
|
|
134
|
+
console.log(`${cmd} command processing error`, e)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function subscribeLogEvents(bot: api.ChatApi, opts: Required<BotOptions>) {
|
|
143
|
+
if (opts.logContacts) {
|
|
144
|
+
bot.on({
|
|
145
|
+
"contactConnected": ({contact}) => console.log(`${contact.profile.displayName} connected`),
|
|
146
|
+
"contactDeletedByContact": ({contact}) => console.log(`${contact.profile.displayName} deleted connection with bot`)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
if (opts.logNetwork) {
|
|
150
|
+
bot.on({
|
|
151
|
+
"hostConnected": ({transportHost}) => console.log(`connected server ${transportHost}`),
|
|
152
|
+
"hostDisconnected": ({transportHost}) => console.log(`diconnected server ${transportHost}`),
|
|
153
|
+
"subscriptionStatus": ({subscriptionStatus, connections}) => console.log(`${connections.length} subscription(s) ${subscriptionStatus.type}`)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function createBotUser(bot: api.ChatApi, profile: T.Profile): Promise<T.User> {
|
|
159
|
+
let user = await bot.apiGetActiveUser()
|
|
160
|
+
if (!user) {
|
|
161
|
+
console.log("No active user in database, creating...")
|
|
162
|
+
user = await bot.apiCreateActiveUser(profile)
|
|
163
|
+
}
|
|
164
|
+
console.log("Bot user: ", user.profile.displayName)
|
|
165
|
+
return user
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function createOrUpdateAddress(bot: api.ChatApi, user: T.User, opts: Required<BotOptions>): Promise<T.UserContactLink | undefined> {
|
|
169
|
+
const {userId} = user
|
|
170
|
+
let address = await bot.apiGetUserAddress(userId)
|
|
171
|
+
if (!address) {
|
|
172
|
+
if (opts.createAddress) {
|
|
173
|
+
console.log("Bot has no address, creating...")
|
|
174
|
+
await bot.apiCreateUserAddress(userId)
|
|
175
|
+
address = await bot.apiGetUserAddress(userId)
|
|
176
|
+
if (!address) {
|
|
177
|
+
console.log("Failed reading created user address, exiting")
|
|
178
|
+
process.exit()
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
console.log("Warning: bot has no address")
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const addressSettings = opts.addressSettings || defaultOpts.addressSettings
|
|
187
|
+
if (!equal(util.botAddressSettings(address), addressSettings)) {
|
|
188
|
+
if (opts.updateAddress) {
|
|
189
|
+
console.log("Bot address settings changed, updating...")
|
|
190
|
+
await bot.apiSetAddressSettings(userId, addressSettings)
|
|
191
|
+
} else {
|
|
192
|
+
console.log("Bot address settings changed")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return address
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function updateBotUserProfile(bot: api.ChatApi, user: T.User, profile: T.Profile, opts: Required<BotOptions>): Promise<void> {
|
|
200
|
+
const {userId} = user
|
|
201
|
+
if (!equal(util.fromLocalProfile(user.profile), profile)) {
|
|
202
|
+
if (opts.updateProfile) {
|
|
203
|
+
console.log("Bot profile changed, updating...")
|
|
204
|
+
const summary = await bot.apiUpdateProfile(userId, profile)
|
|
205
|
+
console.log(
|
|
206
|
+
summary
|
|
207
|
+
? `Bot profile updated: ${summary.updateSuccesses} updated contact(s), ${summary.updateFailures} failed contact update(s).`
|
|
208
|
+
: "Unexpected: profile did not change!"
|
|
209
|
+
)
|
|
210
|
+
} else {
|
|
211
|
+
console.log("Bot profile changed")
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {ChatEvent, ChatResponse, T} from "@simplex-chat/types"
|
|
2
|
+
import * as simplex from "./simplex"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Initialize chat controller
|
|
6
|
+
*/
|
|
7
|
+
export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise<bigint> {
|
|
8
|
+
const [ctrl, res] = await simplex.chat_migrate_init(dbPath, dbKey, confirm)
|
|
9
|
+
const json = JSON.parse(res)
|
|
10
|
+
if (json.type === 'ok') return ctrl
|
|
11
|
+
throw new ChatInitError("Database or migration error (see dbMigrationError property)", json as DBMigrationError)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Close chat store
|
|
16
|
+
*/
|
|
17
|
+
export async function chatCloseStore(ctrl: bigint): Promise<void> {
|
|
18
|
+
const res = await simplex.chat_close_store(ctrl)
|
|
19
|
+
if (res !== "") throw new Error(res)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Send chat command as string
|
|
24
|
+
*/
|
|
25
|
+
export async function chatSendCmd(ctrl: bigint, cmd: string): Promise<ChatResponse> {
|
|
26
|
+
const res = await simplex.chat_send_cmd(ctrl, cmd)
|
|
27
|
+
const json = JSON.parse(res) as APIResult<ChatResponse>
|
|
28
|
+
// console.log(cmd.slice(0, 16), json.result?.type || json.error)
|
|
29
|
+
if (typeof json.result === 'object') return json.result
|
|
30
|
+
if (typeof json.error === 'object') throw new ChatAPIError("Chat command error (see chatError property)", json.error as T.ChatError)
|
|
31
|
+
throw new ChatAPIError("Invalid chat command result")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Receive chat event
|
|
36
|
+
*/
|
|
37
|
+
export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise<ChatEvent | undefined> {
|
|
38
|
+
const res = await simplex.chat_recv_msg_wait(ctrl, wait)
|
|
39
|
+
if (res === "") return undefined
|
|
40
|
+
const json = JSON.parse(res) as APIResult<ChatEvent>
|
|
41
|
+
// if (json.result) console.log("event", json.result.type)
|
|
42
|
+
if (typeof json.result === 'object') return json.result
|
|
43
|
+
if (typeof json.error === 'object') throw new ChatAPIError("Chat event error (see chatError property)", json.error as T.ChatError)
|
|
44
|
+
throw new ChatAPIError("Invalid chat event")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Write buffer to encrypted file
|
|
49
|
+
*/
|
|
50
|
+
export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise<CryptoArgs> {
|
|
51
|
+
const res = await simplex.chat_write_file(ctrl, path, buffer)
|
|
52
|
+
return cryptoArgsResult(res)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read buffer from encrypted file
|
|
57
|
+
*/
|
|
58
|
+
export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise<ArrayBuffer> {
|
|
59
|
+
return await simplex.chat_read_file(path, fileKey, fileNonce)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Encrypt file
|
|
64
|
+
*/
|
|
65
|
+
export async function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise<CryptoArgs> {
|
|
66
|
+
const res = await simplex.chat_encrypt_file(ctrl, fromPath, toPath)
|
|
67
|
+
return cryptoArgsResult(res)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Decrypt file
|
|
72
|
+
*/
|
|
73
|
+
export async function chatDecryptFile(fromPath: string, {fileKey, fileNonce}: CryptoArgs, toPath: string): Promise<void> {
|
|
74
|
+
const res = await simplex.chat_decrypt_file(fromPath, fileKey, fileNonce, toPath)
|
|
75
|
+
if (res !== "") throw new Error(res)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cryptoArgsResult(res: string): CryptoArgs {
|
|
79
|
+
const json = JSON.parse(res)
|
|
80
|
+
switch (json.type) {
|
|
81
|
+
case "result": return json.cryptoArgs as CryptoArgs
|
|
82
|
+
case "error": throw Error(json.writeError)
|
|
83
|
+
default: throw Error("unexpected chat_write_file result: " + res)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface APIResult<R> {
|
|
88
|
+
result?: R
|
|
89
|
+
error?: T.ChatError
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class ChatAPIError extends Error {
|
|
93
|
+
constructor(public message: string, public chatError: T.ChatError | undefined = undefined) {
|
|
94
|
+
super(message)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Migration confirmation mode
|
|
100
|
+
*/
|
|
101
|
+
export enum MigrationConfirmation {
|
|
102
|
+
YesUp = "yesUp",
|
|
103
|
+
YesUpDown = "yesUpDown",
|
|
104
|
+
Console = "console",
|
|
105
|
+
Error = "error"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* File encryption key and nonce
|
|
110
|
+
*/
|
|
111
|
+
export interface CryptoArgs {
|
|
112
|
+
fileKey: string
|
|
113
|
+
fileNonce: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class ChatInitError extends Error {
|
|
117
|
+
constructor(public message: string, public dbMigrationError: DBMigrationError) {
|
|
118
|
+
super(message)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type DBMigrationError =
|
|
123
|
+
| DBMigrationError.InvalidConfirmation
|
|
124
|
+
| DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key
|
|
125
|
+
| DBMigrationError.ErrorMigration
|
|
126
|
+
| DBMigrationError.ErrorSQL
|
|
127
|
+
|
|
128
|
+
export namespace DBMigrationError {
|
|
129
|
+
export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL"
|
|
130
|
+
|
|
131
|
+
interface Interface {
|
|
132
|
+
type: Tag
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface InvalidConfirmation extends Interface {
|
|
136
|
+
type: "invalidConfirmation"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ErrorNotADatabase extends Interface {
|
|
140
|
+
type: "errorNotADatabase"
|
|
141
|
+
dbFile: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ErrorMigration extends Interface {
|
|
145
|
+
type: "errorMigration"
|
|
146
|
+
dbFile: string
|
|
147
|
+
migrationError: MigrationError
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ErrorSQL extends Interface {
|
|
151
|
+
type: "errorSQL"
|
|
152
|
+
dbFile: string
|
|
153
|
+
migrationSQLError: string
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type MigrationError =
|
|
158
|
+
| MigrationError.MEUpgrade
|
|
159
|
+
| MigrationError.MEDowngrade
|
|
160
|
+
| MigrationError.MigrationError
|
|
161
|
+
|
|
162
|
+
export namespace MigrationError {
|
|
163
|
+
export type Tag = "upgrade" | "downgrade" | "migrationError"
|
|
164
|
+
|
|
165
|
+
interface Interface {
|
|
166
|
+
type: Tag
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface MEUpgrade extends Interface {
|
|
170
|
+
type: "upgrade"
|
|
171
|
+
upMigrations: UpMigration
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface MEDowngrade extends Interface {
|
|
175
|
+
type: "downgrade"
|
|
176
|
+
downMigrations: string[]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface MigrationError extends Interface {
|
|
180
|
+
type: "migrationError"
|
|
181
|
+
mtrError: MTRError
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface UpMigration {
|
|
186
|
+
upName: string
|
|
187
|
+
withDown: boolean
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export type MTRError =
|
|
191
|
+
| MTRError.MTRENoDown
|
|
192
|
+
| MTRError.MTREDifferent
|
|
193
|
+
|
|
194
|
+
export namespace MTRError {
|
|
195
|
+
export type Tag = "noDown" | "different"
|
|
196
|
+
|
|
197
|
+
interface Interface {
|
|
198
|
+
type: Tag
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface MTRENoDown extends Interface {
|
|
202
|
+
type: "noDown"
|
|
203
|
+
upMigrations: UpMigration
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface MTREDifferent extends Interface {
|
|
207
|
+
type: "different"
|
|
208
|
+
downMigrations: string[]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const extract = require('extract-zip');
|
|
5
|
+
|
|
6
|
+
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
|
|
7
|
+
const RELEASE_TAG = 'v6.5.0-beta.10';
|
|
8
|
+
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
|
|
9
|
+
|
|
10
|
+
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
|
|
11
|
+
console.error(`✗ Invalid SIMPLEX_BACKEND: "${BACKEND}". Must be "sqlite" or "postgres".`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (BACKEND === 'postgres' && (process.platform !== 'linux' || process.arch !== 'x64')) {
|
|
16
|
+
console.error(`✗ SIMPLEX_BACKEND=postgres is only supported on Linux x86_64.`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ROOT_DIR = process.cwd(); // Root of the package being installed
|
|
21
|
+
const LIBS_DIR = path.join(ROOT_DIR, 'libs')
|
|
22
|
+
const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt');
|
|
23
|
+
|
|
24
|
+
// Detect platform and architecture
|
|
25
|
+
function getPlatformInfo() {
|
|
26
|
+
const platform = process.platform;
|
|
27
|
+
const arch = process.arch;
|
|
28
|
+
|
|
29
|
+
let platformName;
|
|
30
|
+
let archName;
|
|
31
|
+
|
|
32
|
+
if (platform === 'linux') {
|
|
33
|
+
platformName = 'linux';
|
|
34
|
+
} else if (platform === 'darwin') {
|
|
35
|
+
platformName = 'macos';
|
|
36
|
+
} else if (platform === 'win32') {
|
|
37
|
+
platformName = 'windows';
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (arch === 'x64') {
|
|
43
|
+
archName = 'x86_64';
|
|
44
|
+
} else if (arch === 'arm64') {
|
|
45
|
+
archName = 'aarch64';
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { platformName, archName };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Cleanup on libs version mismatch
|
|
54
|
+
function cleanLibsDirectory() {
|
|
55
|
+
if (fs.existsSync(LIBS_DIR)) {
|
|
56
|
+
console.log('Cleaning old libraries...');
|
|
57
|
+
fs.rmSync(LIBS_DIR, { recursive: true, force: true });
|
|
58
|
+
fs.mkdirSync(LIBS_DIR, { recursive: true });
|
|
59
|
+
console.log('✓ Old libraries removed');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if libraries are already installed with the correct version
|
|
64
|
+
function isAlreadyInstalled() {
|
|
65
|
+
if (!fs.existsSync(INSTALLED_FILE)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim();
|
|
71
|
+
const expectedVersion = `${RELEASE_TAG}:${BACKEND}`;
|
|
72
|
+
if (installedVersion === expectedVersion) {
|
|
73
|
+
console.log(`✓ Libraries version ${RELEASE_TAG}:${BACKEND} already installed`);
|
|
74
|
+
return true;
|
|
75
|
+
} else {
|
|
76
|
+
console.log(`Version mismatch: installed ${installedVersion}, need ${expectedVersion}`);
|
|
77
|
+
cleanLibsDirectory();
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.warn(`Could not read installed.txt: ${err.message}`);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function install() {
|
|
87
|
+
try {
|
|
88
|
+
// Check if already installed
|
|
89
|
+
if (isAlreadyInstalled()) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { platformName, archName } = getPlatformInfo();
|
|
94
|
+
const repoName = GITHUB_REPO.split('/')[1];
|
|
95
|
+
const backendSuffix = BACKEND === 'postgres' ? '-postgres' : '';
|
|
96
|
+
const zipFilename = `${repoName}-${platformName}-${archName}${backendSuffix}.zip`;
|
|
97
|
+
const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`;
|
|
98
|
+
const ZIP_PATH = path.join(ROOT_DIR, zipFilename);
|
|
99
|
+
const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract');
|
|
100
|
+
|
|
101
|
+
console.log(`Detected: ${platformName} ${archName}`);
|
|
102
|
+
console.log(`Backend: ${BACKEND}`);
|
|
103
|
+
console.log(`Downloading: ${zipFilename}`);
|
|
104
|
+
|
|
105
|
+
// Create libs directory
|
|
106
|
+
if (!fs.existsSync(LIBS_DIR)) {
|
|
107
|
+
fs.mkdirSync(LIBS_DIR, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Download zip with error handling
|
|
111
|
+
await downloadFile(ZIP_URL, ZIP_PATH);
|
|
112
|
+
|
|
113
|
+
// Extract to temporary directory
|
|
114
|
+
console.log('Extracting to temporary directory...');
|
|
115
|
+
if (!fs.existsSync(TEMP_EXTRACT_DIR)) {
|
|
116
|
+
fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
await extract(ZIP_PATH, { dir: TEMP_EXTRACT_DIR });
|
|
119
|
+
|
|
120
|
+
// Move libs folder contents to final location
|
|
121
|
+
console.log('Moving libraries to libs/...');
|
|
122
|
+
const libsSourcePath = path.join(TEMP_EXTRACT_DIR, 'libs');
|
|
123
|
+
|
|
124
|
+
if (fs.existsSync(libsSourcePath)) {
|
|
125
|
+
// Copy all files from libs folder to LIBS_DIR
|
|
126
|
+
const files = fs.readdirSync(libsSourcePath);
|
|
127
|
+
files.forEach(file => {
|
|
128
|
+
const src = path.join(libsSourcePath, file);
|
|
129
|
+
const dest = path.join(LIBS_DIR, file);
|
|
130
|
+
|
|
131
|
+
if (fs.statSync(src).isDirectory()) {
|
|
132
|
+
copyDirSync(src, dest);
|
|
133
|
+
} else {
|
|
134
|
+
fs.copyFileSync(src, dest);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error('libs folder not found in zip archive');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Write installed.txt with version
|
|
142
|
+
fs.writeFileSync(INSTALLED_FILE, `${RELEASE_TAG}:${BACKEND}`, 'utf-8');
|
|
143
|
+
console.log(`✓ Wrote version ${RELEASE_TAG}:${BACKEND} to installed.txt`);
|
|
144
|
+
|
|
145
|
+
// Cleanup
|
|
146
|
+
fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true });
|
|
147
|
+
fs.unlinkSync(ZIP_PATH);
|
|
148
|
+
console.log('✓ Installation complete');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error('✗ Failed:', err.message);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Helper function to recursively copy directories
|
|
156
|
+
function copyDirSync(src, dest) {
|
|
157
|
+
if (!fs.existsSync(dest)) {
|
|
158
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
159
|
+
}
|
|
160
|
+
const files = fs.readdirSync(src);
|
|
161
|
+
files.forEach(file => {
|
|
162
|
+
const srcFile = path.join(src, file);
|
|
163
|
+
const destFile = path.join(dest, file);
|
|
164
|
+
if (fs.statSync(srcFile).isDirectory()) {
|
|
165
|
+
copyDirSync(srcFile, destFile);
|
|
166
|
+
} else {
|
|
167
|
+
fs.copyFileSync(srcFile, destFile);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function downloadFile(url, dest) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const file = fs.createWriteStream(dest);
|
|
175
|
+
|
|
176
|
+
https.get(url, { headers: { 'User-Agent': 'Node.js' } }, (response) => {
|
|
177
|
+
// Handle redirects
|
|
178
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
179
|
+
file.destroy();
|
|
180
|
+
fs.unlink(dest, () => {});
|
|
181
|
+
return downloadFile(response.headers.location, dest)
|
|
182
|
+
.then(resolve)
|
|
183
|
+
.catch(reject);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Handle 404
|
|
187
|
+
if (response.statusCode === 404) {
|
|
188
|
+
file.destroy();
|
|
189
|
+
fs.unlink(dest, () => {});
|
|
190
|
+
reject(new Error(
|
|
191
|
+
`Release artifact not found (404). Check:\n` +
|
|
192
|
+
` - Repository exists: ${url.split('/releases')[0]}\n` +
|
|
193
|
+
` - Release tag exists: ${RELEASE_TAG}\n` +
|
|
194
|
+
` - Artifact filename is correct`
|
|
195
|
+
));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Handle 403
|
|
200
|
+
if (response.statusCode === 403) {
|
|
201
|
+
file.destroy();
|
|
202
|
+
fs.unlink(dest, () => {});
|
|
203
|
+
reject(new Error(
|
|
204
|
+
`Access denied (403). The repository may be private.\n` +
|
|
205
|
+
`Set GITHUB_TOKEN environment variable for private repos.`
|
|
206
|
+
));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle other HTTP errors
|
|
211
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
212
|
+
file.destroy();
|
|
213
|
+
fs.unlink(dest, () => {});
|
|
214
|
+
reject(new Error(
|
|
215
|
+
`HTTP ${response.statusCode}: Failed to download from ${url}`
|
|
216
|
+
));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
response.pipe(file);
|
|
221
|
+
|
|
222
|
+
file.on('finish', () => {
|
|
223
|
+
file.close();
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
file.on('error', (err) => {
|
|
228
|
+
fs.unlink(dest, () => {});
|
|
229
|
+
reject(new Error(`File write error: ${err.message}`));
|
|
230
|
+
});
|
|
231
|
+
}).on('error', (err) => {
|
|
232
|
+
file.destroy();
|
|
233
|
+
fs.unlink(dest, () => {});
|
|
234
|
+
reject(new Error(`Download error: ${err.message}`));
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
install();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple declarative API to run a chat-bot with a single function call.
|
|
3
|
+
* It automates creating and updating of the bot profile, address and bot commands shown in the app UI.
|
|
4
|
+
*/
|
|
5
|
+
export * as bot from "./bot"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An API to send chat commands and receive chat events to/from chat core.
|
|
9
|
+
* You need to use it in bot event handlers, and for any other use cases.
|
|
10
|
+
*/
|
|
11
|
+
export * as api from "./api"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A low level API to the core library - the same that is used in desktop clients.
|
|
15
|
+
* You are unlikely to ever need to use this module directly.
|
|
16
|
+
*/
|
|
17
|
+
export * as core from "./core"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Useful functions for chat events and types.
|
|
21
|
+
*/
|
|
22
|
+
export * as util from "./util"
|
package/src/simplex.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// These functions are defined in CPP add-on ../cpp/simplex.cc
|
|
2
|
+
|
|
3
|
+
export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]>
|
|
4
|
+
export function chat_close_store(ctrl: bigint): Promise<string>
|
|
5
|
+
export function chat_send_cmd(ctrl: bigint, cmd: string): Promise<string>
|
|
6
|
+
export function chat_recv_msg_wait(ctrl: bigint, wait: number): Promise<string>
|
|
7
|
+
export function chat_write_file(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise<string>
|
|
8
|
+
export function chat_read_file(path: string, key: string, nonce: string): Promise<ArrayBuffer>
|
|
9
|
+
export function chat_encrypt_file(ctrl: bigint, fromPath: string, toPath: string): Promise<string>
|
|
10
|
+
export function chat_decrypt_file(fromPath: string, key: string, nonce: string, toPath: string): Promise<string>
|
package/src/simplex.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("../build/Release/simplex")
|