diff --git a/CHANGELOG.md b/CHANGELOG.md index 241281f01..f78195d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ ## Unreleased ### API-Changes +- jsonrpc: add function: #3641 + - `getChatContacts()` + - `createGroupChat()` + - `createBroadcastList()` + - `setChatName()` + - `setChatProfileImage()` + - `downloadFullMessage()` + - `lookupContactIdByAddr()` + - `sendVideochatInvitation()` + - `searchMessages()` + - `messageIdsToSearchResults()` +- jsonrpc: add type: #3641 + - `MessageSearchResult` + ### Changes diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 037e2ad2d..97f50bad4 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -2,11 +2,11 @@ use anyhow::{anyhow, bail, Context, Result}; use deltachat::{ chat::{ self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat, - remove_contact_from_chat, Chat, ChatId, ChatItem, + remove_contact_from_chat, Chat, ChatId, ChatItem, ProtectionStatus, }, chatlist::Chatlist, config::Config, - contact::{may_be_valid_addr, Contact, ContactId}, + contact::{may_be_valid_addr, Contact, ContactId, Origin}, context::get_info, message::{delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype}, provider::get_provider_info, @@ -39,7 +39,7 @@ use types::webxdc::WebxdcMessageInfo; use self::types::{ chat::{BasicChat, MuteDuration}, - message::{MessageNotificationInfo, MessageViewtype}, + message::{MessageNotificationInfo, MessageSearchResult, MessageViewtype}, }; #[derive(Clone, Debug)] @@ -407,9 +407,7 @@ impl CommandApi { /// really unexpected when deletion results in contacting all members again, /// (3) only leaving groups is also a valid usecase. /// - /// To leave a chat explicitly, use dc_remove_contact_from_chat() with - /// chat_id=DC_CONTACT_ID_SELF) - // TODO fix doc comment after adding dc_remove_contact_from_chat + /// To leave a chat explicitly, use leave_group() async fn delete_chat(&self, account_id: u32, chat_id: u32) -> Result<()> { let ctx = self.get_context(account_id).await?; ChatId::new(chat_id).delete(&ctx).await @@ -494,6 +492,120 @@ impl CommandApi { add_contact_to_chat(&ctx, ChatId::new(chat_id), ContactId::new(contact_id)).await } + /// Get the contact IDs belonging to a chat. + /// + /// - for normal chats, the function always returns exactly one contact, + /// DC_CONTACT_ID_SELF is returned only for SELF-chats. + /// + /// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned + /// explicitly as it may happen that oneself gets removed from a still existing + /// group + /// + /// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included + /// + /// - for mailing lists, the behavior is not documented currently, we will decide on that later. + /// for now, the UI should not show the list for mailing lists. + /// (we do not know all members and there is not always a global mailing list address, + /// so we could return only SELF or the known members; this is not decided yet) + async fn get_chat_contacts(&self, account_id: u32, chat_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + let contacts = chat::get_chat_contacts(&ctx, ChatId::new(chat_id)).await?; + Ok(contacts.iter().map(|id| id.to_u32()).collect::>()) + } + + /// Create a new group chat. + /// + /// After creation, + /// the group has one member with the ID DC_CONTACT_ID_SELF + /// and is in _unpromoted_ state. + /// This means, you can add or remove members, change the name, + /// the group image and so on without messages being sent to all group members. + /// + /// This changes as soon as the first message is sent to the group members + /// and the group becomes _promoted_. + /// After that, all changes are synced with all group members + /// by sending status message. + /// + /// To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`. + /// This may be useful if you want to show some help for just created groups. + /// + /// @param protect If set to 1 the function creates group with protection initially enabled. + /// Only verified members are allowed in these groups + /// and end-to-end-encryption is always enabled. + async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result { + let ctx = self.get_context(account_id).await?; + let protect = match protect { + true => ProtectionStatus::Protected, + false => ProtectionStatus::Unprotected, + }; + chat::create_group_chat(&ctx, protect, &name) + .await + .map(|id| id.to_u32()) + } + + /// Create a new broadcast list. + /// + /// Broadcast lists are similar to groups on the sending device, + /// however, recipients get the messages in normal one-to-one chats + /// and will not be aware of other members. + /// + /// Replies to broadcasts go only to the sender + /// and not to all broadcast recipients. + /// Moreover, replies will not appear in the broadcast list + /// but in the one-to-one chat with the person answering. + /// + /// The name and the image of the broadcast list is set automatically + /// and is visible to the sender only. + /// Not asking for these data allows more focused creation + /// and we bypass the question who will get which data. + /// Also, many users will have at most one broadcast list + /// so, a generic name and image is sufficient at the first place. + /// + /// Later on, however, the name can be changed using dc_set_chat_name(). + /// The image cannot be changed to have a unique, recognizable icon in the chat lists. + /// All in all, this is also what other messengers are doing here. + async fn create_broadcast_list(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + chat::create_broadcast_list(&ctx) + .await + .map(|id| id.to_u32()) + } + + /// Set group name. + /// + /// If the group is already _promoted_ (any message was sent to the group), + /// all group members are informed by a special status message that is sent automatically by this function. + /// + /// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + async fn set_chat_name(&self, account_id: u32, chat_id: u32, new_name: String) -> Result<()> { + let ctx = self.get_context(account_id).await?; + chat::set_chat_name(&ctx, ChatId::new(chat_id), &new_name).await + } + + /// Set group profile image. + /// + /// If the group is already _promoted_ (any message was sent to the group), + /// all group members are informed by a special status message that is sent automatically by this function. + /// + /// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + /// + /// To find out the profile image of a chat, use dc_chat_get_profile_image() + /// + /// @param image_path Full path of the image to use as the group image. The image will immediately be copied to the + /// `blobdir`; the original image will not be needed anymore. + /// If you pass null here, the group image is deleted (for promoted groups, all members are informed about + /// this change anyway). + async fn set_chat_profile_image( + &self, + account_id: u32, + chat_id: u32, + image_path: Option, + ) -> Result<()> { + let ctx = self.get_context(account_id).await?; + chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), image_path.unwrap_or_default()) + .await + } + // for now only text messages, because we only used text messages in desktop thusfar async fn add_device_message( &self, @@ -670,6 +782,65 @@ impl CommandApi { get_msg_info(&ctx, MsgId::new(message_id)).await } + /// Asks the core to start downloading a message fully. + /// This function is typically called when the user hits the "Download" button + /// that is shown by the UI in case `download_state` is `'Available'` or `'Failure'` + /// + /// On success, the @ref DC_MSG "view type of the message" may change + /// or the message may be replaced completely by one or more messages with other message IDs. + /// That may happen e.g. in cases where the message was encrypted + /// and the type could not be determined without fully downloading. + /// Downloaded content can be accessed as usual after download. + /// + /// To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted. + async fn download_full_message(&self, account_id: u32, message_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; + MsgId::new(message_id).download_full(&ctx).await + } + + /// Search messages containing the given query string. + /// Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set). + /// + /// Global chat results are typically displayed using dc_msg_get_summary(), chat + /// search results may just hilite the corresponding messages and present a + /// prev/next button. + /// + /// For global search, result is limited to 1000 messages, + /// this allows incremental search done fast. + /// So, when getting exactly 1000 results, the result may be truncated; + /// the UIs may display sth. as "1000+ messages found" in this case. + /// Chat search (if a chat_id is set) is not limited. + async fn search_messages( + &self, + account_id: u32, + query: String, + chat_id: Option, + ) -> Result> { + let ctx = self.get_context(account_id).await?; + let messages = ctx.search_msgs(chat_id.map(ChatId::new), &query).await?; + Ok(messages + .iter() + .map(|msg_id| msg_id.to_u32()) + .collect::>()) + } + + async fn message_ids_to_search_results( + &self, + account_id: u32, + message_ids: Vec, + ) -> Result> { + let ctx = self.get_context(account_id).await?; + let mut results: HashMap = + HashMap::with_capacity(message_ids.len()); + for id in message_ids { + results.insert( + id, + MessageSearchResult::from_msg_id(&ctx, MsgId::new(id)).await?, + ); + } + Ok(results) + } + // --------------------------------------------- // contact // --------------------------------------------- @@ -815,6 +986,21 @@ impl CommandApi { Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await } + /// Check if an e-mail address belongs to a known and unblocked contact. + /// To get a list of all known and unblocked contacts, use contacts_get_contacts(). + /// + /// To validate an e-mail address independently of the contact database + /// use check_email_validity(). + async fn lookup_contact_id_by_addr( + &self, + account_id: u32, + addr: String, + ) -> Result> { + let ctx = self.get_context(account_id).await?; + let contact_id = Contact::lookup_id_by_addr(&ctx, &addr, Origin::IncomingReplyTo).await?; + Ok(contact_id.map(|id| id.to_u32())) + } + // --------------------------------------------- // chat // --------------------------------------------- @@ -1018,6 +1204,13 @@ impl CommandApi { } } + async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + chat::send_videochat_invitation(&ctx, ChatId::new(chat_id)) + .await + .map(|msg_id| msg_id.to_u32()) + } + // --------------------------------------------- // misc prototyping functions // that might get removed later again diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index f98fc2767..bb5f15107 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Result}; use deltachat::chat::Chat; +use deltachat::constants::Chattype; use deltachat::contact::Contact; use deltachat::context::Context; use deltachat::download; @@ -345,3 +346,42 @@ impl MessageNotificationInfo { }) } } + +#[derive(Serialize, TypeDef)] +#[serde(rename_all = "camelCase")] +pub struct MessageSearchResult { + id: u32, + author_profile_image: Option, + author_name: String, + author_color: String, + chat_name: Option, + message: String, + timestamp: i64, +} + +impl MessageSearchResult { + pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result { + let message = Message::load_from_db(context, msg_id).await?; + let chat = Chat::load_from_db(context, message.get_chat_id()).await?; + let sender = Contact::load_from_db(context, message.get_from_id()).await?; + + let profile_image = match sender.get_profile_image(context).await? { + Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()), + None => None, + }; + + Ok(Self { + id: msg_id.to_u32(), + author_profile_image: profile_image, + author_name: sender.get_display_name().to_owned(), + author_color: color_int_to_hex_string(sender.get_color()), + chat_name: if chat.get_type() == Chattype::Single { + Some(chat.get_name().to_owned()) + } else { + None + }, + message: message.get_text().unwrap_or_default(), + timestamp: message.get_timestamp(), + }) + } +} diff --git a/deltachat-jsonrpc/typescript/generated/client.ts b/deltachat-jsonrpc/typescript/generated/client.ts index 9b28600ae..a7874c0b6 100644 --- a/deltachat-jsonrpc/typescript/generated/client.ts +++ b/deltachat-jsonrpc/typescript/generated/client.ts @@ -240,8 +240,7 @@ export class RawClient { * really unexpected when deletion results in contacting all members again, * (3) only leaving groups is also a valid usecase. * - * To leave a chat explicitly, use dc_remove_contact_from_chat() with - * chat_id=DC_CONTACT_ID_SELF) + * To leave a chat explicitly, use leave_group() */ public deleteChat(accountId: T.U32, chatId: T.U32): Promise { return (this._transport.request('delete_chat', [accountId, chatId] as RPC.Params)) as Promise; @@ -311,6 +310,110 @@ export class RawClient { return (this._transport.request('add_contact_to_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise; } + /** + * Get the contact IDs belonging to a chat. + * + * - for normal chats, the function always returns exactly one contact, + * DC_CONTACT_ID_SELF is returned only for SELF-chats. + * + * - for group chats all members are returned, DC_CONTACT_ID_SELF is returned + * explicitly as it may happen that oneself gets removed from a still existing + * group + * + * - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included + * + * - for mailing lists, the behavior is not documented currently, we will decide on that later. + * for now, the UI should not show the list for mailing lists. + * (we do not know all members and there is not always a global mailing list address, + * so we could return only SELF or the known members; this is not decided yet) + */ + public getChatContacts(accountId: T.U32, chatId: T.U32): Promise<(T.U32)[]> { + return (this._transport.request('get_chat_contacts', [accountId, chatId] as RPC.Params)) as Promise<(T.U32)[]>; + } + + /** + * Create a new group chat. + * + * After creation, + * the group has one member with the ID DC_CONTACT_ID_SELF + * and is in _unpromoted_ state. + * This means, you can add or remove members, change the name, + * the group image and so on without messages being sent to all group members. + * + * This changes as soon as the first message is sent to the group members + * and the group becomes _promoted_. + * After that, all changes are synced with all group members + * by sending status message. + * + * To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`. + * This may be useful if you want to show some help for just created groups. + * + * @param protect If set to 1 the function creates group with protection initially enabled. + * Only verified members are allowed in these groups + * and end-to-end-encryption is always enabled. + */ + public createGroupChat(accountId: T.U32, name: string, protect: boolean): Promise { + return (this._transport.request('create_group_chat', [accountId, name, protect] as RPC.Params)) as Promise; + } + + /** + * Create a new broadcast list. + * + * Broadcast lists are similar to groups on the sending device, + * however, recipients get the messages in normal one-to-one chats + * and will not be aware of other members. + * + * Replies to broadcasts go only to the sender + * and not to all broadcast recipients. + * Moreover, replies will not appear in the broadcast list + * but in the one-to-one chat with the person answering. + * + * The name and the image of the broadcast list is set automatically + * and is visible to the sender only. + * Not asking for these data allows more focused creation + * and we bypass the question who will get which data. + * Also, many users will have at most one broadcast list + * so, a generic name and image is sufficient at the first place. + * + * Later on, however, the name can be changed using dc_set_chat_name(). + * The image cannot be changed to have a unique, recognizable icon in the chat lists. + * All in all, this is also what other messengers are doing here. + */ + public createBroadcastList(accountId: T.U32): Promise { + return (this._transport.request('create_broadcast_list', [accountId] as RPC.Params)) as Promise; + } + + /** + * Set group name. + * + * If the group is already _promoted_ (any message was sent to the group), + * all group members are informed by a special status message that is sent automatically by this function. + * + * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + */ + public setChatName(accountId: T.U32, chatId: T.U32, newName: string): Promise { + return (this._transport.request('set_chat_name', [accountId, chatId, newName] as RPC.Params)) as Promise; + } + + /** + * Set group profile image. + * + * If the group is already _promoted_ (any message was sent to the group), + * all group members are informed by a special status message that is sent automatically by this function. + * + * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + * + * To find out the profile image of a chat, use dc_chat_get_profile_image() + * + * @param image_path Full path of the image to use as the group image. The image will immediately be copied to the + * `blobdir`; the original image will not be needed anymore. + * If you pass null here, the group image is deleted (for promoted groups, all members are informed about + * this change anyway). + */ + public setChatProfileImage(accountId: T.U32, chatId: T.U32, imagePath: (string|null)): Promise { + return (this._transport.request('set_chat_profile_image', [accountId, chatId, imagePath] as RPC.Params)) as Promise; + } + public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise { return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise; @@ -426,6 +529,46 @@ export class RawClient { return (this._transport.request('get_message_info', [accountId, messageId] as RPC.Params)) as Promise; } + /** + * Asks the core to start downloading a message fully. + * This function is typically called when the user hits the "Download" button + * that is shown by the UI in case `download_state` is `'Available'` or `'Failure'` + * + * On success, the @ref DC_MSG "view type of the message" may change + * or the message may be replaced completely by one or more messages with other message IDs. + * That may happen e.g. in cases where the message was encrypted + * and the type could not be determined without fully downloading. + * Downloaded content can be accessed as usual after download. + * + * To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted. + */ + public downloadFullMessage(accountId: T.U32, messageId: T.U32): Promise { + return (this._transport.request('download_full_message', [accountId, messageId] as RPC.Params)) as Promise; + } + + /** + * Search messages containing the given query string. + * Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set). + * + * Global chat results are typically displayed using dc_msg_get_summary(), chat + * search results may just hilite the corresponding messages and present a + * prev/next button. + * + * For global search, result is limited to 1000 messages, + * this allows incremental search done fast. + * So, when getting exactly 1000 results, the result may be truncated; + * the UIs may display sth. as "1000+ messages found" in this case. + * Chat search (if a chat_id is set) is not limited. + */ + public searchMessages(accountId: T.U32, query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> { + return (this._transport.request('search_messages', [accountId, query, chatId] as RPC.Params)) as Promise<(T.U32)[]>; + } + + + public messageIdsToSearchResults(accountId: T.U32, messageIds: (T.U32)[]): Promise> { + return (this._transport.request('message_ids_to_search_results', [accountId, messageIds] as RPC.Params)) as Promise>; + } + /** * Get a single contact options by ID. */ @@ -491,6 +634,17 @@ export class RawClient { return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise; } + /** + * Check if an e-mail address belongs to a known and unblocked contact. + * To get a list of all known and unblocked contacts, use contacts_get_contacts(). + * + * To validate an e-mail address independently of the contact database + * use check_email_validity(). + */ + public lookupContactIdByAddr(accountId: T.U32, addr: string): Promise<(T.U32|null)> { + return (this._transport.request('lookup_contact_id_by_addr', [accountId, addr] as RPC.Params)) as Promise<(T.U32|null)>; + } + /** * Returns all message IDs of the given types in a chat. * Typically used to show a gallery. @@ -602,6 +756,11 @@ export class RawClient { return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>; } + + public sendVideochatInvitation(accountId: T.U32, chatId: T.U32): Promise { + return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise; + } + /** * Returns the messageid of the sent message */ diff --git a/deltachat-jsonrpc/typescript/generated/types.ts b/deltachat-jsonrpc/typescript/generated/types.ts index 9d9456643..5930037f7 100644 --- a/deltachat-jsonrpc/typescript/generated/types.ts +++ b/deltachat-jsonrpc/typescript/generated/types.ts @@ -146,5 +146,6 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag * also known as summary_text2 */ "summaryText":string;}; +export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;}; export type F64=number; -export type __AllTyps=[string,boolean,Record,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record,U32,U32,string,U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],null,U32,U32,U32,string,U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,null,U32,U32,(Message|null),U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null]; +export type __AllTyps=[string,boolean,Record,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record,U32,string,(string|null),null,U32,Record,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],null,U32,U32,U32,string,U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];