Merge branch 'master' into sqlcipher

This commit is contained in:
Evgeny Poberezkin
2022-09-13 08:39:24 +01:00
14 changed files with 1026 additions and 65 deletions

View File

@@ -148,12 +148,12 @@ We plan to add soon:
## For developers
We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours.
You can:
You already can:
- use SimpleX Chat library to integrate chat functionality into your apps.
- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon).
- use SimpleX Chat library to integrate chat functionality into your mobile apps.
- create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/).
- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScipt chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution.
If you are considering developing with SimpleX platform please get in touch for any advice and support.

View File

@@ -119,6 +119,10 @@ dependencies {
// Biometric authentication
implementation 'androidx.biometric:biometric:1.2.0-alpha04'
// GIFs support
implementation "io.coil-kt:coil-compose:2.1.0"
implementation "io.coil-kt:coil-gif:2.1.0"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View File

@@ -777,7 +777,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager
val cItem = r.chatItem.chatItem
chatModel.addChatItem(cInfo, cItem)
val file = cItem.file
if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE && appPrefs.privacyAcceptImages.get()) {
if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV && appPrefs.privacyAcceptImages.get()) {
withApi { receiveFile(file.fileId) }
}
if (!cItem.chatDir.sent && !cItem.isCall && (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id)) {

View File

@@ -8,6 +8,7 @@ import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.graphics.drawable.AnimatedImageDrawable
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
@@ -146,6 +147,7 @@ fun ComposeView(
val textStyle = remember { mutableStateOf(smallFont) }
// attachments
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
val chosenAnimImage = remember { mutableStateOf<Uri?>(null) }
val chosenFile = remember { mutableStateOf<Uri?>(null) }
val photoUri = remember { mutableStateOf<Uri?>(null) }
val photoTmpFile = remember { mutableStateOf<File?>(null) }
@@ -194,24 +196,23 @@ fun ComposeView(
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
}
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { uri: Uri? ->
val processPickedImage = { uri: Uri? ->
if (uri != null) {
val source = ImageDecoder.createSource(context.contentResolver, uri)
val drawable = ImageDecoder.decodeDrawable(source)
val bitmap = ImageDecoder.decodeBitmap(source)
chosenImage.value = bitmap
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagePreview))
}
}
val galleryLauncherFallback = rememberGetContentLauncher { uri: Uri? ->
if (uri != null) {
val source = ImageDecoder.createSource(context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
chosenImage.value = bitmap
if (drawable is AnimatedImageDrawable) {
// It's a gif or webp
chosenAnimImage.value = uri
} else {
chosenImage.value = bitmap
}
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagePreview))
}
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage)
val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage)
val filesLauncher = rememberGetContentLauncher { uri: Uri? ->
if (uri != null) {
val fileSize = getFileSize(context, uri)
@@ -334,6 +335,7 @@ fun ComposeView(
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
textStyle.value = smallFont
chosenImage.value = null
chosenAnimImage.value = null
chosenFile.value = null
linkUrl.value = null
prevLinkUrl.value = null
@@ -376,6 +378,13 @@ fun ComposeView(
mc = MsgContent.MCImage(cs.message, preview.image)
}
}
val chosenGifImageVal = chosenAnimImage.value
if (chosenGifImageVal != null) {
file = saveAnimImage(context, chosenGifImageVal)
if (file != null) {
mc = MsgContent.MCImage(cs.message, preview.image)
}
}
}
is ComposePreview.FilePreview -> {
val chosenFileVal = chosenFile.value
@@ -436,6 +445,7 @@ fun ComposeView(
fun cancelImage() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenImage.value = null
chosenAnimImage.value = null
}
fun cancelFile() {

View File

@@ -1,4 +1,5 @@
import android.graphics.Bitmap
import android.os.Build.VERSION.SDK_INT
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
@@ -13,14 +14,24 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import chat.simplex.app.BuildConfig
import chat.simplex.app.R
import chat.simplex.app.model.CIFile
import chat.simplex.app.model.CIFileStatus
import chat.simplex.app.views.helpers.*
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import java.io.File
@Composable
fun CIImageView(
@@ -88,13 +99,46 @@ fun CIImageView(
)
}
@Composable
fun imageView(painter: Painter, onClick: () -> Unit) {
Image(
painter,
contentDescription = stringResource(R.string.image_descr),
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
.width(1000.dp)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
),
contentScale = ContentScale.FillWidth,
)
}
Box(contentAlignment = Alignment.TopEnd) {
val context = LocalContext.current
val imageBitmap: Bitmap? = getLoadedImage(context, file)
if (imageBitmap != null) {
imageView(imageBitmap, onClick = {
val filePath = getLoadedFilePath(context, file)
if (imageBitmap != null && filePath != null) {
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
val imageLoader = ImageLoader.Builder(context)
.components {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
val imagePainter = rememberAsyncImagePainter(
ImageRequest.Builder(context).data(data = uri).size(coil.size.Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader
)
imageView(imagePainter, onClick = {
if (getLoadedFilePath(context, file) != null) {
ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, close) }
ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, uri, close) }
}
})
} else {

View File

@@ -1,4 +1,6 @@
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTransformGestures
@@ -7,13 +9,21 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import coil.size.Size
@Composable
fun ImageFullScreenView(imageBitmap: Bitmap, close: () -> Unit) {
fun ImageFullScreenView(imageBitmap: Bitmap, uri: Uri, close: () -> Unit) {
BackHandler(onBack = close)
Column(
Modifier
@@ -24,8 +34,23 @@ fun ImageFullScreenView(imageBitmap: Bitmap, close: () -> Unit) {
var scale by remember { mutableStateOf(1f) }
var translationX by remember { mutableStateOf(0f) }
var translationY by remember { mutableStateOf(0f) }
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
Image(
imageBitmap.asImageBitmap(),
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader
),
contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Fit,
modifier = Modifier

View File

@@ -57,8 +57,17 @@ fun saveImage(cxt: Context, ciFile: CIFile?) {
val fileName = ciFile?.fileName
if (filePath != null && fileName != null) {
val values = ContentValues()
val lowercaseName = fileName.lowercase()
val mimeType = when {
lowercaseName.endsWith(".png") -> "image/png"
lowercaseName.endsWith(".gif") -> "image/gif"
lowercaseName.endsWith(".webp") -> "image/webp"
lowercaseName.endsWith(".avif") -> "image/avif"
lowercaseName.endsWith(".svg") -> "image/svg+xml"
else -> "image/jpeg"
}
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
values.put(MediaStore.MediaColumns.TITLE, fileName)
val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

View File

@@ -212,6 +212,7 @@ private fun spannableStringToAnnotatedString(
// maximum image file size to be auto-accepted
const val MAX_IMAGE_SIZE: Long = 236700
const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
const val MAX_FILE_SIZE: Long = 8000000
fun getFilesDirectory(context: Context): String {
@@ -320,6 +321,32 @@ fun saveImage(context: Context, image: Bitmap): String? {
}
}
fun saveAnimImage(context: Context, uri: Uri): String? {
return try {
val filename = getFileName(context, uri)?.lowercase()
var ext = when {
// remove everything but extension
filename?.contains(".") == true -> filename.replaceBeforeLast('.', "").replace(".", "")
else -> "gif"
}
// Just in case the image has a strange extension
if (ext.length < 3 || ext.length > 4) ext = "gif"
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
val file = File(getAppFilePath(context, fileToSave))
val output = FileOutputStream(file)
context.contentResolver.openInputStream(uri)!!.use { input ->
output.use { output ->
input.copyTo(output)
}
}
fileToSave
} catch (e: Exception) {
Log.e(chat.simplex.app.TAG, "Util.kt saveAnimImage error: ${e.message}")
null
}
}
fun saveFileFromUri(context: Context, uri: Uri): String? {
return try {
val inputStream = context.contentResolver.openInputStream(uri)

View File

@@ -0,0 +1,55 @@
# Sending small files
## Problem
Sending files has a substantial constant overhead, and requires multiple online presenses from both sides, with additional connection handshake. For large files this overhead is justified, as otherwise files would consume queue quota, but for small files it makes sending files slow.
## Solution
Send small files in the same connection. There can be two modes of sending files - the one that requires explicit acceptance, but after the acceptance the file will be delivered inline, without creating a new conneciton. Another, when the file will be sent straight after the message - that would require preliminary agreement, per contact - this mode can be useful for small voice messages and gifs.
## Design
1. Add optional `fileInline :: Maybe FileInlineMode` property to `FileInvitation`:
```haskell
data FileInlineMode
= FIInvitation -- recepient must accept
| FIChunks -- file is sent after the message without acceptance
```
2. Add `XFileAcptInline SharedMsgId String` message to accept inline files, this can only be sent in case inline mode is offered, so the sender would support it:
```
{
"properties": {
"type": {"enum": ["x.file.acpt.inline"]},
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"fileName": {"type": "string"}
}
}
}
}
```
3. Add `XFileChunks` message that have to be sent in front of the sequence of chunks (sent only in `FIInvitation` mode):
```
{
"properties": {
"type": {"enum": ["x.file.chunks"]},
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"fileName": {"type": "string"}
}
}
}
}
```
4. Support file chunks in the main connection if the previous message was `XFileChunks` or `FileInvitation` in `FIChunks` mode.

View File

@@ -1,23 +1,43 @@
# SimpleX Chat JavaScript client
This is a TypeScript library that defines WebSocket API client for [SimpleX Chat terminal CLI](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md) that should be run as a WebSockets server on any port:
```bash
simplex-chat -p 5225
```
Client API provides types and functions to:
- create and change user profile (although, in most cases you can do it manually, via SimpleX Chat terminal app).
- create and accept invitations or connect with the contacts.
- create and manage long-term user address, accepting connection requests automatically.
- create, join and manage group.
- send and receive files.
## Use cases
- chat bots: you can implement any logic of connecting with and communicating with SimpleX Chat users. Using chat groups a chat bot can connect SimleX Chat users with each other.
- control of the equipment: e.g. servers or home automation. SimpleX Chat provides secure and authorised connections, so this is more secure than using rest APIs.
Please share your use cases and implementations.
## Quick start
```
npm i simplex-chat
npm run build
```
See example of chat bot in [squaring-bot.js](./examples/squaring-bot.js)
See the example of a simple chat bot in [squaring-bot.js](./examples/squaring-bot.js):
- start `simplex-chat` as a server on port 5225: `simplex-chat -p 5225 -d test_db`
- run chatbot: `node examples/squaring-bot`
- connect to chatbot via SimpleX Chat client using the address of the chat bot
## Documentation
Please refer to:
Please refer to the available client API in [client.ts](./src/client.ts).
- available client API - [client.ts](./src/client.ts).
- available commands - `ChatCommand` type in [command.ts](./src/command.ts) - if some command is not created as a ChatClient method, you can pass any command object to `sendChatCommand` method, or if the type for some command is not available you can pass command string (same strings as supported in terminal/mobile API) to `sendChatCmdStr` method.
- available chat messages - `ChatResponse` type in [response.ts](./src/command.ts).
**Please note**: you should NOT use local display names that are supported in terminal app, as they can change when contact profile is updated and you can have race conditions - use commands that use chat IDs.
## Lisense
## License
[AGPL v3](./LICENSE)

View File

@@ -6,15 +6,18 @@ run()
async function run() {
const chat = await ChatClient.create("ws://localhost:5225")
// this example assumes that you have initialized user profile for chat bot via terminal CLI
const user = await chat.apiGetActiveUser()
if (!user) {
console.log("no user profile")
return
}
console.log(`Bot profile: ${user.profile.displayName} (${user.profile.fullName})`)
// creates or uses the existing long-term address for the bot
const address = (await chat.apiGetUserAddress()) || (await chat.apiCreateUserAddress())
console.log(`Bot address: ${address}`)
await chat.sendChatCmdStr("/auto_accept on")
// enables automatic acceptance of contact connections
await chat.addressAutoAccept(true)
await processMessages(chat)
async function processMessages(chat) {
@@ -22,6 +25,7 @@ async function run() {
const resp = r instanceof Promise ? await r : r
switch (resp.type) {
case "contactConnected": {
// sends welcome message when the new contact is connected
const {contact} = resp
console.log(`${contact.profile.displayName} connected`)
await chat.apiSendTextMessage(
@@ -32,6 +36,7 @@ async function run() {
continue
}
case "newChatItem": {
// calculates the square of the number and sends the reply
const {chatInfo} = resp.chatItem
if (chatInfo.type !== ChatInfoType.Direct) continue
const msg = ciContentText(resp.chatItem.chatItem.content)

View File

@@ -1,7 +1,7 @@
import {ABQueue} from "./queue"
import {ChatTransport, ChatServer, ChatSrvRequest, ChatSrvResponse, ChatResponseError, localServer, noop} from "./transport"
import {ChatCommand, ChatType} from "./command"
import {ChatResponse} from "./response"
import {ChatCommand, ChatType, Profile} from "./command"
import {ChatResponse, ChatInfo} from "./response"
import * as CC from "./command"
import * as CR from "./response"
@@ -118,14 +118,37 @@ export class ChatClient {
}
}
async apiStopChat(): Promise<void> {
const r = await this.sendChatCommand({type: "apiStopChat"})
if (r.type !== "chatStopped") {
throw new ChatCommandError("error stopping chat", r)
}
}
apiSetIncognito(incognito: boolean): Promise<void> {
return this.okChatCommand({type: "setIncognito", incognito})
}
async addressAutoAccept(autoAccept: boolean, autoReply: CC.MsgContent): Promise<void> {
const r = await this.sendChatCommand({type: "addressAutoAccept", autoAccept, autoReply})
if (r.type !== "userContactLinkUpdated") {
throw new ChatCommandError("error changing user contact address mode", r)
}
}
async apiGetChats(): Promise<CR.Chat[]> {
const r = await this.sendChatCommand({type: "apiGetChats"})
if (r.type === "apiChats") return r.chats
throw new ChatCommandError("error loading chats", r)
}
async apiGetChat(chatType: ChatType, chatId: number, pagination: CC.ChatPagination = {count: 100}): Promise<CR.Chat> {
const r = await this.sendChatCommand({type: "apiGetChat", chatType, chatId, pagination})
async apiGetChat(
chatType: ChatType,
chatId: number,
pagination: CC.ChatPagination = {count: 100},
search: string | undefined = undefined
): Promise<CR.Chat> {
const r = await this.sendChatCommand({type: "apiGetChat", chatType, chatId, pagination, search})
if (r.type === "apiChat") return r.chat
throw new ChatCommandError("error loading chat", r)
}
@@ -152,18 +175,6 @@ export class ChatClient {
throw new ChatCommandError("error deleting chat item", r)
}
// func getUserSMPServers() throws -> [String] {
// let r = chatSendCmdSync(.getUserSMPServers)
// if case let .userSMPServers(smpServers) = r { return smpServers }
// throw r
// }
// func setUserSMPServers(smpServers: [String]) async throws {
// let r = await chatSendCmd(.setUserSMPServers(smpServers: smpServers))
// if case .cmdOk = r { return }
// throw r
// }
async apiCreateLink(): Promise<string> {
const r = await this.sendChatCommand({type: "addContact"})
if (r.type === "invitation") return r.connReqInvitation
@@ -184,7 +195,24 @@ export class ChatClient {
async apiDeleteChat(chatType: ChatType, chatId: number): Promise<void> {
const r = await this.sendChatCommand({type: "apiDeleteChat", chatType, chatId})
if (r.type !== "contactDeleted") throw new ChatCommandError("error deleting chat", r)
switch (chatType) {
case ChatType.Direct:
if (r.type === "contactDeleted") return
break
case ChatType.Group:
if (r.type === "groupDeletedUser") return
break
case ChatType.ContactRequest:
if (r.type === "contactConnectionDeleted") return
break
}
throw new ChatCommandError("error deleting chat", r)
}
async apiClearChat(chatType: ChatType, chatId: number): Promise<ChatInfo> {
const r = await this.sendChatCommand({type: "apiClearChat", chatType, chatId})
if (r.type === "chatCleared") return r.chatInfo
throw new ChatCommandError("error clearing chat", r)
}
async apiUpdateProfile(profile: CC.Profile): Promise<CC.Profile | undefined> {
@@ -199,11 +227,11 @@ export class ChatClient {
}
}
// func apiParseMarkdown(text: String) throws -> [FormattedText]? {
// let r = chatSendCmdSync(.apiParseMarkdown(text: text))
// if case let .apiParsedMarkdown(formattedText) = r { return formattedText }
// throw r
// }
async apiSetContactAlias(contactId: number, localAlias: string): Promise<CR.Contact> {
const r = await this.sendChatCommand({type: "apiSetContactAlias", contactId, localAlias})
if (r.type === "contactAliasUpdated") return r.toContact
throw new ChatCommandError("error updating contact alias", r)
}
async apiCreateUserAddress(): Promise<string> {
const r = await this.sendChatCommand({type: "createMyAddress"})
@@ -246,6 +274,66 @@ export class ChatClient {
return this.okChatCommand({type: "apiChatRead", chatType, chatId, itemRange})
}
async apiContactInfo(contactId: number): Promise<[CR.ConnectionStats?, Profile?]> {
const r = await this.sendChatCommand({type: "apiContactInfo", contactId})
if (r.type === "contactInfo") return [r.connectionStats, r.customUserProfile]
throw new ChatCommandError("error getting contact info", r)
}
async apiGroupMemberInfo(groupId: number, memberId: number): Promise<CR.ConnectionStats | undefined> {
const r = await this.sendChatCommand({type: "apiGroupMemberInfo", groupId, memberId})
if (r.type === "groupMemberInfo") return r.connectionStats_
throw new ChatCommandError("error getting group info", r)
}
async apiReceiveFile(fileId: number): Promise<CR.AChatItem> {
const r = await this.sendChatCommand({type: "receiveFile", fileId})
if (r.type === "rcvFileAccepted") return r.chatItem
throw new ChatCommandError("error receiving file", r)
}
async apiNewGroup(groupProfile: CR.GroupProfile): Promise<CR.GroupInfo> {
const r = await this.sendChatCommand({type: "newGroup", groupProfile})
if (r.type === "groupCreated") return r.groupInfo
throw new ChatCommandError("error creating group", r)
}
async apiAddMember(groupId: number, contactId: number, memberRole: CC.GroupMemberRole): Promise<CR.GroupMember> {
const r = await this.sendChatCommand({type: "apiAddMember", groupId, contactId, memberRole})
if (r.type === "sentGroupInvitation") return r.member
throw new ChatCommandError("error adding member", r)
}
async apiJoinGroup(groupId: number): Promise<CR.GroupInfo> {
const r = await this.sendChatCommand({type: "apiJoinGroup", groupId})
if (r.type === "userAcceptedGroupSent") return r.groupInfo
throw new ChatCommandError("error joining group", r)
}
async apiRemoveMember(groupId: number, memberId: number): Promise<CR.GroupMember> {
const r = await this.sendChatCommand({type: "apiRemoveMember", groupId, memberId})
if (r.type === "userDeletedMember") return r.member
throw new ChatCommandError("error removing member", r)
}
async apiLeaveGroup(groupId: number): Promise<CR.GroupInfo> {
const r = await this.sendChatCommand({type: "apiLeaveGroup", groupId})
if (r.type === "leftMemberUser") return r.groupInfo
throw new ChatCommandError("error leaving group", r)
}
async apiListMembers(groupId: number): Promise<CR.GroupMember[]> {
const r = await this.sendChatCommand({type: "apiListMembers", groupId})
if (r.type === "groupMembers") return r.group.members
throw new ChatCommandError("error getting group members", r)
}
async apiUpdateGroup(groupId: number, groupProfile: CR.GroupProfile): Promise<CR.GroupInfo> {
const r = await this.sendChatCommand({type: "apiUpdateGroupProfile", groupId, groupProfile})
if (r.type === "groupUpdated") return r.toGroup
throw new ChatCommandError("error updating group", r)
}
private async okChatCommand(command: ChatCommand): Promise<void> {
const r = await this.sendChatCommand(command)
if (r.type !== "cmdOk") throw new ChatCommandError(`${command.type} command error`, r)

View File

@@ -2,7 +2,12 @@ export type ChatCommand =
| ShowActiveUser
| CreateActiveUser
| StartChat
| APIStopChat
| SetFilesFolder
| SetIncognito
| APIExportArchive
| APIImportArchive
| APIDeleteStorage
| APIGetChats
| APIGetChat
| APISendMessage
@@ -10,12 +15,23 @@ export type ChatCommand =
| APIDeleteChatItem
| APIChatRead
| APIDeleteChat
| APIClearChat
| APIAcceptContact
| APIRejectContact
| APIUpdateProfile
| APISetContactAlias
| APIParseMarkdown
| NewGroup
| APIAddMember
| APIJoinGroup
| APIRemoveMember
| APILeaveGroup
| APIListMembers
| APIUpdateGroupProfile
| GetUserSMPServers
| SetUserSMPServers
| APIContactInfo
| APIGroupMemberInfo
| AddContact
| Connect
| ConnectSimplex
@@ -23,12 +39,48 @@ export type ChatCommand =
| DeleteMyAddress
| ShowMyAddress
| AddressAutoAccept
| ReceiveFile
| CancelFile
| FileStatus
// not included commands (they are not needed for Websocket clients, and can still be sent as strings):
// APIActivateChat
// APISuspendChat
// ResubscribeAllConnections
// APIGetChatItems - not implemented
// APISendCallInvitation
// APIRejectCall
// APISendCallOffer
// APISendCallAnswer
// APISendCallExtraInfo
// APIEndCall
// APIGetCallInvitations
// APICallStatus
// APIGetNtfToken
// APIRegisterToken
// APIVerifyToken
// APIDeleteToken
// APIGetNtfMessage
// APIMemberRole -- not implemented
// ListContacts
// ListGroups
// APISetNetworkConfig
// APIGetNetworkConfig
// APISetChatSettings
// ShowMessages
// LastMessages
// SendMessageBroadcast
type ChatCommandTag =
| "showActiveUser"
| "createActiveUser"
| "startChat"
| "apiStopChat"
| "setFilesFolder"
| "setIncognito"
| "apiExportArchive"
| "apiImportArchive"
| "apiDeleteStorage"
| "apiGetChats"
| "apiGetChat"
| "apiSendMessage"
@@ -36,12 +88,23 @@ type ChatCommandTag =
| "apiDeleteChatItem"
| "apiChatRead"
| "apiDeleteChat"
| "apiClearChat"
| "apiAcceptContact"
| "apiRejectContact"
| "apiUpdateProfile"
| "apiSetContactAlias"
| "apiParseMarkdown"
| "newGroup"
| "apiAddMember"
| "apiJoinGroup"
| "apiRemoveMember"
| "apiLeaveGroup"
| "apiListMembers"
| "apiUpdateGroupProfile"
| "getUserSMPServers"
| "setUserSMPServers"
| "apiContactInfo"
| "apiGroupMemberInfo"
| "addContact"
| "connect"
| "connectSimplex"
@@ -49,6 +112,9 @@ type ChatCommandTag =
| "deleteMyAddress"
| "showMyAddress"
| "addressAutoAccept"
| "receiveFile"
| "cancelFile"
| "fileStatus"
interface IChatCommand {
type: ChatCommandTag
@@ -65,6 +131,11 @@ export interface CreateActiveUser extends IChatCommand {
export interface StartChat extends IChatCommand {
type: "startChat"
subscribeConnections?: boolean
}
export interface APIStopChat extends IChatCommand {
type: "apiStopChat"
}
export interface SetFilesFolder extends IChatCommand {
@@ -72,8 +143,28 @@ export interface SetFilesFolder extends IChatCommand {
filePath: string
}
export interface SetIncognito extends IChatCommand {
type: "setIncognito"
incognito: boolean
}
export interface APIExportArchive extends IChatCommand {
type: "apiExportArchive"
config: ArchiveConfig
}
export interface APIImportArchive extends IChatCommand {
type: "apiImportArchive"
config: ArchiveConfig
}
export interface APIDeleteStorage extends IChatCommand {
type: "apiDeleteStorage"
}
export interface APIGetChats extends IChatCommand {
type: "apiGetChats"
pendingConnections?: boolean
}
export interface APIGetChat extends IChatCommand {
@@ -81,6 +172,7 @@ export interface APIGetChat extends IChatCommand {
chatType: ChatType
chatId: number
pagination: ChatPagination
search?: string
}
export interface APISendMessage extends IChatCommand {
@@ -130,6 +222,12 @@ export interface APIDeleteChat extends IChatCommand {
chatId: number
}
export interface APIClearChat extends IChatCommand {
type: "apiClearChat"
chatType: ChatType
chatId: number
}
export interface APIAcceptContact extends IChatCommand {
type: "apiAcceptContact"
contactReqId: number
@@ -145,11 +243,56 @@ export interface APIUpdateProfile extends IChatCommand {
profile: Profile
}
export interface APISetContactAlias extends IChatCommand {
type: "apiSetContactAlias"
contactId: number
localAlias: string
}
export interface APIParseMarkdown extends IChatCommand {
type: "apiParseMarkdown"
text: string
}
export interface NewGroup extends IChatCommand {
type: "newGroup"
groupProfile: GroupProfile
}
export interface APIAddMember extends IChatCommand {
type: "apiAddMember"
groupId: number
contactId: number
memberRole: GroupMemberRole
}
export interface APIJoinGroup extends IChatCommand {
type: "apiJoinGroup"
groupId: number
}
export interface APIRemoveMember extends IChatCommand {
type: "apiRemoveMember"
groupId: number
memberId: number
}
export interface APILeaveGroup extends IChatCommand {
type: "apiLeaveGroup"
groupId: number
}
export interface APIListMembers extends IChatCommand {
type: "apiListMembers"
groupId: number
}
export interface APIUpdateGroupProfile extends IChatCommand {
type: "apiUpdateGroupProfile"
groupId: number
groupProfile: GroupProfile
}
export interface GetUserSMPServers extends IChatCommand {
type: "getUserSMPServers"
}
@@ -159,6 +302,17 @@ export interface SetUserSMPServers extends IChatCommand {
servers: [string]
}
export interface APIContactInfo extends IChatCommand {
type: "apiContactInfo"
contactId: number
}
export interface APIGroupMemberInfo extends IChatCommand {
type: "apiGroupMemberInfo"
groupId: number
memberId: number
}
export interface AddContact extends IChatCommand {
type: "addContact"
}
@@ -186,7 +340,24 @@ export interface ShowMyAddress extends IChatCommand {
export interface AddressAutoAccept extends IChatCommand {
type: "addressAutoAccept"
enable: boolean
autoAccept: boolean
autoReply?: MsgContent
}
export interface ReceiveFile extends IChatCommand {
type: "receiveFile"
fileId: number
filePath?: string
}
export interface CancelFile extends IChatCommand {
type: "cancelFile"
fileId: number
}
export interface FileStatus extends IChatCommand {
type: "fileStatus"
fileId: number
}
export interface Profile {
@@ -208,12 +379,13 @@ export type ChatPagination =
export type ChatItemId = number
type MsgContentTag = "text" | "link" | "images"
type MsgContentTag = "text" | "link" | "image" | "file"
export type MsgContent = MCText | MCUnknown
export type MsgContent = MCText | MCLink | MCImage | MCFile | MCUnknown
interface MC {
type: MsgContentTag
text: string
}
interface MCText extends MC {
@@ -221,9 +393,32 @@ interface MCText extends MC {
text: string
}
interface MCLink extends MC {
type: "link"
text: string
preview: LinkPreview
}
interface MCImage extends MC {
type: "image"
image: string // image preview as base64 encoded data string
}
interface MCFile extends MC {
type: "file"
text: string
}
interface MCUnknown {
type: string
text?: string
text: string
}
interface LinkPreview {
uri: string
title: string
description: string
image: string
}
export enum DeleteMode {
@@ -231,6 +426,24 @@ export enum DeleteMode {
Internal = "internal",
}
interface ArchiveConfig {
archivePath: string
disableCompression?: boolean
parentTempDirectory?: string
}
export enum GroupMemberRole {
GRMember = "member",
GRAdmin = "admin",
GROwner = "owner",
}
interface GroupProfile {
displayName: string
fullName: string // can be empty string
image?: string
}
export function cmdString(cmd: ChatCommand): string {
switch (cmd.type) {
case "showActiveUser":
@@ -238,11 +451,21 @@ export function cmdString(cmd: ChatCommand): string {
case "createActiveUser":
return `/u ${JSON.stringify(cmd.profile)}`
case "startChat":
return "/_start"
return `/_start subscribe=${cmd.subscribeConnections ? "on" : "off"}`
case "apiStopChat":
return "/_stop"
case "setFilesFolder":
return `/_files_folder ${cmd.filePath}`
case "setIncognito":
return `/incognito ${cmd.incognito ? "on" : "off"}`
case "apiExportArchive":
return `/_db export ${JSON.stringify(cmd.config)}`
case "apiImportArchive":
return `/_db import ${JSON.stringify(cmd.config)}`
case "apiDeleteStorage":
return "/_db delete"
case "apiGetChats":
return "/_get chats"
return `/_get chats pcc=${cmd.pendingConnections ? "on" : "off"}`
case "apiGetChat":
return `/_get chat ${cmd.chatType}${cmd.chatId}${paginationStr(cmd.pagination)}`
case "apiSendMessage":
@@ -257,18 +480,40 @@ export function cmdString(cmd: ChatCommand): string {
}
case "apiDeleteChat":
return `/_delete ${cmd.chatType}${cmd.chatId}`
case "apiClearChat":
return `/_clear chat ${cmd.chatType}${cmd.chatId}`
case "apiAcceptContact":
return `/_accept ${cmd.contactReqId}`
case "apiRejectContact":
return `/_reject ${cmd.contactReqId}`
case "apiUpdateProfile":
return `/_profile ${JSON.stringify(cmd.profile)}`
case "apiSetContactAlias":
return `/_set alias @${cmd.contactId} ${cmd.localAlias.trim()}`
case "apiParseMarkdown":
return `/_parse ${cmd.text}`
case "newGroup":
return `/_group ${JSON.stringify(cmd.groupProfile)}`
case "apiAddMember":
return `/_add #${cmd.groupId} ${cmd.contactId} ${cmd.memberRole}`
case "apiJoinGroup":
return `/_join #${cmd.groupId}`
case "apiRemoveMember":
return `/_remove #${cmd.groupId} ${cmd.memberId}`
case "apiLeaveGroup":
return `/_leave #${cmd.groupId}`
case "apiListMembers":
return `/_members #${cmd.groupId}`
case "apiUpdateGroupProfile":
return `/_group_profile #${cmd.groupId} ${JSON.stringify(cmd.groupProfile)}`
case "getUserSMPServers":
return "/smp_servers"
case "setUserSMPServers":
return `/smp_servers ${cmd.servers.join(",") || "default"}`
case "apiContactInfo":
return `/_info @${cmd.contactId}`
case "apiGroupMemberInfo":
return `/_info #${cmd.groupId} ${cmd.memberId}`
case "addContact":
return "/connect"
case "connect":
@@ -282,7 +527,13 @@ export function cmdString(cmd: ChatCommand): string {
case "showMyAddress":
return "/show_address"
case "addressAutoAccept":
return `/auto_accept ${cmd.enable ? "on" : "off"}`
return `/auto_accept ${cmd.autoAccept ? "on" : "off"}${cmd.autoReply ? " " + JSON.stringify(cmd.autoReply) : ""}`
case "receiveFile":
return `/freceive ${cmd.fileId}${cmd.filePath ? " " + cmd.filePath : ""}`
case "cancelFile":
return `/fcancel ${cmd.fileId}`
case "fileStatus":
return `/fstatus ${cmd.fileId}`
}
}

View File

@@ -1,13 +1,16 @@
import {ChatItemId, MsgContent, DeleteMode, Profile} from "./command"
import {ChatItemId, MsgContent, DeleteMode, Profile, GroupMemberRole} from "./command"
export type ChatResponse =
| CRActiveUser
| CRChatStarted
| CRChatRunning
| CRChatStopped
| CRApiChats
| CRApiChat
| CRApiParsedMarkdown
| CRUserSMPServers
| CRContactInfo
| CRGroupMemberInfo
| CRNewChatItem
| CRChatItemStatusUpdated
| CRChatItemUpdated
@@ -20,11 +23,14 @@ export type ChatResponse =
| CRUserProfile
| CRUserProfileNoChange
| CRUserProfileUpdated
| CRContactAliasUpdated
| CRInvitation
| CRSentConfirmation
| CRSentInvitation
| CRContactUpdated
| CRContactsMerged
| CRContactDeleted
| CRChatCleared
| CRUserContactLinkCreated
| CRUserContactLinkDeleted
| CRReceivedContactRequest
@@ -38,22 +44,72 @@ export type ChatResponse =
| CRContactSubscribed
| CRContactSubError
| CRContactSubSummary
| CRContactsDisconnected
| CRContactsSubscribed
| CRHostConnected
| CRHostDisconnected
| CRGroupEmpty
| CRMemberSubError
| CRMemberSubSummary
| CRGroupSubscribed
| CRRcvFileAccepted
| CRRcvFileAcceptedSndCancelled
| CRRcvFileStart
| CRRcvFileComplete
| CRRcvFileCancelled
| CRRcvFileSndCancelled
| CRSndFileStart
| CRSndFileComplete
| CRSndFileCancelled
| CRSndFileRcvCancelled
| CRSndGroupFileCancelled
| CRSndFileSubError
| CRRcvFileSubError
| CRPendingSubSummary
| CRGroupCreated
| CRGroupMembers
| CRUserAcceptedGroupSent
| CRUserDeletedMember
| CRSentGroupInvitation
| CRLeftMemberUser
| CRGroupDeletedUser
| CRGroupInvitation
| CRReceivedGroupInvitation
| CRUserJoinedGroup
| CRJoinedGroupMember
| CRJoinedGroupMemberConnecting
| CRConnectedToGroupMember
| CRDeletedMember
| CRDeletedMemberUser
| CRLeftMember
| CRGroupRemoved
| CRGroupDeleted
| CRGroupUpdated
| CRUserContactLinkSubscribed
| CRUserContactLinkSubError
| CRNewContactConnection
| CRContactConnectionDeleted
| CRMessageError
| CRChatCmdError
| CRChatError
// not included
// CRChatItemDeletedNotFound
// CRBroadcastSent
// CRGroupsList
// CRFileTransferStatus
type ChatResponseTag =
| "activeUser"
| "chatStarted"
| "chatRunning"
| "chatStopped"
| "apiChats"
| "apiChat"
| "apiParsedMarkdown"
| "userSMPServers"
| "contactInfo"
| "groupMemberInfo"
| "newChatItem"
| "chatItemStatusUpdated"
| "chatItemUpdated"
@@ -68,11 +124,14 @@ type ChatResponseTag =
| "userProfile"
| "userProfileNoChange"
| "userProfileUpdated"
| "contactAliasUpdated"
| "invitation"
| "sentConfirmation"
| "sentInvitation"
| "contactUpdated"
| "contactsMerged"
| "contactDeleted"
| "chatCleared"
| "receivedContactRequest"
| "acceptingContactRequest"
| "contactAlreadyExists"
@@ -84,10 +143,52 @@ type ChatResponseTag =
| "contactSubscribed"
| "contactSubError"
| "contactSubSummary"
| "contactsDisconnected"
| "contactsSubscribed"
| "hostConnected"
| "hostDisconnected"
| "groupEmpty"
| "memberSubError"
| "memberSubSummary"
| "groupSubscribed"
| "rcvFileAccepted"
| "rcvFileAcceptedSndCancelled"
| "rcvFileStart"
| "rcvFileComplete"
| "rcvFileCancelled"
| "rcvFileSndCancelled"
| "sndFileStart"
| "sndFileComplete"
| "sndFileCancelled"
| "sndFileRcvCancelled"
| "sndGroupFileCancelled"
| "fileTransferStatus"
| "sndFileSubError"
| "rcvFileSubError"
| "pendingSubSummary"
| "groupCreated"
| "groupMembers"
| "userAcceptedGroupSent"
| "userDeletedMember"
| "sentGroupInvitation"
| "leftMemberUser"
| "groupDeletedUser"
| "groupInvitation"
| "receivedGroupInvitation"
| "userJoinedGroup"
| "joinedGroupMember"
| "joinedGroupMemberConnecting"
| "connectedToGroupMember"
| "deletedMember"
| "deletedMemberUser"
| "leftMember"
| "groupRemoved"
| "groupDeleted"
| "groupUpdated"
| "userContactLinkSubscribed"
| "userContactLinkSubError"
| "newContactConnection"
| "contactConnectionDeleted"
| "messageError"
| "chatCmdError"
| "chatError"
@@ -109,6 +210,10 @@ export interface CRChatRunning extends CR {
type: "chatRunning"
}
export interface CRChatStopped extends CR {
type: "chatStopped"
}
export interface CRApiChats extends CR {
type: "apiChats"
chats: Chat[]
@@ -129,6 +234,20 @@ export interface CRUserSMPServers extends CR {
smpServers: string[]
}
export interface CRContactInfo extends CR {
type: "contactInfo"
contact: Contact
connectionStats: ConnectionStats
customUserProfile?: Profile
}
export interface CRGroupMemberInfo extends CR {
type: "groupMemberInfo"
groupInfo: GroupInfo
member: GroupMember
connectionStats_?: ConnectionStats
}
export interface CRNewChatItem extends CR {
type: "newChatItem"
chatItem: AChatItem
@@ -169,6 +288,7 @@ export interface CRUserContactLinkUpdated extends CR {
type: "userContactLinkUpdated"
connReqContact: string
autoAccept: boolean
autoReply?: MsgContent
}
export interface CRContactRequestRejected extends CR {
@@ -191,6 +311,11 @@ export interface CRUserProfileUpdated extends CR {
toProfile: Profile
}
export interface CRContactAliasUpdated extends CR {
type: "contactAliasUpdated"
toContact: Contact
}
export interface CRInvitation extends CR {
type: "invitation"
connReqInvitation: string
@@ -210,11 +335,22 @@ export interface CRContactUpdated extends CR {
toContact: Contact
}
export interface CRContactsMerged extends CR {
type: "contactsMerged"
intoContact: Contact
mergedContact: Contact
}
export interface CRContactDeleted extends CR {
type: "contactDeleted"
contact: Contact
}
export interface CRChatCleared extends CR {
type: "chatCleared"
chatInfo: ChatInfo
}
export interface CRUserContactLinkCreated extends CR {
type: "userContactLinkCreated"
connReqContact: string
@@ -280,16 +416,242 @@ export interface CRContactSubSummary extends CR {
contactSubscriptions: ContactSubStatus[]
}
export interface CRContactsDisconnected extends CR {
type: "contactsDisconnected"
server: string
contactRefs: ContactRef[]
}
export interface CRContactsSubscribed extends CR {
type: "contactsSubscribed"
server: string
contactRefs: ContactRef[]
}
export interface CRHostConnected extends CR {
type: "hostConnected"
protocol: string
transportHost: string
}
export interface CRHostDisconnected extends CR {
type: "hostDisconnected"
protocol: string
transportHost: string
}
export interface CRGroupEmpty extends CR {
type: "groupEmpty"
groupInfo: GroupInfo
}
export interface CRMemberSubError extends CR {
type: "memberSubError"
groupInfo: GroupInfo
member: GroupMember
chatError: ChatError
}
export interface CRMemberSubSummary extends CR {
type: "memberSubSummary"
memberSubscriptions: MemberSubStatus[]
}
export interface CRGroupSubscribed extends CR {
type: "groupSubscribed"
groupInfo: GroupInfo
}
export interface CRRcvFileAccepted extends CR {
type: "rcvFileAccepted"
chatItem: AChatItem
}
export interface CRRcvFileAcceptedSndCancelled extends CR {
type: "rcvFileAcceptedSndCancelled"
rcvFileTransfer: RcvFileTransfer
}
export interface CRRcvFileStart extends CR {
type: "rcvFileStart"
chatItem: AChatItem
}
export interface CRRcvFileComplete extends CR {
type: "rcvFileComplete"
chatItem: AChatItem
}
export interface CRRcvFileCancelled extends CR {
type: "rcvFileCancelled"
rcvFileTransfer: RcvFileTransfer
}
export interface CRRcvFileSndCancelled extends CR {
type: "rcvFileSndCancelled"
rcvFileTransfer: RcvFileTransfer
}
export interface CRSndFileStart extends CR {
type: "sndFileStart"
chatItem: AChatItem
sndFileTransfer: SndFileTransfer
}
export interface CRSndFileComplete extends CR {
type: "sndFileComplete"
chatItem: AChatItem
sndFileTransfer: SndFileTransfer
}
export interface CRSndFileCancelled extends CR {
type: "sndFileCancelled"
chatItem: AChatItem
sndFileTransfer: SndFileTransfer
}
export interface CRSndFileRcvCancelled extends CR {
type: "sndFileRcvCancelled"
chatItem: AChatItem
sndFileTransfer: SndFileTransfer
}
export interface CRSndGroupFileCancelled extends CR {
type: "sndGroupFileCancelled"
chatItem: AChatItem
fileTransferMeta: FileTransferMeta
sndFileTransfers: SndFileTransfer[]
}
export interface CRSndFileSubError extends CR {
type: "sndFileSubError"
sndFileTransfer: SndFileTransfer
chatError: ChatError
}
export interface CRRcvFileSubError extends CR {
type: "rcvFileSubError"
rcvFileTransfer: RcvFileTransfer
chatError: ChatError
}
export interface CRPendingSubSummary extends CR {
type: "pendingSubSummary"
pendingSubStatus: PendingSubStatus[]
}
export interface CRGroupCreated extends CR {
type: "groupCreated"
groupInfo: GroupInfo
}
export interface CRGroupMembers extends CR {
type: "groupMembers"
group: Group
}
export interface CRUserAcceptedGroupSent extends CR {
type: "userAcceptedGroupSent"
groupInfo: GroupInfo
}
export interface CRUserDeletedMember extends CR {
type: "userDeletedMember"
groupInfo: GroupInfo
member: GroupMember
}
export interface CRSentGroupInvitation extends CR {
type: "sentGroupInvitation"
groupInfo: GroupInfo
contact: Contact
member: GroupMember
}
export interface CRLeftMemberUser extends CR {
type: "leftMemberUser"
groupInfo: GroupInfo
}
export interface CRGroupDeletedUser extends CR {
type: "groupDeletedUser"
groupInfo: GroupInfo
}
export interface CRGroupInvitation extends CR {
type: "groupInvitation"
groupInfo: GroupInfo
}
export interface CRReceivedGroupInvitation extends CR {
type: "receivedGroupInvitation"
groupInfo: GroupInfo
contact: Contact
memberRole: GroupMemberRole
}
export interface CRUserJoinedGroup extends CR {
type: "userJoinedGroup"
groupInfo: GroupInfo
hostMember: GroupMember
}
export interface CRJoinedGroupMember extends CR {
type: "joinedGroupMember"
groupInfo: GroupInfo
member: GroupMember
}
export interface CRJoinedGroupMemberConnecting extends CR {
type: "joinedGroupMemberConnecting"
groupInfo: GroupInfo
hostMember: GroupMember
member: GroupMember
}
export interface CRConnectedToGroupMember extends CR {
type: "connectedToGroupMember"
groupInfo: GroupInfo
member: GroupMember
}
export interface CRDeletedMember extends CR {
type: "deletedMember"
groupInfo: GroupInfo
byMember: GroupMember
deletedMember: GroupMember
}
export interface CRDeletedMemberUser extends CR {
type: "deletedMemberUser"
groupInfo: GroupInfo
member: GroupMember
}
export interface CRLeftMember extends CR {
type: "leftMember"
groupInfo: GroupInfo
member: GroupMember
}
export interface CRGroupRemoved extends CR {
type: "groupRemoved"
groupInfo: GroupInfo
}
export interface CRGroupDeleted extends CR {
type: "groupDeleted"
groupInfo: GroupInfo
member: GroupMember
}
export interface CRGroupUpdated extends CR {
type: "groupUpdated"
fromGroup: GroupInfo
toGroup: GroupInfo
member_?: GroupMember
}
export interface CRUserContactLinkSubscribed extends CR {
type: "userContactLinkSubscribed"
}
@@ -299,6 +661,16 @@ export interface CRUserContactLinkSubError extends CR {
chatError: ChatError
}
export interface CRNewContactConnection extends CR {
type: "newContactConnection"
connection: PendingContactConnection
}
export interface CRContactConnectionDeleted extends CR {
type: "contactConnectionDeleted"
connection: PendingContactConnection
}
export interface CRMessageError extends CR {
type: "messageError"
severity: string
@@ -365,6 +737,16 @@ export interface Contact {
createdAt: Date
}
export interface ContactRef {
contactId: number
localDisplayName: string
}
export interface Group {
groupInfo: GroupInfo
members: GroupMember[]
}
export interface GroupInfo {
groupId: number
localDisplayName: string
@@ -382,7 +764,7 @@ export interface GroupProfile {
export interface GroupMember {
groupMemberId: number
memberId: string
// memberRole: GroupMemberRole
memberRole: GroupMemberRole
// memberCategory: GroupMemberCategory
// memberStatus: GroupMemberStatus
// invitedBy: InvitedBy
@@ -498,7 +880,36 @@ export function ciContentText(content: CIContent): string | undefined {
}
}
interface RcvFileTransfer {}
interface RcvFileTransfer {
fileId: number
// fileInvitation: FileInvitation
// fileStatus: RcvFileStatus
senderDisplayName: string
chunkSize: number
cancelled: boolean
grpMemberId?: number
}
interface SndFileTransfer {
fileId: number
fileName: string
filePath: string
fileSize: number
chunkSize: number
recipientDisplayName: string
connId: number
// agentConnId: string
// fileStatus: FileStatus
}
interface FileTransferMeta {
fileId: number
fileName: string
filePath: string
fileSize: number
chunkSize: number
cancelled: boolean
}
export interface ChatStats {
unreadCount: number
@@ -580,6 +991,18 @@ interface ContactSubStatus {}
interface PendingSubStatus {}
export interface ConnectionStats {
rcvServers?: string[]
sndServers?: string[]
}
interface PendingContactConnection {}
interface MemberSubStatus {
member: GroupMember
memberError?: ChatError
}
interface AgentErrorType {
type: string
[x: string]: any