diff --git a/CHANGELOG.md b/CHANGELOG.md index 43239d899..713443809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ ### Changes +- jsonrpc in cffi also sends events now #3662 +- jsonrpc: new format for events and better typescript autocompletion ### Fixes - share stock string translations across accounts created by the same account manager #3640 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index a0c007bd5..a8c7f20f3 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -4452,11 +4452,13 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter( mod jsonrpc { use super::*; use deltachat_jsonrpc::api::CommandApi; + use deltachat_jsonrpc::events::event_to_json_rpc_notification; use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession}; pub struct dc_jsonrpc_instance_t { receiver: OutReceiver, handle: RpcSession, + event_thread: JoinHandle>, } #[no_mangle] @@ -4472,9 +4474,36 @@ mod jsonrpc { deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone()); let (request_handle, receiver) = RpcClient::new(); + let request_handle2 = request_handle.clone(); let handle = RpcSession::new(request_handle, cmd_api); - let instance = dc_jsonrpc_instance_t { receiver, handle }; + let events = block_on({ + async { + let am = (*account_manager).inner.clone(); + let ev = am.read().await.get_event_emitter(); + drop(am); + ev + } + }); + + let event_thread = spawn({ + async move { + while let Some(event) = events.recv().await { + let event = event_to_json_rpc_notification(event); + request_handle2 + .send_notification("event", Some(event)) + .await?; + } + let res: Result<(), anyhow::Error> = Ok(()); + res + } + }); + + let instance = dc_jsonrpc_instance_t { + receiver, + handle, + event_thread, + }; Box::into_raw(Box::new(instance)) } @@ -4485,7 +4514,7 @@ mod jsonrpc { eprintln!("ignoring careless call to dc_jsonrpc_unref()"); return; } - + (*jsonrpc_instance).event_thread.abort(); Box::from_raw(jsonrpc_instance); } diff --git a/deltachat-jsonrpc/src/api/events.rs b/deltachat-jsonrpc/src/api/events.rs index 61e1162c9..89786697b 100644 --- a/deltachat-jsonrpc/src/api/events.rs +++ b/deltachat-jsonrpc/src/api/events.rs @@ -4,142 +4,353 @@ use serde_json::{json, Value}; use typescript_type_def::TypeDef; pub fn event_to_json_rpc_notification(event: Event) -> Value { - let (field1, field2): (Value, Value) = match &event.typ { - // events with a single string in field1 - EventType::Info(txt) - | EventType::SmtpConnected(txt) - | EventType::ImapConnected(txt) - | EventType::SmtpMessageSent(txt) - | EventType::ImapMessageDeleted(txt) - | EventType::ImapMessageMoved(txt) - | EventType::NewBlobFile(txt) - | EventType::DeletedBlobFile(txt) - | EventType::Warning(txt) - | EventType::Error(txt) - | EventType::ErrorSelfNotInGroup(txt) => (json!(txt), Value::Null), - EventType::ImexFileWritten(path) => (json!(path.to_str()), Value::Null), - // single number - EventType::MsgsNoticed(chat_id) | EventType::ChatModified(chat_id) => { - (json!(chat_id), Value::Null) - } - EventType::ImexProgress(progress) => (json!(progress), Value::Null), - // both fields contain numbers - EventType::MsgsChanged { chat_id, msg_id } - | EventType::IncomingMsg { chat_id, msg_id } - | EventType::MsgDelivered { chat_id, msg_id } - | EventType::MsgFailed { chat_id, msg_id } - | EventType::MsgRead { chat_id, msg_id } => (json!(chat_id), json!(msg_id)), - EventType::ChatEphemeralTimerModified { chat_id, timer } => (json!(chat_id), json!(timer)), - EventType::SecurejoinInviterProgress { - contact_id, - progress, - } - | EventType::SecurejoinJoinerProgress { - contact_id, - progress, - } => (json!(contact_id), json!(progress)), - // field 1 number or null - EventType::ContactsChanged(maybe_number) | EventType::LocationChanged(maybe_number) => ( - match maybe_number { - Some(number) => json!(number), - None => Value::Null, - }, - Value::Null, - ), - // number and maybe string - EventType::ConfigureProgress { progress, comment } => ( - json!(progress), - match comment { - Some(content) => json!(content), - None => Value::Null, - }, - ), - EventType::ConnectivityChanged => (Value::Null, Value::Null), - EventType::SelfavatarChanged => (Value::Null, Value::Null), - EventType::WebxdcStatusUpdate { - msg_id, - status_update_serial, - } => (json!(msg_id), json!(status_update_serial)), - EventType::WebxdcInstanceDeleted { msg_id } => (json!(msg_id), Value::Null), - }; - - let id: EventTypeName = event.typ.into(); + let id: JSONRPCEventType = event.typ.into(); json!({ - "id": id, + "event": id, "contextId": event.id, - "field1": field1, - "field2": field2 }) } #[derive(Serialize, TypeDef)] -pub enum EventTypeName { - Info, - SmtpConnected, - ImapConnected, - SmtpMessageSent, - ImapMessageDeleted, - ImapMessageMoved, - NewBlobFile, - DeletedBlobFile, - Warning, - Error, - ErrorSelfNotInGroup, - MsgsChanged, - IncomingMsg, - MsgsNoticed, - MsgDelivered, - MsgFailed, - MsgRead, - ChatModified, - ChatEphemeralTimerModified, - ContactsChanged, - LocationChanged, - ConfigureProgress, - ImexProgress, - ImexFileWritten, - SecurejoinInviterProgress, - SecurejoinJoinerProgress, +#[serde(tag = "type", rename = "Event")] +pub enum JSONRPCEventType { + /// The library-user may write an informational string to the log. + /// + /// This event should *not* be reported to the end-user using a popup or something like + /// that. + Info { + msg: String, + }, + + /// Emitted when SMTP connection is established and login was successful. + SmtpConnected { + msg: String, + }, + + /// Emitted when IMAP connection is established and login was successful. + ImapConnected { + msg: String, + }, + + /// Emitted when a message was successfully sent to the SMTP server. + SmtpMessageSent { + msg: String, + }, + + /// Emitted when an IMAP message has been marked as deleted + ImapMessageDeleted { + msg: String, + }, + + /// Emitted when an IMAP message has been moved + ImapMessageMoved { + msg: String, + }, + + /// Emitted when an new file in the $BLOBDIR was created + NewBlobFile { + file: String, + }, + + /// Emitted when an file in the $BLOBDIR was deleted + DeletedBlobFile { + file: String, + }, + + /// The library-user should write a warning string to the log. + /// + /// This event should *not* be reported to the end-user using a popup or something like + /// that. + Warning { + msg: String, + }, + + /// The library-user should report an error to the end-user. + /// + /// As most things are asynchronous, things may go wrong at any time and the user + /// should not be disturbed by a dialog or so. Instead, use a bubble or so. + /// + /// However, for ongoing processes (eg. configure()) + /// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer()) + /// it might be better to delay showing these events until the function has really + /// failed (returned false). It should be sufficient to report only the *last* error + /// in a messasge box then. + Error { + msg: String, + }, + + /// An action cannot be performed because the user is not in the group. + /// Reported eg. after a call to + /// setChatName(), setChatProfileImage(), + /// addContactToChat(), removeContactFromChat(), + /// and messages sending functions. + ErrorSelfNotInGroup { + msg: String, + }, + + /// Messages or chats changed. One or more messages or chats changed for various + /// reasons in the database: + /// - Messages sent, received or removed + /// - Chats created, deleted or archived + /// - A draft has been set + /// + /// `chatId` is set if only a single chat is affected by the changes, otherwise 0. + /// `msgId` is set if only a single message is affected by the changes, otherwise 0. + #[serde(rename_all = "camelCase")] + MsgsChanged { + chat_id: u32, + msg_id: u32, + }, + + /// There is a fresh message. Typically, the user will show an notification + /// when receiving this message. + /// + /// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event. + #[serde(rename_all = "camelCase")] + IncomingMsg { + chat_id: u32, + msg_id: u32, + }, + + /// Messages were seen or noticed. + /// chat id is always set. + #[serde(rename_all = "camelCase")] + MsgsNoticed { + chat_id: u32, + }, + + /// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to + /// DC_STATE_OUT_DELIVERED, see `Message.state`. + #[serde(rename_all = "camelCase")] + MsgDelivered { + chat_id: u32, + msg_id: u32, + }, + + /// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to + /// DC_STATE_OUT_FAILED, see `Message.state`. + #[serde(rename_all = "camelCase")] + MsgFailed { + chat_id: u32, + msg_id: u32, + }, + + /// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to + /// DC_STATE_OUT_MDN_RCVD, see `Message.state`. + #[serde(rename_all = "camelCase")] + MsgRead { + chat_id: u32, + msg_id: u32, + }, + + /// Chat changed. The name or the image of a chat group was changed or members were added or removed. + /// Or the verify state of a chat has changed. + /// See setChatName(), setChatProfileImage(), addContactToChat() + /// and removeContactFromChat(). + /// + /// This event does not include ephemeral timer modification, which + /// is a separate event. + #[serde(rename_all = "camelCase")] + ChatModified { + chat_id: u32, + }, + + /// Chat ephemeral timer changed. + #[serde(rename_all = "camelCase")] + ChatEphemeralTimerModified { + chat_id: u32, + timer: u32, + }, + + /// Contact(s) created, renamed, blocked or deleted. + /// + /// @param data1 (int) If set, this is the contact_id of an added contact that should be selected. + #[serde(rename_all = "camelCase")] + ContactsChanged { + contact_id: Option, + }, + + /// Location of one or more contact has changed. + /// + /// @param data1 (u32) contact_id of the contact for which the location has changed. + /// If the locations of several contacts have been changed, + /// this parameter is set to `None`. + #[serde(rename_all = "camelCase")] + LocationChanged { + contact_id: Option, + }, + + /// Inform about the configuration progress started by configure(). + ConfigureProgress { + /// Progress. + /// + /// 0=error, 1-999=progress in permille, 1000=success and done + progress: usize, + + /// Progress comment or error, something to display to the user. + comment: Option, + }, + + /// Inform about the import/export progress started by imex(). + /// + /// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done + /// @param data2 0 + #[serde(rename_all = "camelCase")] + ImexProgress { + progress: usize, + }, + + /// A file has been exported. A file has been written by imex(). + /// This event may be sent multiple times by a single call to imex(). + /// + /// A typical purpose for a handler of this event may be to make the file public to some system + /// services. + /// + /// @param data2 0 + #[serde(rename_all = "camelCase")] + ImexFileWritten { + path: String, + }, + + /// Progress information of a secure-join handshake from the view of the inviter + /// (Alice, the person who shows the QR code). + /// + /// These events are typically sent after a joiner has scanned the QR code + /// generated by getChatSecurejoinQrCodeSvg(). + /// + /// @param data1 (int) ID of the contact that wants to join. + /// @param data2 (int) Progress as: + /// 300=vg-/vc-request received, typically shown as "bob@addr joins". + /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". + /// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. + /// 1000=Protocol finished for this contact. + #[serde(rename_all = "camelCase")] + SecurejoinInviterProgress { + contact_id: u32, + progress: usize, + }, + + /// Progress information of a secure-join handshake from the view of the joiner + /// (Bob, the person who scans the QR code). + /// The events are typically sent while secureJoin(), which + /// may take some time, is executed. + /// @param data1 (int) ID of the inviting contact. + /// @param data2 (int) Progress as: + /// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." + /// (Bob has verified alice and waits until Alice does the same for him) + #[serde(rename_all = "camelCase")] + SecurejoinJoinerProgress { + contact_id: u32, + progress: usize, + }, + + /// The connectivity to the server changed. + /// This means that you should refresh the connectivity view + /// and possibly the connectivtiy HTML; see getConnectivity() and + /// getConnectivityHtml() for details. ConnectivityChanged, + SelfavatarChanged, - WebxdcStatusUpdate, - WebXdInstanceDeleted, + + #[serde(rename_all = "camelCase")] + WebxdcStatusUpdate { + msg_id: u32, + status_update_serial: u32, + }, + + /// Inform that a message containing a webxdc instance has been deleted + #[serde(rename_all = "camelCase")] + WebxdcInstanceDeleted { + msg_id: u32, + }, } -impl From for EventTypeName { +impl From for JSONRPCEventType { fn from(event: EventType) -> Self { - use EventTypeName::*; + use JSONRPCEventType::*; match event { - EventType::Info(_) => Info, - EventType::SmtpConnected(_) => SmtpConnected, - EventType::ImapConnected(_) => ImapConnected, - EventType::SmtpMessageSent(_) => SmtpMessageSent, - EventType::ImapMessageDeleted(_) => ImapMessageDeleted, - EventType::ImapMessageMoved(_) => ImapMessageMoved, - EventType::NewBlobFile(_) => NewBlobFile, - EventType::DeletedBlobFile(_) => DeletedBlobFile, - EventType::Warning(_) => Warning, - EventType::Error(_) => Error, - EventType::ErrorSelfNotInGroup(_) => ErrorSelfNotInGroup, - EventType::MsgsChanged { .. } => MsgsChanged, - EventType::IncomingMsg { .. } => IncomingMsg, - EventType::MsgsNoticed(_) => MsgsNoticed, - EventType::MsgDelivered { .. } => MsgDelivered, - EventType::MsgFailed { .. } => MsgFailed, - EventType::MsgRead { .. } => MsgRead, - EventType::ChatModified(_) => ChatModified, - EventType::ChatEphemeralTimerModified { .. } => ChatEphemeralTimerModified, - EventType::ContactsChanged(_) => ContactsChanged, - EventType::LocationChanged(_) => LocationChanged, - EventType::ConfigureProgress { .. } => ConfigureProgress, - EventType::ImexProgress(_) => ImexProgress, - EventType::ImexFileWritten(_) => ImexFileWritten, - EventType::SecurejoinInviterProgress { .. } => SecurejoinInviterProgress, - EventType::SecurejoinJoinerProgress { .. } => SecurejoinJoinerProgress, + EventType::Info(msg) => Info { msg }, + EventType::SmtpConnected(msg) => SmtpConnected { msg }, + EventType::ImapConnected(msg) => ImapConnected { msg }, + EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg }, + EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg }, + EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg }, + EventType::NewBlobFile(file) => NewBlobFile { file }, + EventType::DeletedBlobFile(file) => DeletedBlobFile { file }, + EventType::Warning(msg) => Warning { msg }, + EventType::Error(msg) => Error { msg }, + EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg }, + EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged { + chat_id: chat_id.to_u32(), + msg_id: msg_id.to_u32(), + }, + EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg { + chat_id: chat_id.to_u32(), + msg_id: msg_id.to_u32(), + }, + EventType::MsgsNoticed(chat_id) => MsgsNoticed { + chat_id: chat_id.to_u32(), + }, + EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered { + chat_id: chat_id.to_u32(), + msg_id: msg_id.to_u32(), + }, + EventType::MsgFailed { chat_id, msg_id } => MsgFailed { + chat_id: chat_id.to_u32(), + msg_id: msg_id.to_u32(), + }, + EventType::MsgRead { chat_id, msg_id } => MsgRead { + chat_id: chat_id.to_u32(), + msg_id: msg_id.to_u32(), + }, + EventType::ChatModified(chat_id) => ChatModified { + chat_id: chat_id.to_u32(), + }, + EventType::ChatEphemeralTimerModified { chat_id, timer } => { + ChatEphemeralTimerModified { + chat_id: chat_id.to_u32(), + timer: timer.to_u32(), + } + } + EventType::ContactsChanged(contact) => ContactsChanged { + contact_id: contact.map(|c| c.to_u32()), + }, + EventType::LocationChanged(contact) => LocationChanged { + contact_id: contact.map(|c| c.to_u32()), + }, + EventType::ConfigureProgress { progress, comment } => { + ConfigureProgress { progress, comment } + } + EventType::ImexProgress(progress) => ImexProgress { progress }, + EventType::ImexFileWritten(path) => ImexFileWritten { + path: path.to_str().unwrap_or_default().to_owned(), + }, + EventType::SecurejoinInviterProgress { + contact_id, + progress, + } => SecurejoinInviterProgress { + contact_id: contact_id.to_u32(), + progress, + }, + EventType::SecurejoinJoinerProgress { + contact_id, + progress, + } => SecurejoinJoinerProgress { + contact_id: contact_id.to_u32(), + progress, + }, EventType::ConnectivityChanged => ConnectivityChanged, EventType::SelfavatarChanged => SelfavatarChanged, - EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate, - EventType::WebxdcInstanceDeleted { .. } => WebXdInstanceDeleted, + EventType::WebxdcStatusUpdate { + msg_id, + status_update_serial, + } => WebxdcStatusUpdate { + msg_id: msg_id.to_u32(), + status_update_serial: status_update_serial.to_u32(), + }, + EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted { + msg_id: msg_id.to_u32(), + }, } } } @@ -153,7 +364,8 @@ fn generate_events_ts_types_definition() { root_namespace: None, ..typescript_type_def::DefinitionFileOptions::default() }; - typescript_type_def::write_definition_file::<_, EventTypeName>(&mut buf, options).unwrap(); + typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options) + .unwrap(); String::from_utf8(buf).unwrap() }; std::fs::write("typescript/generated/events.ts", events).unwrap(); diff --git a/deltachat-jsonrpc/typescript/example/example.ts b/deltachat-jsonrpc/typescript/example/example.ts index 951703fde..14dfc9881 100644 --- a/deltachat-jsonrpc/typescript/example/example.ts +++ b/deltachat-jsonrpc/typescript/example/example.ts @@ -1,4 +1,4 @@ -import { DeltaChat, DeltaChatEvent } from "../deltachat.js"; +import { DcEvent, DeltaChat } from "../deltachat.js"; var SELECTED_ACCOUNT = 0; @@ -7,7 +7,7 @@ window.addEventListener("DOMContentLoaded", (_event) => { SELECTED_ACCOUNT = Number(id); window.dispatchEvent(new Event("account-changed")); }; - console.log('launch run script...') + console.log("launch run script..."); run().catch((err) => console.error("run failed", err)); }); @@ -16,13 +16,13 @@ async function run() { const $side = document.getElementById("side")!; const $head = document.getElementById("header")!; - const client = new DeltaChat('ws://localhost:20808/ws') + const client = new DeltaChat("ws://localhost:20808/ws"); - ;(window as any).client = client.rpc; + (window as any).client = client.rpc; - client.on("ALL", event => { - onIncomingEvent(event) - }) + client.on("ALL", (accountId, event) => { + onIncomingEvent(accountId, event); + }); window.addEventListener("account-changed", async (_event: Event) => { listChatsForSelectedAccount(); @@ -31,9 +31,9 @@ async function run() { await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]); async function loadAccountsInHeader() { - console.log('load accounts') + console.log("load accounts"); const accounts = await client.rpc.getAllAccounts(); - console.log('accounts loaded', accounts) + console.log("accounts loaded", accounts); for (const account of accounts) { if (account.type === "Configured") { write( @@ -48,14 +48,14 @@ async function run() { ` ${account.id}: (unconfigured)  ` - ) + ); } } } async function listChatsForSelectedAccount() { clear($main); - const selectedAccount = SELECTED_ACCOUNT + const selectedAccount = SELECTED_ACCOUNT; const info = await client.rpc.getAccountInfo(selectedAccount); if (info.type !== "Configured") { return write($main, "Account is not configured"); @@ -88,14 +88,15 @@ async function run() { } } - function onIncomingEvent(event: DeltaChatEvent) { + function onIncomingEvent(accountId: number, event: DcEvent) { write( $side, `

