Files
chatmail-core/node/lib/context.ts
Hocuri 9cd000c4f2 feat: Verified 1:1 chats (#4315)
Implement #4188

BREAKING CHANGE: Remove unused DC_STR_PROTECTION_(EN)ABLED* strings
BREAKING CHANGE: Remove unused dc_set_chat_protection()
2023-07-09 14:06:45 +02:00

939 lines
26 KiB
TypeScript

/* eslint-disable camelcase */
import binding from './binding'
import { C, EventId2EventName } from './constants'
import { Chat } from './chat'
import { ChatList } from './chatlist'
import { Contact } from './contact'
import { Message } from './message'
import { Lot } from './lot'
import { Locations } from './locations'
import rawDebug from 'debug'
import { AccountManager } from './deltachat'
import { join } from 'path'
import { EventEmitter } from 'stream'
const debug = rawDebug('deltachat:node:index')
const noop = function () {}
interface NativeContext {}
/**
* Wrapper around dcn_context_t*
*
* only acts as event emitter when created in standalone mode (without account manager)
* with `Context.open`
*/
export class Context extends EventEmitter {
constructor(
readonly manager: AccountManager | null,
private inner_dcn_context: NativeContext,
readonly account_id: number | null
) {
super()
debug('DeltaChat constructor')
if (inner_dcn_context === null) {
throw new Error('inner_dcn_context can not be null')
}
}
/** Opens a stanalone context (without an account manager)
* automatically starts the event handler */
static open(cwd: string): Context {
const dbFile = join(cwd, 'db.sqlite')
const context = new Context(null, binding.dcn_context_new(dbFile), null)
debug('Opened context')
function handleCoreEvent(
eventId: number,
data1: number,
data2: number | string
) {
const eventString = EventId2EventName[eventId]
debug(eventString, data1, data2)
if (!context.emit) {
console.log('Received an event but EventEmitter is already destroyed.')
console.log(eventString, data1, data2)
return
}
context.emit(eventString, data1, data2)
context.emit('ALL', eventString, data1, data2)
}
binding.dcn_start_event_handler(
context.dcn_context,
handleCoreEvent.bind(this)
)
debug('Started event handler')
return context
}
get dcn_context() {
return this.inner_dcn_context
}
get is_open() {
return Boolean(binding.dcn_context_is_open())
}
open(passphrase?: string) {
return Boolean(
binding.dcn_context_open(this.dcn_context, passphrase ? passphrase : '')
)
}
unref() {
binding.dcn_context_unref(this.dcn_context)
;(this.inner_dcn_context as any) = null
}
acceptChat(chatId: number) {
binding.dcn_accept_chat(this.dcn_context, chatId)
}
blockChat(chatId: number) {
binding.dcn_block_chat(this.dcn_context, chatId)
}
addAddressBook(addressBook: string) {
debug(`addAddressBook ${addressBook}`)
return binding.dcn_add_address_book(this.dcn_context, addressBook)
}
addContactToChat(chatId: number, contactId: number) {
debug(`addContactToChat ${chatId} ${contactId}`)
return Boolean(
binding.dcn_add_contact_to_chat(
this.dcn_context,
Number(chatId),
Number(contactId)
)
)
}
addDeviceMessage(label: string, msg: Message | string) {
debug(`addDeviceMessage ${label} ${msg}`)
if (!msg) {
throw new Error('invalid msg parameter')
}
if (typeof label !== 'string') {
throw new Error('invalid label parameter, must be a string')
}
if (typeof msg === 'string') {
const msgObj = this.messageNew()
msgObj.setText(msg)
msg = msgObj
}
if (!msg.dc_msg) {
throw new Error('invalid msg object')
}
return binding.dcn_add_device_msg(this.dcn_context, label, msg.dc_msg)
}
setChatVisibility(
chatId: number,
visibility:
| C.DC_CHAT_VISIBILITY_NORMAL
| C.DC_CHAT_VISIBILITY_ARCHIVED
| C.DC_CHAT_VISIBILITY_PINNED
) {
debug(`setChatVisibility ${chatId} ${visibility}`)
binding.dcn_set_chat_visibility(
this.dcn_context,
Number(chatId),
visibility
)
}
blockContact(contactId: number, block: boolean) {
debug(`blockContact ${contactId} ${block}`)
binding.dcn_block_contact(
this.dcn_context,
Number(contactId),
block ? 1 : 0
)
}
checkQrCode(qrCode: string) {
debug(`checkQrCode ${qrCode}`)
const dc_lot = binding.dcn_check_qr(this.dcn_context, qrCode)
let result = dc_lot ? new Lot(dc_lot) : null
if (result) {
return { id: result.getId(), ...result.toJson() }
}
return result
}
configure(opts: any): Promise<void> {
return new Promise((resolve, reject) => {
debug('configure')
const onSuccess = () => {
removeListeners()
resolve()
}
const onFail = (error: string) => {
removeListeners()
reject(new Error(error))
}
let onConfigure: (...args: any[]) => void
if (this.account_id === null) {
onConfigure = (data1: number, data2: string) => {
if (data1 === 0) return onFail(data2)
else if (data1 === 1000) return onSuccess()
}
} else {
onConfigure = (accountId: number, data1: number, data2: string) => {
if (this.account_id !== accountId) {
return
}
if (data1 === 0) return onFail(data2)
else if (data1 === 1000) return onSuccess()
}
}
const removeListeners = () => {
;(this.manager || this).removeListener(
'DC_EVENT_CONFIGURE_PROGRESS',
onConfigure
)
}
const registerListeners = () => {
;(this.manager || this).on('DC_EVENT_CONFIGURE_PROGRESS', onConfigure)
}
registerListeners()
if (!opts) opts = {}
Object.keys(opts).forEach((key) => {
const value = opts[key]
this.setConfig(key, value)
})
binding.dcn_configure(this.dcn_context)
})
}
continueKeyTransfer(messageId: number, setupCode: string) {
debug(`continueKeyTransfer ${messageId}`)
return new Promise((resolve, reject) => {
binding.dcn_continue_key_transfer(
this.dcn_context,
Number(messageId),
setupCode,
(result: number) => resolve(result === 1)
)
})
}
/** @returns chatId */
createBroadcastList(): number {
debug(`createBroadcastList`)
return binding.dcn_create_broadcast_list(this.dcn_context)
}
/** @returns chatId */
createChatByContactId(contactId: number): number {
debug(`createChatByContactId ${contactId}`)
return binding.dcn_create_chat_by_contact_id(
this.dcn_context,
Number(contactId)
)
}
/** @returns contactId */
createContact(name: string, addr: string): number {
debug(`createContact ${name} ${addr}`)
return binding.dcn_create_contact(this.dcn_context, name, addr)
}
/**
*
* @param chatName The name of the chat that should be created
* @param is_protected Whether the chat should be protected at creation time
* @returns chatId
*/
createGroupChat(chatName: string, is_protected: boolean = false): number {
debug(`createGroupChat ${chatName} [protected:${is_protected}]`)
return binding.dcn_create_group_chat(
this.dcn_context,
is_protected ? 1 : 0,
chatName
)
}
deleteChat(chatId: number) {
debug(`deleteChat ${chatId}`)
binding.dcn_delete_chat(this.dcn_context, Number(chatId))
}
deleteContact(contactId: number) {
debug(`deleteContact ${contactId}`)
return Boolean(
binding.dcn_delete_contact(this.dcn_context, Number(contactId))
)
}
deleteMessages(messageIds: number[]) {
if (!Array.isArray(messageIds)) {
messageIds = [messageIds]
}
messageIds = messageIds.map((id) => Number(id))
debug('deleteMessages', messageIds)
binding.dcn_delete_msgs(this.dcn_context, messageIds)
}
forwardMessages(messageIds: number[], chatId: number) {
if (!Array.isArray(messageIds)) {
messageIds = [messageIds]
}
messageIds = messageIds.map((id) => Number(id))
debug('forwardMessages', messageIds)
binding.dcn_forward_msgs(this.dcn_context, messageIds, chatId)
}
getBlobdir(): string {
debug('getBlobdir')
return binding.dcn_get_blobdir(this.dcn_context)
}
getBlockedCount(): number {
debug('getBlockedCount')
return binding.dcn_get_blocked_cnt(this.dcn_context)
}
getBlockedContacts(): number[] {
debug('getBlockedContacts')
return binding.dcn_get_blocked_contacts(this.dcn_context)
}
getChat(chatId: number) {
debug(`getChat ${chatId}`)
const dc_chat = binding.dcn_get_chat(this.dcn_context, Number(chatId))
return dc_chat ? new Chat(dc_chat) : null
}
getChatContacts(chatId: number): number[] {
debug(`getChatContacts ${chatId}`)
return binding.dcn_get_chat_contacts(this.dcn_context, Number(chatId))
}
getChatIdByContactId(contactId: number): number {
debug(`getChatIdByContactId ${contactId}`)
return binding.dcn_get_chat_id_by_contact_id(
this.dcn_context,
Number(contactId)
)
}
getChatMedia(
chatId: number,
msgType1: number,
msgType2: number,
msgType3: number
): number[] {
debug(`getChatMedia ${chatId}`)
return binding.dcn_get_chat_media(
this.dcn_context,
Number(chatId),
msgType1,
msgType2 || 0,
msgType3 || 0
)
}
getMimeHeaders(messageId: number): string {
debug(`getMimeHeaders ${messageId}`)
return binding.dcn_get_mime_headers(this.dcn_context, Number(messageId))
}
getChatlistItemSummary(chatId: number, messageId: number) {
debug(`getChatlistItemSummary ${chatId} ${messageId}`)
return new Lot(
binding.dcn_chatlist_get_summary2(this.dcn_context, chatId, messageId)
)
}
getChatMessages(chatId: number, flags: number, marker1before: number) {
debug(`getChatMessages ${chatId} ${flags} ${marker1before}`)
return binding.dcn_get_chat_msgs(
this.dcn_context,
Number(chatId),
flags,
marker1before
)
}
/**
* Get encryption info for a chat.
* Get a multi-line encryption info, containing encryption preferences of all members.
* Can be used to find out why messages sent to group are not encrypted.
*
* @param chatId ID of the chat to get the encryption info for.
* @return Multi-line text, must be released using dc_str_unref() after usage.
*/
getChatEncrytionInfo(chatId: number): string {
return binding.dcn_get_chat_encrinfo(this.dcn_context, chatId)
}
getChats(listFlags: number, queryStr: string, queryContactId: number) {
debug('getChats')
const result = []
const list = this.getChatList(listFlags, queryStr, queryContactId)
const count = list.getCount()
for (let i = 0; i < count; i++) {
result.push(list.getChatId(i))
}
return result
}
getChatList(listFlags: number, queryStr: string, queryContactId: number) {
listFlags = listFlags || 0
queryStr = queryStr || ''
queryContactId = queryContactId || 0
debug(`getChatList ${listFlags} ${queryStr} ${queryContactId}`)
return new ChatList(
binding.dcn_get_chatlist(
this.dcn_context,
listFlags,
queryStr,
Number(queryContactId)
)
)
}
getConfig(key: string): string {
debug(`getConfig ${key}`)
return binding.dcn_get_config(this.dcn_context, key)
}
getContact(contactId: number) {
debug(`getContact ${contactId}`)
const dc_contact = binding.dcn_get_contact(
this.dcn_context,
Number(contactId)
)
return dc_contact ? new Contact(dc_contact) : null
}
getContactEncryptionInfo(contactId: number) {
debug(`getContactEncryptionInfo ${contactId}`)
return binding.dcn_get_contact_encrinfo(this.dcn_context, Number(contactId))
}
getContacts(listFlags: number, query: string) {
listFlags = listFlags || 0
query = query || ''
debug(`getContacts ${listFlags} ${query}`)
return binding.dcn_get_contacts(this.dcn_context, listFlags, query)
}
wasDeviceMessageEverAdded(label: string) {
debug(`wasDeviceMessageEverAdded ${label}`)
const added = binding.dcn_was_device_msg_ever_added(this.dcn_context, label)
return added === 1
}
getDraft(chatId: number) {
debug(`getDraft ${chatId}`)
const dc_msg = binding.dcn_get_draft(this.dcn_context, Number(chatId))
return dc_msg ? new Message(dc_msg) : null
}
getFreshMessageCount(chatId: number): number {
debug(`getFreshMessageCount ${chatId}`)
return binding.dcn_get_fresh_msg_cnt(this.dcn_context, Number(chatId))
}
getFreshMessages() {
debug('getFreshMessages')
return binding.dcn_get_fresh_msgs(this.dcn_context)
}
getInfo() {
debug('getInfo')
const info = binding.dcn_get_info(this.dcn_context)
return AccountManager.parseGetInfo(info)
}
getMessage(messageId: number) {
debug(`getMessage ${messageId}`)
const dc_msg = binding.dcn_get_msg(this.dcn_context, Number(messageId))
return dc_msg ? new Message(dc_msg) : null
}
getMessageCount(chatId: number): number {
debug(`getMessageCount ${chatId}`)
return binding.dcn_get_msg_cnt(this.dcn_context, Number(chatId))
}
getMessageInfo(messageId: number): string {
debug(`getMessageInfo ${messageId}`)
return binding.dcn_get_msg_info(this.dcn_context, Number(messageId))
}
getMessageHTML(messageId: number): string {
debug(`getMessageHTML ${messageId}`)
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
}
getNextMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
}
getPreviousMediaMessage(
messageId: number,
msgType1: number,
msgType2: number,
msgType3: number
) {
debug(
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
)
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
}
_getNextMedia(
messageId: number,
dir: number,
msgType1: number,
msgType2: number,
msgType3: number
): number {
return binding.dcn_get_next_media(
this.dcn_context,
Number(messageId),
dir,
msgType1 || 0,
msgType2 || 0,
msgType3 || 0
)
}
getSecurejoinQrCode(chatId: number): string {
debug(`getSecurejoinQrCode ${chatId}`)
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))
}
getSecurejoinQrCodeSVG(chatId: number): string {
debug(`getSecurejoinQrCodeSVG ${chatId}`)
return binding.dcn_get_securejoin_qr_svg(this.dcn_context, chatId)
}
startIO(): void {
debug(`startIO`)
binding.dcn_start_io(this.dcn_context)
}
stopIO(): void {
debug(`stopIO`)
binding.dcn_stop_io(this.dcn_context)
}
stopOngoingProcess(): void {
debug(`stopOngoingProcess`)
binding.dcn_stop_ongoing_process(this.dcn_context)
}
/**
*
* @deprecated please use `AccountManager.getSystemInfo()` instead
*/
static getSystemInfo() {
return AccountManager.getSystemInfo()
}
getConnectivity(): number {
return binding.dcn_get_connectivity(this.dcn_context)
}
getConnectivityHTML(): String {
return binding.dcn_get_connectivity_html(this.dcn_context)
}
importExport(what: number, param1: string, param2 = '') {
debug(`importExport ${what} ${param1} ${param2}`)
binding.dcn_imex(this.dcn_context, what, param1, param2)
}
importExportHasBackup(dir: string) {
debug(`importExportHasBackup ${dir}`)
return binding.dcn_imex_has_backup(this.dcn_context, dir)
}
initiateKeyTransfer(): Promise<string> {
return new Promise((resolve, reject) => {
debug('initiateKeyTransfer2')
binding.dcn_initiate_key_transfer(this.dcn_context, resolve)
})
}
isConfigured() {
debug('isConfigured')
return Boolean(binding.dcn_is_configured(this.dcn_context))
}
isContactInChat(chatId: number, contactId: number) {
debug(`isContactInChat ${chatId} ${contactId}`)
return Boolean(
binding.dcn_is_contact_in_chat(
this.dcn_context,
Number(chatId),
Number(contactId)
)
)
}
/**
*
* @returns resulting chat id or 0 on error
*/
joinSecurejoin(qrCode: string): number {
debug(`joinSecurejoin ${qrCode}`)
return binding.dcn_join_securejoin(this.dcn_context, qrCode)
}
lookupContactIdByAddr(addr: string): number {
debug(`lookupContactIdByAddr ${addr}`)
return binding.dcn_lookup_contact_id_by_addr(this.dcn_context, addr)
}
markNoticedChat(chatId: number) {
debug(`markNoticedChat ${chatId}`)
binding.dcn_marknoticed_chat(this.dcn_context, Number(chatId))
}
markSeenMessages(messageIds: number[]) {
if (!Array.isArray(messageIds)) {
messageIds = [messageIds]
}
messageIds = messageIds.map((id) => Number(id))
debug('markSeenMessages', messageIds)
binding.dcn_markseen_msgs(this.dcn_context, messageIds)
}
maybeNetwork() {
debug('maybeNetwork')
binding.dcn_maybe_network(this.dcn_context)
}
messageNew(viewType = C.DC_MSG_TEXT) {
debug(`messageNew ${viewType}`)
return new Message(binding.dcn_msg_new(this.dcn_context, viewType))
}
removeContactFromChat(chatId: number, contactId: number) {
debug(`removeContactFromChat ${chatId} ${contactId}`)
return Boolean(
binding.dcn_remove_contact_from_chat(
this.dcn_context,
Number(chatId),
Number(contactId)
)
)
}
/**
*
* @param chatId ID of the chat to search messages in. Set this to 0 for a global search.
* @param query The query to search for.
*/
searchMessages(chatId: number, query: string): number[] {
debug(`searchMessages ${chatId} ${query}`)
return binding.dcn_search_msgs(this.dcn_context, Number(chatId), query)
}
sendMessage(chatId: number, msg: string | Message) {
debug(`sendMessage ${chatId}`)
if (!msg) {
throw new Error('invalid msg parameter')
}
if (typeof msg === 'string') {
const msgObj = this.messageNew()
msgObj.setText(msg)
msg = msgObj
}
if (!msg.dc_msg) {
throw new Error('invalid msg object')
}
return binding.dcn_send_msg(this.dcn_context, Number(chatId), msg.dc_msg)
}
downloadFullMessage(messageId: number) {
binding.dcn_download_full_msg(this.dcn_context, messageId)
}
/**
*
* @returns {Promise<number>} Promise that resolves into the resulting message id
*/
sendVideochatInvitation(chatId: number): Promise<number> {
debug(`sendVideochatInvitation ${chatId}`)
return new Promise((resolve, reject) => {
binding.dcn_send_videochat_invitation(
this.dcn_context,
chatId,
(result: number) => {
if (result !== 0) {
resolve(result)
} else {
reject(
'Videochatinvitation failed to send, see error events for detailed info'
)
}
}
)
})
}
setChatName(chatId: number, name: string) {
debug(`setChatName ${chatId} ${name}`)
return Boolean(
binding.dcn_set_chat_name(this.dcn_context, Number(chatId), name)
)
}
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(
this.dcn_context,
Number(chatId)
)
}
setChatEphemeralTimer(chatId: number, timer: number) {
debug(`setChatEphemeralTimer ${chatId} ${timer}`)
return Boolean(
binding.dcn_set_chat_ephemeral_timer(
this.dcn_context,
Number(chatId),
Number(timer)
)
)
}
setChatProfileImage(chatId: number, image: string) {
debug(`setChatProfileImage ${chatId} ${image}`)
return Boolean(
binding.dcn_set_chat_profile_image(
this.dcn_context,
Number(chatId),
image || ''
)
)
}
setConfig(key: string, value: string | boolean | number): number {
debug(`setConfig (string) ${key} ${value}`)
if (value === null) {
return binding.dcn_set_config_null(this.dcn_context, key)
} else {
if (typeof value === 'boolean') {
value = value === true ? '1' : '0'
} else if (typeof value === 'number') {
value = String(value)
}
return binding.dcn_set_config(this.dcn_context, key, value)
}
}
setConfigFromQr(qrcodeContent: string): boolean {
return Boolean(
binding.dcn_set_config_from_qr(this.dcn_context, qrcodeContent)
)
}
estimateDeletionCount(fromServer: boolean, seconds: number): number {
debug(`estimateDeletionCount fromServer: ${fromServer} seconds: ${seconds}`)
return binding.dcn_estimate_deletion_cnt(
this.dcn_context,
fromServer === true ? 1 : 0,
seconds
)
}
setStockTranslation(stockId: number, stockMsg: string) {
debug(`setStockTranslation ${stockId} ${stockMsg}`)
return Boolean(
binding.dcn_set_stock_translation(
this.dcn_context,
Number(stockId),
stockMsg
)
)
}
setDraft(chatId: number, msg: Message | null) {
debug(`setDraft ${chatId}`)
binding.dcn_set_draft(
this.dcn_context,
Number(chatId),
msg ? msg.dc_msg : null
)
}
setLocation(latitude: number, longitude: number, accuracy: number) {
debug(`setLocation ${latitude}`)
binding.dcn_set_location(
this.dcn_context,
Number(latitude),
Number(longitude),
Number(accuracy)
)
}
/*
* @param chatId Chat-id to get location information for.
* 0 to get locations independently of the chat.
* @param contactId Contact id to get location information for.
* If also a chat-id is given, this should be a member of the given chat.
* 0 to get locations independently of the contact.
* @param timestampFrom Start of timespan to return.
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
* 0 for "start from the beginning".
* @param timestampTo End of timespan to return.
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
* 0 for "all up to now".
* @return Array of locations, NULL is never returned.
* The array is sorted descending;
* the first entry in the array is the location with the newest timestamp.
*
* Examples:
* // get locations from the last hour for a global map
* getLocations(0, 0, time(NULL)-60*60, 0);
*
* // get locations from a contact for a global map
* getLocations(0, contact_id, 0, 0);
*
* // get all locations known for a given chat
* getLocations(chat_id, 0, 0, 0);
*
* // get locations from a single contact for a given chat
* getLocations(chat_id, contact_id, 0, 0);
*/
getLocations(
chatId: number,
contactId: number,
timestampFrom = 0,
timestampTo = 0
) {
const locations = new Locations(
binding.dcn_get_locations(
this.dcn_context,
Number(chatId),
Number(contactId),
timestampFrom,
timestampTo
)
)
return locations.toJson()
}
/**
*
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
*/
setChatMuteDuration(chatId: number, duration: number) {
return Boolean(
binding.dcn_set_chat_mute_duration(this.dcn_context, chatId, duration)
)
}
/** get information about the provider */
getProviderFromEmail(email: string) {
debug('DeltaChat.getProviderFromEmail')
const provider = binding.dcn_provider_new_from_email(
this.dcn_context,
email
)
if (!provider) {
return undefined
}
return {
before_login_hint: binding.dcn_provider_get_before_login_hint(provider),
overview_page: binding.dcn_provider_get_overview_page(provider),
status: binding.dcn_provider_get_status(provider),
}
}
sendWebxdcStatusUpdate<T>(
msgId: number,
json: WebxdcSendingStatusUpdate<T>,
descr: string
) {
return Boolean(
binding.dcn_send_webxdc_status_update(
this.dcn_context,
msgId,
JSON.stringify(json),
descr
)
)
}
getWebxdcStatusUpdates<T>(
msgId: number,
serial = 0
): WebxdcReceivedStatusUpdate<T>[] {
return JSON.parse(
binding.dcn_get_webxdc_status_updates(this.dcn_context, msgId, serial)
)
}
/** the string contains the binary data, it is an "u8 string", maybe we will use a more efficient type in the future. */
getWebxdcBlob(message: Message, filename: string): Buffer | null {
return binding.dcn_msg_get_webxdc_blob(message.dc_msg, filename)
}
}
export type WebxdcInfo = {
name: string
icon: string
summary: string
/**
* if set by the webxdc, name of the document in edit
*/
document?: string
}
type WebxdcSendingStatusUpdate<T> = {
/** the payload, deserialized json:
* any javascript primitive, array or object. */
payload: T
/** optional, short, informational message that will be added to the chat,
* eg. "Alice voted" or "Bob scored 123 in MyGame";
* usually only one line of text is shown,
* use this option sparingly to not spam the chat. */
info?: string
/** optional, short text, shown beside app icon;
* it is recommended to use some aggregated value,
* eg. "8 votes", "Highscore: 123" */
summary?: string
/**
* optional, name of the document in edit,
* must not be used eg. in games where the Webxdc does not create documents
*/
document?: string
}
type WebxdcReceivedStatusUpdate<T> = {
/** the payload, deserialized json */
payload: T
/** the serial number of this update. Serials are larger `0` and newer serials have higher numbers. */
serial: number
/** the maximum serial currently known.
* If `max_serial` equals `serial` this update is the last update (until new network messages arrive). */
max_serial: number
/** optional, short, informational message. */
info?: string
/** optional, short text, shown beside app icon. If there are no updates, an empty JSON-array is returned. */
summary?: string
}