# 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(
¤t_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(¤t_address)?;
break 'inner;
};
hdk::remove_link(
¤t_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())
}
}
```