# P2PFileShare (without chunking)
###### tags: `Kizuna-architecture`
<style> .ui-infobar, #doc.markdown-body { max-width: 2000px; } </style>
## User Stories
- [As a user, I want to send files to a chat so that I can share my files.](https://beyonder.atlassian.net/browse/KIZUNA-63)
- [As a user, I want to send media files to a chat so that I can share my images/videos.](https://beyonder.atlassian.net/browse/KIZUNA-64)
- [As a user, I want to send voice notes to a chat/groupchat so that I can share that voice note](https://beyonder.atlassian.net/browse/KIZUNA-65)
- [I want to be notified of incoming file attachment message so I know that someone sent me a file](https://beyonder.atlassian.net/browse/KIZUNA-371)
- [I want to see media files sent to the chat so that I can check what they sent.](https://beyonder.atlassian.net/browse/KIZUNA-61)
- [I want to see shared files/media in a chat/group chat so that I can browse through previously sent files.](https://beyonder.atlassian.net/browse/KIZUNA-96)
- [I want to download files sent to the chat so that I can have it locally.](https://beyonder.atlassian.net/browse/KIZUNA-60)
- [I want to choose whether to auto download files/media ](https://beyonder.atlassian.net/browse/KIZUNA-331)(maybe move to preference?)
- [I want to choose whether to have preview of files/media](https://beyonder.atlassian.net/browse/KIZUNA-332)(maybe move to preference?)
## Notes
* 16Mb file size limit
* will become almost identical to P2PMessage
* should we just merge this with P2PMessage (i.e. have the ability to choose the payload type for the P2PMessage)?
- pros of merging
- we can recycle almost all zome fns
- getters will be much easier (especially getter for scrolling, latest messages)
- dedicated getters for files or messages will be easy as well since we are only dealing with source chain (not the same with group)
- We do not have to write duplicated codes
- cons of merging
- make it harder for us to implement chunking in the future
- zome becomes too big?
* **suggestion**: have p2pmessage accomodate small file sizes (16Mb) and then have another zome in the future dedicated for large file sending?
* chunking and DHT storage can be done for group file sending
* TODO: Asynchronous file sending through ephemeral storage
* TODO: Encryption
## Entry Relationship Diagram
```mermaid
graph LR
subgraph p2pmessage zome
subgraph agents
alice
bobby
end
subgraph files
file_sent_to_alice
file_sent_to_bobby
alice.->|has in source chain|file_sent_to_alice
alice.->|has in source chain|file_sent_to_bobby
bobby.->|has in source chain|file_sent_to_alice
bobby.->|has in source chain|file_sent_to_bobby
end
end
```
## Entries
```rust=
#[hdk_entry(id="file", visibility="private")]
struct P2PFile {
author: AgentPubKey,
receiver: AgentPubKey,
file_name: String,
file_size: usize,
file_type: String,
time_sent: Timestamp,
reply_to: Option<EntryHash>
bytes: Vec<u8> // actualy bytes of file
}
#[hdk_entry(id="filereceipt", visibility="private")]
struct P2PFileReceipt {
id: Vec<EntryHash>,
time_received: Option<Timestamp>,
status: Status
}
```
## Data Structures
```rust=
enum Status {
Sent,
Received {timestamp: Timestamp},
Read {timestamp: Timestamp}
}
struct FileBundle(P2PFile, Vec<P2PFileReceipts>)
struct AgentFiles(HashMap<AgentPubKey, Vec<EntryHash>>)
struct FileContents(HashMap<EntryHash, FileBundle>)
struct ReceiptContents(HashMap<EntryHash, P2PFileReceipt>)
struct FilesOutput(
AgentFiles,
FileContents,
ReceiptContents
)
```
## Zome functions
### `init`
```mermaid
sequenceDiagram
participant ACC as Alice_Conversation_Cell
participant BCC as Bobby_Conversation_Cell
par alice init
ACC-->>ACC: set unrestricted access to receive_file
ACC-->>ACC: set unrestricted access to notify_delivery
ACC-->>ACC: set unrestricted access to recv_remote_signal
and bob init
BCC-->>BCC: set unrestricted access to receive_file
BCC-->>BCC: set unrestricted acesss to notify_delivery
BCC-->>BCC: set unrestricted access to recv_remote_signal
end
```
### send_file
```rust=
struct FileInput {
receiver: AgentPubKey,
file_name: String,
file_size: usize,
file_type: String,
file_hash: String,
bytes: Vec<u8>
}
fn send_file(file_input: FileInput) -> ExternResult<FileBundle>
```
```mermaid
sequenceDiagram
participant AUI as Alice_UI
participant ACC as Alice_Conversation_Cell
participant BCC as Bobby_Conversation_Cell
participant BUI as Bobby_UI
participant NH as Neighborhood
AUI-->>ACC: call `send_file` function
rect rgba(255,0,0,0.3)
ACC-->>ACC: check if receiver is blocked by calling `in_blocked` <br> in Contacts zome
end
opt receiver is blocked
ACC-->>AUI: return error (cannot send to blocked)
end
ACC-->>ACC: construct P2PFile from input
ACC-->>ACC: construct 'Sent' P2PFileReceipt
ACC-->>ACC: commit P2PFile and P2PFileReceipt to source chain <br> while calling post_commit callback
ACC-->>AUI: return FileBundle <br> (skip this until post_commit becomes available)
Note over ACC: yellow box will be moved to post commit
rect rgba(255,255,0,0.2)
ACC-->>BCC: call_remote Bobby's `receive_file` function
alt call_remote ok (receiver is online)
rect rgba(0, 0, 255, 0.4)
BCC-->>BCC: invoke `receive_file`
end
BCC-->>ACC: return P2PFile and P2PFileReceipt
ACC-->>ACC: commit P2PFileReceipt to source chain
ACC-->>AUI: return FileBundle
else call_remote timeout (receiver is offline)
Note over ACC, BCC: timeout can also mean that the peer was not found in which case the receiver needs to poll the message from the neighborhood
rect rgba(0,255,0,0.8)
ACC-->>NH: call `send_to_neighbor` with P2PFile as payload
end
ACC-->>ACC: commit P2PFileReceipt to source chain
ACC-->>AUI: return FileBundle
else call_remote unauthorized
Note over ACC, BCC: case should not exist
end
end
```
### `send_to_neighbor`
```rust=
fn send_to_neighbor(file_bundle: FileBundle) -> ExternResult<BooleanWrapper>
```
```mermaid
sequenceDiagram
participant BCC as Bobby_Conversation_Cell
participant NH as Neighborhood
BCC-->>NH: send P2PFile or P2PFileReceipt via ephemeral delivery system
NH-->>BCC: return true
```
### `receive_file`
```rust
fn receive_message(file: P2PFile) -> ExternResult<FileBundle>
```
```mermaid
sequenceDiagram
participant ACC as Alice_Conversation_Cell
participant BCC as Bobby_Conversation_Cell
participant BUI as Bobby_UI
rect rgba(255,0,0,0.3)
BCC-->>BCC: check if author is blocked by calling `in_blocked` <br> in Contacts zome
end
opt author is not blocked
BCC-->>BCC: generate timestamp for 'Delivered' receipt
BCC-->>BCC: construct P2PFile <br> and 'Delivered' P2PFileReceipt
BCC-->>BCC: commit P2PFile and P2PFileReceipt to source chain
BCC-->>BUI: emit_signal FileBundle
BCC-->>ACC: return FileBundle
end
```
### `notify_delivery`
```rust
fn notify_delivery(file_receipt: P2PFileReceipt) -> ExternResult<BooleanWrapper>
```
```mermaid
sequenceDiagram
participant AUI as Alice_UI
participant ACC as Alice_Conversation_Cell
participant BCC as Bobby_Conversation_Cell
ACC-->>ACC: commit P2PFileReceipt to source chain
ACC-->>AUI: emit signal P2PFileElement
AUI-->>AUI: display file (flag as Delivered)
ACC-->>BCC: return true
```
### `fetch_undelivered_items`
```rust=
pub struct P2PFilesOutput(
AgentFiles,
FileContents,
ReceiptContents
)
fn fetch_undelivered_items() -> ExternResult<P2PFilesOutput>
```
```mermaid
sequenceDiagram
participant ACC as Alice_Conversation_Cell
participant BCC as Bobby_Conversation_Cell
participant NH as Neighborhood
participant BUI as Bobby_UI
Note over BCC, NH: This process will be elaborated when the ephemeral delivery system is implemented
rect rgba(200, 200, 200, 0.5)
BCC-->>NH: ask for P2PFile/P2PFileReceipts intended for Bobby
NH-->>BCC: return P2PFile/P2FileReceipts
end
BCC-->>BCC: initialize empty vector "ids" of type EntryHash
BCC-->>BCC: initialize empty vector "files" of type P2PFile
loop for every P2PFile/P2PFileReceipts
rect rgba(255,0,0,0.3)
BCC-->>BCC: check if author is blocked by calling `in_blocked` <br> in Contacts zome
end
opt author is not blocked
alt entry type is P2PFileReceipt
rect rgba(0, 255, 0, .5)
BCC-->>BCC: call `notify_delivery`
end
else entry type is P2PMessage
BCC-->>BCC: commit P2PFile entry to source chain
BCC-->>BCC: append P2PFile.entry_hash to vector "ids"
BCC-->>BCC: append P2PFile to vector "messages"
end
end
end
BCC-->>BCC: construct 'Delivered' P2PFileReceipt with the vector "ids" as id
BCC-->>BCC: commit P2PFileReceipt to source chain
rect rgba(0, 0, 255, .3)
BCC-->>ACC: call_remote `notify_delivery` function
end
ACC-->>BCC: return call_remote result
alt call_remote ok
else call_remote timeout
rect rgba(0, 255, 0, .5)
BCC-->>NH: call `send_to_neighbor` with the P2PFileReceipt as payload
end
else call_remote unauthorized
Note over BCC, NH: case should not exist
end
BCC-->>BUI: return true
```
### `get_files_by_agent_by_timestamp`
```rust=
pub struct P2PAgentDateFilter {
conversant: AgentPubKey,
date: Timestamp
}
fn get_files_by_agent_by_timestamp(filter: P2PAgentDateFilter) -> ExternResult<P2PFilesOutput>
```
```mermaid
sequenceDiagram
participant AUI as Alice_UI
participant ACC as Alice_Conversation_Cell
AUI-->>ACC: call `get_files_by_agent_by_timestamp`
ACC-->>ACC: query source chain for P2PFile elements
ACC-->>ACC: query source chain for P2PFileReceipt elements
ACC-->>ACC: initialize AgentFiles (conversant as single key)
ACC-->>ACC: initialize FileContents (empty)
ACC-->>ACC: initialize ReceiptContents (empty)
loop for every P2PFile
opt P2PFile.author == conversant or P2File.receiver == conversant
opt date_end >= P2PFile.header.timestamp >= date_start
ACC-->>ACC: add P2PFile.entry_hash to AgentFiles[P2PFile.conversant]
ACC-->>ACC: construct FileBundle(P2PFile, <empty vector>)
ACC-->>ACC: insert P2PFile.entry_hash:FileBundle to FileContents
end
end
end
loop for every P2PFileReceipt
loop for every id in P2PFileReceipt.ids
opt id in FileContents.keys
ACC-->>ACC: add P2PFileReceipt.entry_hash to the FileBundle in FileContents[P2PFileReceipt.id]
ACC-->>ACC: insert P2PFileReceipt.entry_hash:P2PFileReceipt to ReceiptContents
end
end
end
ACC-->>AUI: construct and return P2PFilesOutput
```
### `get_latest_files`
```rust=
pub struct BatchSize(u8)
fn get_latest_files(batch_size: BatchSize) -> ExternResult<P2PFilesOutput>
```
```mermaid
sequenceDiagram
participant AUI as Alice_UI
participant ACC as Alice_Conversation_Cell
AUI-->>ACC: call `get_files_by_agent_by_timestamp`
ACC-->>ACC: query source chain for P2PFile elements
ACC-->>ACC: query source chain for P2PFileReceipt elements
ACC-->>ACC: initialize AgentFiles (empty)
ACC-->>ACC: initialize FileContents (empty)
ACC-->>ACC: initialize ReceiptContents (empty)
loop for every P2PFile
opt P2PFile.author != self && P2PFile.author not in AgentFiles.keys
ACC-->>ACC: insert P2PFile.author:<empty entryhash vector> to AgentFiles
end
opt AgentFiles[P2PFile.author].length >= batch_size
ACC-->>ACC: continue with next item in loop
end
alt author == self
ACC-->>ACC: add P2PFile.entry_hash to AgentFile[P2PFile.receiver]
else author != self
ACC-->>ACC: add P2PFile.entry_hash to AgentFiles[P2PFile.author]
end
ACC-->>ACC: construct FileBundle(P2PFile, <mutable empty vector>)
ACC-->>ACC: insert P2PFile.entry_hash:FileBundle to FileContents
end
loop for every P2PFileReceipt
loop for every id in P2PFileReceipt.ids
opt id in FileContents.keys
ACC-->>ACC: add P2PFileReceipt.entry_hash to the FileBundle in FileContents[P2PFileReceipt.id]
ACC-->>ACC: insert P2PFileReceipt.entry_hash:P2PFileReceipt to ReceiptContents
end
end
end
ACC-->>AUI: construct and return P2PFilesOutput
```
### `get_next_batch_messages`
```rust=
pub struct P2PMessageFilterBatch {
conversant: AgentPubKey,
batch_size: u8,
last_fetched_timestamp: Timestamp, // header timestamp; oldest message in the last fetched message
last_fetched_file_id: EntryHash
}
fn get_next_batch_messages(filter: P2PChatFilterBatch) -> ExternResult<P2PMessageHashTables>
```
```mermaid
sequenceDiagram
participant AUI as Alice_UI
participant ACC as Alice_Conversation_Cell
AUI-->>ACC: call `get_files_by_agent_by_timestamp`
ACC-->>ACC: query source chain for P2PFile elements
ACC-->>ACC: query source chain for P2PFileReceipt elements
ACC-->>ACC: initialize AgentFiles (conversant as single key)
ACC-->>ACC: initialize FileContents (empty)
ACC-->>ACC: initialize ReceiptContents (empty)
loop for every P2PFile
opt P2PFile.header.timestamp <= last_fetched_timestamp <br> && P2PFile.entry_hash != last_fetched_file_id <br> && (P2PFile.author == conversant || P2PFile.receiver == conversant)
opt AgentFiles[conversant].length >= batch_size
ACC-->>ACC: break the loop
end
ACC-->>ACC: add P2File.entry_hash to AgentFiles[conversant]
ACC-->>ACC: construct FileBundle(P2PFile, <mutable empty vector>)
ACC-->>ACC: insert P2PFile.entry_hash:FileBundle to FileContents
end
end
loop for every P2PFileReceipt
loop for every id in P2PFileReceipt.ids
opt id in FileContents.keys
ACC-->>ACC: add P2PFileReceipt.entry_hash to the FileBundle in FileContents[P2PFileReceipt.id]
ACC-->>ACC: insert P2PFileReceipt.entry_hash:P2PFileReceipt to ReceiptContents
end
end
end
ACC-->>AUI: construct and return P2PFilesOutput
```