# Messages Zome ## User Stories - As a user, I want to send a message to a contact so that I can deliver textual information to that user - As a user, I want to reply to a specific message so that it would be clear to which message i'm replying to. - As a user, I want to be able to search messages in a chat using a keyword so that I can easily find previously sent messages - As a user, I want to be able to search messages in a chat using a keyword so that I can easily find previously sent messages - As a user, I want to see the status of my messages so that I know if my message has been sent or if my messages has been seen (sending, sent, seen, error) - As a user, I want to search messages by date so that I can pinpoint specific messages - As a user, I want to delete my own message so that I can remove wrong sent messages. - As a user, I want to edit a message I sent so that I can update information. ## Entry Structure ```rust Entry "message_anchor" struct MessageAnchor { author: Address, timestamp: u64, receiver: Address, message_type: String, } Links: { agent_id->message_anchor|message| message_anchor->message } Entry "message" struct Message { anchor: Address, payload: String, lastmodified: u64 } Links: { agent_id->message message_anchor->message|messages| message->message } ``` **Anchor explanation** The message zome follows an anchor-first pattern. An entry -- the message_anchor -- serves as the anchor for another entry -- the message itself. The message_anchor contains static information about the more dynamic message entry. Edited messages form a chain with a link from the latest version of the message to the previous version until the very first version. When a message is edited, a new message entry is created and commited and the link from the message_anchor is transferred form the previous version to the more recent version. **agent_id -> message_anchor** Links the message_anchor from the the author's agent address. This allows fetching of all message_anchors using the agent's address. **agent_id -> message** Links the message from the author's agent address. This allows quick fetching og all messages created by a user. **message_anchor -> message** Links a message (containing the actual payload message) from the message_anchor. The link always points to the latest version of a message (in cases of edited messages). A message can be fetched using the anchor's address. **message -> message** Links a message to its immediated previous version forming a chain. The edit history of a message can be fetched by traversing the links one message after another. ## Entry Relationship Diagram ```mermaid graph LR subgraph messageszome subgraph agents alice_id end subgraph entries subgraph message_anchors alice_id-->message_1_anchor alice_id-->message_2_anchor end subgraph messages message_1_v1 -.-> message_1_anchor alice_id --> message_1_v1 message_1_anchor --> message_1_v1 message_2_v3 -.-> message_2_anchor message_2_v2 -.-> message_2_anchor message_2_v1 -.-> message_2_anchor message_2_v2 --> message_2_v1 message_2_v3 --> message_2_v2 message_2_anchor --> message_2_v3 alice_id --> message_2_v3 end end end ``` ## Validation ### Entries ### Links ## Zome Functions ### send ```rust pub fn send_message(recipient: Address, message: String, timestamp: u64) -> ZomeApiResult<MessageReturn> { let message_anchor = MessageAnchor::new(AGENT_ADDRESS.to_string().into(), recipient.clone(), timestamp.clone(), "text".to_string()); let message_anchor_entry = message_anchor.clone().entry(); let message_anchor_address = hdk::commit_entry(&message_anchor_entry)?; hdk::link_entries( &AGENT_ADDRESS, &message_anchor_address, AGENT_MESSAGE_ANCHOR_LINK_TYPE, "" )?; let message_struct = Message::new(message_anchor_address.clone(), message.clone(), timestamp.clone()); let message_entry = message_struct.entry(); let message_entry_address = hdk::commit_entry(&message_entry)?; hdk::link_entries( &message_anchor_address, &message_entry_address, MESSAGE_ANCHOR_LINK_TYPE, "" )?; hdk::link_entries( &AGENT_ADDRESS, &message_entry_address, AUTHOR_MESSAGE_ANCHOR_LINK_TYPE, "" )?; let message_struct = Message::new(message_anchor_address.clone(), message.clone(), timestamp.clone()); let message_return = MessageReturn::new(message_anchor.clone(), message_anchor_address.clone(), message_struct); match hdk::send(recipient.to_owned(), json!(message_return).to_string(), 10000.into()) { Ok(_response) => { let _emitted = hdk::emit_signal( "message_sent".to_owned(), JsonString::from_json(&format!("{{\"code\": \"{}\", \"message\": \"{}\", \"recipient\": \"{}\"}}", "message_sent".to_owned(), message, recipient.to_string())) )?; Ok(message_return) }, _=> { let _emitted = hdk::emit_signal( "send_message_recipient_offline", JsonString::from_json(&format!("{{\"code\": \"{}\"}}", "recipient_offline".to_owned())) )?; Ok(message_return) } } } ``` Takes in 3 input values -- the recipient agent's address \<HashtSring\>, the message payload \<Strinh\>, and the timestamp \<u64\> of the message creation. If successful, the function commits a MessageAnchor entry and a MessageEntry to the DHT, emits a signal to the sender that the call was successful, and returns a structure MessageReturn if successful. On the recipient's side, the receive zome function is triggered. MessageReturn { address: address, author: anchor.author, recipient: anchor.recipient, timestamp: anchor.timestamp, message: message.payload, lastmodified: message.lastmodified } ### receive A callback triggered when a message is received through the send function. Emits a signal notifying the recipient that a message has been received and contains the actual payload of the message. ```rust pub fn receive(_from: Address, payload: String) -> String { let maybe_dna_instance_id_json = hdk::property("instance_id"); let dna_instance_id: Option<String> = match maybe_dna_instance_id_json { Ok(dna_instance_id_json ) => { match from_str(&dna_instance_id_json.to_string()) { Ok(instance_id) => Some(instance_id), _ => Some("".to_owned()), } }, _ => None, }; let _emitted = match dna_instance_id { Some(instance_id) => { hdk::emit_signal( "message_received", JsonString::from_json(&format!("{{\"payload\": {}, \"instance_id\": \"{}\"}}", payload, instance_id)) ) }, None => { hdk::emit_signal( "message_received", JsonString::from_json(&format!("{{\"payload\": {}}}", payload)) ) } }; format!("{{\"code\": \"{}\"}}", "message_sent".to_string()) } ``` ### update_message Updates an existing message with a new message string. Takes in as input the address of the MessageAnchor \<HashString\>, the new message \<String\>, and the timestamp of when the edit mas made. Returns true if the operation was successful. ```rust pub fn update_message(id: Address, message: String, lastmodified: u64) -> ZomeApiResult<bool> { let message_address = get_message_head_address(id.clone())?; hdk::remove_link( &id, &message_address, MESSAGE_ANCHOR_LINK_TYPE, "" )?; hdk::remove_link( &AGENT_ADDRESS, &message_address, AUTHOR_MESSAGE_ANCHOR_LINK_TYPE, "" )?; let message_struct = Message::new(id.clone(), message.clone(), lastmodified.clone()); let message_entry = message_struct.entry(); let message_entry_address = hdk::update_entry(message_entry, &message_address)?; hdk::link_entries( &id, &message_entry_address, MESSAGE_ANCHOR_LINK_TYPE, "" )?; hdk::link_entries( &AGENT_ADDRESS, &message_entry_address, AUTHOR_MESSAGE_ANCHOR_LINK_TYPE, "" )?; hdk::link_entries( &message_entry_address, &message_address, MESSAGE_MESSAGE_ANCHOR_LINK_TYPE, "" )?; Ok(true) } ``` ### delete_messages *(may be updated to just remove the MessageAnchor and all links instead of removing all Message entries)* Deletes an existing message. Takes in as input the address of the MessageAnchor \<HashString\>. Marks as removed the Message entries (all message versions) and the MessageAnchor. Returns true if the operation was successful. ```rust pub fn delete_messages(anchor_addresses: Vec<Address>) -> ZomeApiResult<bool> { let deduped_addresses = address_deduper(anchor_addresses); for i in 0..deduped_addresses.len() { let anchor_address = deduped_addresses[i].clone(); let message_head_address = get_message_head_address(anchor_address.clone())?; let mut current_address: Address = message_head_address.clone(); 'inner: loop { let message_link = hdk::get_links( &current_address, LinkMatch::Exactly(MESSAGE_MESSAGE_ANCHOR_LINK_TYPE), LinkMatch::Exactly("") )?.addresses(); if message_link.is_empty() { hdk::remove_link( &AGENT_ADDRESS, &message_head_address, AUTHOR_MESSAGE_ANCHOR_LINK_TYPE, "" )?; hdk::remove_entry(&current_address)?; break 'inner; }; hdk::remove_link( &current_address, &message_link[0], MESSAGE_MESSAGE_ANCHOR_LINK_TYPE, "" )?; current_address = message_link[0].clone(); }; hdk::remove_link( &anchor_address, &message_head_address, MESSAGE_ANCHOR_LINK_TYPE, "" )?; hdk::remove_entry(&anchor_address)?; } Ok(true) } ``` ### get_all_messages_from_addresses Fetches all messages authored by the given agent addresses. Takes in an array of addresses \<HashString\> and returns an array of MessageReturn structures. ```rust pub fn get_all_messages_from_addresses(ids: Vec<Address>)->ZomeApiResult<Vec<MessageReturn>> { let deduped_ids = address_deduper(ids); let messages_entry_address = deduped_ids.into_iter().map(|id: HashString| { match hdk::get_links( &id, LinkMatch::Exactly(AUTHOR_MESSAGE_ANCHOR_LINK_TYPE), LinkMatch::Exactly("") ) { Ok(res) => res.addresses(), Err(_e) => Vec::default(), } }).flatten().collect::<Vec<HashString>>(); match messages_entry_address.is_empty() { false => { let mut res: Vec<MessageReturn> = Vec::new(); for i in 0..messages_entry_address.len() { // get the entries associated with the address let entry_result = hdk::api::get_entry_result( &messages_entry_address[i], GetEntryOptions::new( StatusRequestKind::default(), true, true, Timeout::default() ) )?; match entry_result.found() { true => { let result_type = entry_result.result; match result_type { GetEntryResultType::Single(item) => { if let Some(entry) = item.entry { if let Some(message) = Message::from_entry(&entry){ let anchor_entry = hdk::get_entry(&message.anchor)?; let message_anchor = match anchor_entry { Some(t) => { if let Some(anchor) = MessageAnchor::from_entry(&t) { anchor } else { MessageAnchor::blank() } }, None => MessageAnchor::blank() }; res.push(MessageReturn::new(message_anchor,message.anchor.clone(),message)) } } }, GetEntryResultType::All(history) => { if let Some(entry_item) = history.items.last() { if let Some(entry) = &(*entry_item).entry { if let Some(message) = Message::from_entry(&entry){ let anchor_entry = hdk::get_entry(&message.anchor)?; let message_anchor = match anchor_entry { Some(t) => { if let Some(anchor) = MessageAnchor::from_entry(&t) { anchor } else { MessageAnchor::blank() } }, None => MessageAnchor::blank() }; res.push(MessageReturn::new(message_anchor,message.anchor.clone(),message)) } } } } } }, false => continue } } Ok(res) } true => Ok(Vec::new()) } } ```