- [${event.id} on account ${event.contextId}]
- f1: ${JSON.stringify(event.field1)}
- f2: ${JSON.stringify(event.field2)} + [${event.type} on account ${accountId}]
+ f1: ${JSON.stringify( + Object.assign({}, event, { type: undefined }) + )}

` ); } diff --git a/deltachat-jsonrpc/typescript/generated/events.ts b/deltachat-jsonrpc/typescript/generated/events.ts index 16a372a21..2f0fa5084 100644 --- a/deltachat-jsonrpc/typescript/generated/events.ts +++ b/deltachat-jsonrpc/typescript/generated/events.ts @@ -1,3 +1,202 @@ // AUTO-GENERATED by typescript-type-def -export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate"|"WebXdInstanceDeleted"); +export type U32=number; +export type Usize=number; +export type Event=(({ +/** + * The library-user may write an informational string to the log. + * + * This event should *not* be reported to the end-user using a popup or something like + * that. + */ +"type":"Info";}&{"msg":string;})|({ +/** + * Emitted when SMTP connection is established and login was successful. + */ +"type":"SmtpConnected";}&{"msg":string;})|({ +/** + * Emitted when IMAP connection is established and login was successful. + */ +"type":"ImapConnected";}&{"msg":string;})|({ +/** + * Emitted when a message was successfully sent to the SMTP server. + */ +"type":"SmtpMessageSent";}&{"msg":string;})|({ +/** + * Emitted when an IMAP message has been marked as deleted + */ +"type":"ImapMessageDeleted";}&{"msg":string;})|({ +/** + * Emitted when an IMAP message has been moved + */ +"type":"ImapMessageMoved";}&{"msg":string;})|({ +/** + * Emitted when an new file in the $BLOBDIR was created + */ +"type":"NewBlobFile";}&{"file":string;})|({ +/** + * Emitted when an file in the $BLOBDIR was deleted + */ +"type":"DeletedBlobFile";}&{"file":string;})|({ +/** + * The library-user should write a warning string to the log. + * + * This event should *not* be reported to the end-user using a popup or something like + * that. + */ +"type":"Warning";}&{"msg":string;})|({ +/** + * The library-user should report an error to the end-user. + * + * As most things are asynchronous, things may go wrong at any time and the user + * should not be disturbed by a dialog or so. Instead, use a bubble or so. + * + * However, for ongoing processes (eg. configure()) + * or for functions that are expected to fail (eg. dc_continue_key_transfer()) + * it might be better to delay showing these events until the function has really + * failed (returned false). It should be sufficient to report only the *last* error + * in a messasge box then. + */ +"type":"Error";}&{"msg":string;})|({ +/** + * An action cannot be performed because the user is not in the group. + * Reported eg. after a call to + * dc_set_chat_name(), dc_set_chat_profile_image(), + * dc_add_contact_to_chat(), dc_remove_contact_from_chat(), + * dc_send_text_msg() or another sending function. + */ +"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({ +/** + * Messages or chats changed. One or more messages or chats changed for various + * reasons in the database: + * - Messages sent, received or removed + * - Chats created, deleted or archived + * - A draft has been set + * + * `chat_id` is set if only a single chat is affected by the changes, otherwise 0. + * `msg_id` is set if only a single message is affected by the changes, otherwise 0. + */ +"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({ +/** + * There is a fresh message. Typically, the user will show an notification + * when receiving this message. + * + * There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event. + */ +"type":"IncomingMsg";}&{"chatId":U32;"msgId":U32;})|({ +/** + * Messages were seen or noticed. + * chat id is always set. + */ +"type":"MsgsNoticed";}&{"chatId":U32;})|({ +/** + * A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to + * DC_STATE_OUT_DELIVERED, see dc_msg_get_state(). + */ +"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({ +/** + * A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to + * DC_STATE_OUT_FAILED, see dc_msg_get_state(). + */ +"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({ +/** + * A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to + * DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state(). + */ +"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({ +/** + * Chat changed. The name or the image of a chat group was changed or members were added or removed. + * Or the verify state of a chat has changed. + * See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat() + * and dc_remove_contact_from_chat(). + * + * This event does not include ephemeral timer modification, which + * is a separate event. + */ +"type":"ChatModified";}&{"chatId":U32;})|({ +/** + * Chat ephemeral timer changed. + */ +"type":"ChatEphemeralTimerModified";}&{"chatId":U32;"timer":U32;})|({ +/** + * Contact(s) created, renamed, blocked or deleted. + * + * @param data1 (int) If set, this is the contact_id of an added contact that should be selected. + */ +"type":"ContactsChanged";}&{"contactId":(U32|null);})|({ +/** + * Location of one or more contact has changed. + * + * @param data1 (u32) contact_id of the contact for which the location has changed. + * If the locations of several contacts have been changed, + * eg. after calling dc_delete_all_locations(), this parameter is set to `None`. + */ +"type":"LocationChanged";}&{"contactId":(U32|null);})|({ +/** + * Inform about the configuration progress started by configure(). + */ +"type":"ConfigureProgress";}&{ +/** + * Progress. + * + * 0=error, 1-999=progress in permille, 1000=success and done + */ +"progress":Usize; +/** + * Progress comment or error, something to display to the user. + */ +"comment":(string|null);})|({ +/** + * Inform about the import/export progress started by imex(). + * + * @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done + * @param data2 0 + */ +"type":"ImexProgress";}&{"progress":Usize;})|({ +/** + * A file has been exported. A file has been written by imex(). + * This event may be sent multiple times by a single call to imex(). + * + * A typical purpose for a handler of this event may be to make the file public to some system + * services. + * + * @param data2 0 + */ +"type":"ImexFileWritten";}&{"path":string;})|({ +/** + * Progress information of a secure-join handshake from the view of the inviter + * (Alice, the person who shows the QR code). + * + * These events are typically sent after a joiner has scanned the QR code + * generated by dc_get_securejoin_qr(). + * + * @param data1 (int) ID of the contact that wants to join. + * @param data2 (int) Progress as: + * 300=vg-/vc-request received, typically shown as "bob@addr joins". + * 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". + * 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. + * 1000=Protocol finished for this contact. + */ +"type":"SecurejoinInviterProgress";}&{"contactId":U32;"progress":Usize;})|({ +/** + * Progress information of a secure-join handshake from the view of the joiner + * (Bob, the person who scans the QR code). + * The events are typically sent while dc_join_securejoin(), which + * may take some time, is executed. + * @param data1 (int) ID of the inviting contact. + * @param data2 (int) Progress as: + * 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." + * (Bob has verified alice and waits until Alice does the same for him) + */ +"type":"SecurejoinJoinerProgress";}&{"contactId":U32;"progress":Usize;})|{ +/** + * The connectivity to the server changed. + * This means that you should refresh the connectivity view + * and possibly the connectivtiy HTML; see dc_get_connectivity() and + * dc_get_connectivity_html() for details. + */ +"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({ +/** + * Inform that a message containing a webxdc instance has been deleted + */ +"type":"WebxdcInstanceDeleted";}&{"msgId":U32;})); diff --git a/deltachat-jsonrpc/typescript/src/client.ts b/deltachat-jsonrpc/typescript/src/client.ts index cddcaeab8..993ced6b4 100644 --- a/deltachat-jsonrpc/typescript/src/client.ts +++ b/deltachat-jsonrpc/typescript/src/client.ts @@ -1,40 +1,56 @@ import * as T from "../generated/types.js"; import * as RPC from "../generated/jsonrpc.js"; import { RawClient } from "../generated/client.js"; -import { EventTypeName } from "../generated/events.js"; +import { Event } from "../generated/events.js"; import { WebsocketTransport, BaseTransport, Request } from "yerpc"; import { TinyEmitter } from "tiny-emitter"; -export type DeltaChatEvent = { - id: EventTypeName; +type DCWireEvent = { + event: T; contextId: number; - field1: any; - field2: any; }; -export type Events = Record< - EventTypeName | "ALL", - (event: DeltaChatEvent) => void ->; +// export type Events = Record< +// Event["type"] | "ALL", +// (event: DeltaChatEvent) => void +// >; + +type Events = { ALL: (accountId: number, event: Event) => void } & { + [Property in Event["type"]]: ( + accountId: number, + event: Extract + ) => void; +}; + +type ContextEvents = { ALL: (event: Event) => void } & { + [Property in Event["type"]]: ( + event: Extract + ) => void; +}; + +export type DcEvent = Event; export class BaseDeltaChat< Transport extends BaseTransport > extends TinyEmitter { rpc: RawClient; account?: T.Account; - private contextEmitters: TinyEmitter[] = []; + private contextEmitters: TinyEmitter[] = []; constructor(public transport: Transport) { super(); this.rpc = new RawClient(this.transport); this.transport.on("request", (request: Request) => { const method = request.method; if (method === "event") { - const event = request.params! as DeltaChatEvent; - this.emit(event.id, event); - this.emit("ALL", event); + const event = request.params! as DCWireEvent; + this.emit(event.event.type, event.contextId, event.event as any); + this.emit("ALL", event.contextId, event.event as any); if (this.contextEmitters[event.contextId]) { - this.contextEmitters[event.contextId].emit(event.id, event); - this.contextEmitters[event.contextId].emit("ALL", event); + this.contextEmitters[event.contextId].emit( + event.event.type, + event.event as any + ); + this.contextEmitters[event.contextId].emit("ALL", event.event); } } }); @@ -70,7 +86,7 @@ export class DeltaChat extends BaseDeltaChat { if (typeof opts === "string") opts = { url: opts }; if (opts) opts = { ...DEFAULT_OPTS, ...opts }; else opts = { ...DEFAULT_OPTS }; - const transport = new WebsocketTransport(opts.url) + const transport = new WebsocketTransport(opts.url); super(transport); this.opts = opts; } diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index b80f4599a..fd59668e8 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -1,12 +1,8 @@ import { assert, expect } from "chai"; -import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js"; -import { - RpcServerHandle, - createTempUser, - startServer, -} from "./test_base.js"; +import { DeltaChat, DcEvent } from "../deltachat.js"; +import { RpcServerHandle, createTempUser, startServer } from "./test_base.js"; -const EVENT_TIMEOUT = 20000 +const EVENT_TIMEOUT = 20000; describe("online tests", function () { let serverHandle: RpcServerHandle; @@ -16,7 +12,7 @@ describe("online tests", function () { let accountId1: number, accountId2: number; before(async function () { - this.timeout(12000) + this.timeout(12000); if (!process.env.DCC_NEW_TMP_EMAIL) { if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) { console.error( @@ -31,10 +27,10 @@ describe("online tests", function () { this.skip(); } serverHandle = await startServer(); - dc = new DeltaChat(serverHandle.url) + dc = new DeltaChat(serverHandle.url); - dc.on("ALL", ({ id, contextId }) => { - if (id !== "Info") console.log(contextId, id); + dc.on("ALL", (contextId, { type }) => { + if (type !== "Info") console.log(contextId, type); }); account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL); @@ -74,7 +70,7 @@ describe("online tests", function () { addr: account2.email, mail_pw: account2.password, }); - await dc.rpc.configure(accountId2) + await dc.rpc.configure(accountId2); accountsConfigured = true; }); @@ -89,14 +85,17 @@ describe("online tests", function () { account2.email, null ); - const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId); + const chatId = await dc.rpc.contactsCreateChatByContactId( + accountId1, + contactId + ); const eventPromise = Promise.race([ waitForEvent(dc, "MsgsChanged", accountId2), waitForEvent(dc, "IncomingMsg", accountId2), ]); await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId); - const { field1: chatIdOnAccountB } = await eventPromise; + const { chatId: chatIdOnAccountB } = await eventPromise; await dc.rpc.acceptChat(accountId2, chatIdOnAccountB); const messageList = await dc.rpc.getMessageIds( accountId2, @@ -121,7 +120,10 @@ describe("online tests", function () { account2.email, null ); - const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId); + const chatId = await dc.rpc.contactsCreateChatByContactId( + accountId1, + contactId + ); const eventPromise = Promise.race([ waitForEvent(dc, "MsgsChanged", accountId2), waitForEvent(dc, "IncomingMsg", accountId2), @@ -131,7 +133,7 @@ describe("online tests", function () { console.log("wait for message from A"); const event = await eventPromise; - const { field1: chatIdOnAccountB } = event; + const { chatId: chatIdOnAccountB } = event; await dc.rpc.acceptChat(accountId2, chatIdOnAccountB); const messageList = await dc.rpc.getMessageIds( @@ -179,22 +181,22 @@ describe("online tests", function () { }); }); -async function waitForEvent( +async function waitForEvent( dc: DeltaChat, - eventType: EventTypeName, + eventType: T, accountId: number, timeout: number = EVENT_TIMEOUT -): Promise { +): Promise> { return new Promise((resolve, reject) => { const rejectTimeout = setTimeout( - () => reject(new Error('Timeout reached before event came in')), + () => reject(new Error("Timeout reached before event came in")), timeout - ) - const callback = (event: DeltaChatEvent) => { - if (event.contextId == accountId) { + ); + const callback = (contextId: number, event: DcEvent) => { + if (contextId == accountId) { dc.off(eventType, callback); - clearTimeout(rejectTimeout) - resolve(event); + clearTimeout(rejectTimeout); + resolve(event as any); } }; dc.on(eventType, callback);