# 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 ```