A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #107 from tilesprivacy/feat/device-discovery-mDNS

Local device linking with mDNS

authored by

Anandu Pavanan and committed by
GitHub
69d99535 3495b96c

+482 -79
+39 -2
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "acto" 7 + version = "0.8.0" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "148541f13c28e3e840354ee4d6c99046c10be2c81068bbd23b9e3a38f95a917e" 10 + dependencies = [ 11 + "parking_lot", 12 + "pin-project-lite", 13 + "rustc_version", 14 + "smol_str", 15 + "sync_wrapper", 16 + "tokio", 17 + "tracing", 18 + ] 19 + 20 + [[package]] 6 21 name = "adler2" 7 22 version = "2.0.1" 8 23 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3097 3112 "serde", 3098 3113 "smallvec", 3099 3114 "strum", 3115 + "swarm-discovery", 3100 3116 "sync_wrapper", 3101 3117 "time", 3102 3118 "tokio", ··· 5289 5305 5290 5306 [[package]] 5291 5307 name = "rustls-webpki" 5292 - version = "0.103.9" 5308 + version = "0.103.10" 5293 5309 source = "registry+https://github.com/rust-lang/crates.io-index" 5294 - checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" 5310 + checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" 5295 5311 dependencies = [ 5296 5312 "ring", 5297 5313 "rustls-pki-types", ··· 5731 5747 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 5732 5748 5733 5749 [[package]] 5750 + name = "smol_str" 5751 + version = "0.1.24" 5752 + source = "registry+https://github.com/rust-lang/crates.io-index" 5753 + checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" 5754 + 5755 + [[package]] 5734 5756 name = "snap" 5735 5757 version = "1.1.1" 5736 5758 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5875 5897 version = "2.6.1" 5876 5898 source = "registry+https://github.com/rust-lang/crates.io-index" 5877 5899 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 5900 + 5901 + [[package]] 5902 + name = "swarm-discovery" 5903 + version = "0.5.0" 5904 + source = "registry+https://github.com/rust-lang/crates.io-index" 5905 + checksum = "1a5ab62937edac8b23fa40e55a358ea1924245b17fc1eb20d14929c8f11be98d" 5906 + dependencies = [ 5907 + "acto", 5908 + "hickory-proto", 5909 + "rand 0.9.2", 5910 + "socket2 0.6.3", 5911 + "thiserror 2.0.18", 5912 + "tokio", 5913 + "tracing", 5914 + ] 5878 5915 5879 5916 [[package]] 5880 5917 name = "syn"
+6
tilekit/src/accounts.rs
··· 64 64 value 65 65 } 66 66 67 + pub fn get_random_bytes_32() -> [u8; 32] { 68 + let mut value = [0u8; 32]; 69 + OsRng.fill_bytes(&mut value); 70 + value 71 + } 72 + 67 73 #[cfg(test)] 68 74 mod tests { 69 75 use keyring::{mock, set_default_credential_builder};
+1 -1
tiles/Cargo.toml
··· 21 21 rusqlite_migration = "2.4.1" 22 22 uuid = {version = "1.21.0", features = ["v7"]} 23 23 axum = "0.8.8" 24 - iroh = "0.97.0" 24 + iroh = {version = "0.97.0", features = ["address-lookup-mdns"]} 25 25 iroh-ping = "0.9.0" 26 26 iroh-tickets = "0.4.0" 27 27 axum-macros = "0.5.0"
+26 -1
tiles/src/commands/mod.rs
··· 6 6 use owo_colors::OwoColorize; 7 7 use tiles::core; 8 8 use tiles::core::accounts::{ 9 - RootUser, create_root_account, get_root_user_details, save_root_account, set_nickname, 9 + RootUser, create_root_account, get_peer_list, get_root_user_details, save_root_account, 10 + set_nickname, unlink, 10 11 }; 12 + use tiles::core::storage::db::get_db_conn; 11 13 use tiles::runtime::Runtime; 12 14 use tiles::utils::config::{ 13 15 ConfigProvider, DefaultProvider, get_or_create_config, set_user_data_path, ··· 335 337 "Local Identity not created yet, use {}", 336 338 "tiles account create".yellow() 337 339 ) 340 + } 341 + 342 + pub fn show_peers() -> Result<()> { 343 + let db_conn = get_db_conn(core::storage::db::DBTYPE::COMMON)?; 344 + 345 + let peers = get_peer_list(&db_conn)?; 346 + 347 + println!("DID\tNickname\n"); 348 + for peer in peers { 349 + println!("{}\t{}", peer.user_id, peer.username) 350 + } 351 + Ok(()) 352 + } 353 + 354 + pub fn unlink_peer(user_id: &str) -> Result<()> { 355 + let db_conn = get_db_conn(core::storage::db::DBTYPE::COMMON)?; 356 + 357 + if let Err(err) = unlink(&db_conn, user_id) { 358 + println!("{:?}", err) 359 + } else { 360 + println!("Succesfully disabled the peer") 361 + } 362 + Ok(()) 338 363 } 339 364 340 365 #[cfg(test)]
+138 -1
tiles/src/core/accounts.rs
··· 50 50 let value_lower = value.to_lowercase(); 51 51 match value_lower.as_str() { 52 52 "local" => Ok(ACCOUNT::LOCAL), 53 + "self" => Ok(ACCOUNT::SELF), 53 54 _ => Err(AccountError { 54 55 error: "Invalid account type".to_owned(), 55 56 }), ··· 225 226 .map_err(<rusqlite::Error as Into<anyhow::Error>>::into) 226 227 } 227 228 229 + pub fn get_user(conn: &Connection, did: &str) -> Result<User> { 230 + let mut fetch_current_user = conn.prepare("select id, user_id, username, account_type, active_profile, root, created_at, updated_at from users where user_id= ?1")?; 231 + 232 + fetch_current_user 233 + .query_one([did], |row| { 234 + let id: String = row.get(0)?; 235 + let account_type: String = row.get(3)?; 236 + let created_at: f64 = row.get(6)?; 237 + let updated_at: f64 = row.get(7)?; 238 + Ok(User { 239 + id: Uuid::try_parse(&id).map_err(FromSqlError::other)?, 240 + user_id: row.get(1)?, 241 + username: row.get(2)?, 242 + account_type: ACCOUNT::try_from(account_type).map_err(FromSqlError::other)?, 243 + active_profile: row.get(4)?, 244 + root: row.get(5)?, 245 + 246 + created_at: created_at as u64, 247 + updated_at: updated_at as u64, 248 + }) 249 + }) 250 + .map_err(<rusqlite::Error as Into<anyhow::Error>>::into) 251 + } 252 + 228 253 pub fn save_root_account_db() -> Result<()> { 229 254 let conn = get_db_conn(DBTYPE::COMMON)?; 230 255 let config = get_or_create_config()?; ··· 294 319 Ok(()) 295 320 } 296 321 297 - pub fn get_user_by_user_id(conn: &Connection, user_id: &str) -> Result<()> { 322 + pub fn get_user_by_user_id(conn: &Connection, user_id: String) -> Result<()> { 298 323 let mut fetch_root_user = conn.prepare("select id from users where user_id = ?1")?; 299 324 300 325 match fetch_root_user.query_one([user_id], |_row| Ok(())) { ··· 318 343 } 319 344 } 320 345 346 + pub fn get_peer_list(db_conn: &Connection) -> Result<Vec<User>> { 347 + let mut stmt= db_conn.prepare("select id, user_id, username, account_type, active_profile, root, created_at, updated_at from users where account_type != \'local\'")?; 348 + 349 + let user_rows = stmt 350 + .query_map([], |row| { 351 + let id: String = row.get(0)?; 352 + let account_type: String = row.get(3)?; 353 + let created_at: f64 = row.get(6)?; 354 + let updated_at: f64 = row.get(7)?; 355 + Ok(User { 356 + id: Uuid::try_parse(&id).map_err(FromSqlError::other)?, 357 + user_id: row.get(1)?, 358 + username: row.get(2)?, 359 + account_type: ACCOUNT::try_from(account_type).map_err(FromSqlError::other)?, 360 + active_profile: row.get(4)?, 361 + root: row.get(5)?, 362 + 363 + created_at: created_at as u64, 364 + updated_at: updated_at as u64, 365 + }) 366 + }) 367 + .map_err(<rusqlite::Error as Into<anyhow::Error>>::into)?; 368 + 369 + let mut peer_list: Vec<User> = vec![]; 370 + 371 + for peer in user_rows { 372 + peer_list.push(peer?); 373 + } 374 + 375 + Ok(peer_list) 376 + } 377 + 378 + pub fn unlink(db_conn: &Connection, user_id: &str) -> Result<()> { 379 + let user = get_current_user(db_conn)?; 380 + if user.user_id == user_id { 381 + return Err(anyhow!("Cannot unlink yourself")); 382 + } 383 + 384 + match db_conn.execute( 385 + "delete from users where user_id = ?1 and account_type != \'local\'", 386 + [user_id], 387 + ) { 388 + Ok(_res) => Ok(()), 389 + Err(err) => Err(anyhow!("Unable to unlink the peer due to {:?}", err)), 390 + } 391 + } 392 + 321 393 #[cfg(test)] 322 394 mod tests { 323 395 use super::*; ··· 665 737 .unwrap(); 666 738 667 739 assert!(get_current_user(&conn).is_err()); 740 + } 741 + 742 + fn create_user(conn: &Connection, account_type: ACCOUNT) -> User { 743 + let user = User { 744 + id: Uuid::now_v7(), 745 + user_id: String::from("did"), 746 + username: String::from("nickname"), 747 + account_type, 748 + active_profile: true, 749 + root: true, 750 + created_at: SystemTime::now() 751 + .duration_since(UNIX_EPOCH) 752 + .expect("time went backwards") 753 + .as_secs(), 754 + updated_at: SystemTime::now() 755 + .duration_since(UNIX_EPOCH) 756 + .expect("time went backwards") 757 + .as_secs(), 758 + }; 759 + 760 + conn.execute("insert into users (id, user_id, username, active_profile, account_type, root) values (?1, ?2, ?3,?4, ?5, ?6)", (&user.id.to_string(), &user.user_id, &user.username, &user.active_profile, 761 + user.account_type.to_string(), &user.root)).unwrap(); 762 + user 763 + } 764 + 765 + #[test] 766 + fn test_list_peers_with_atleast_0_peer() { 767 + let conn = setup_db_schema(); 768 + let _local_user = create_user(&conn, ACCOUNT::LOCAL); 769 + 770 + let user_list = get_peer_list(&conn).unwrap(); 771 + 772 + assert!(user_list.is_empty()) 773 + } 774 + 775 + #[test] 776 + fn test_list_peers_with_more_than_0_peer() { 777 + let conn = setup_db_schema(); 778 + let _local_user = create_user(&conn, ACCOUNT::LOCAL); 779 + save_self_account_db(&conn, "varathan", "did:jey:varathan").unwrap(); 780 + let user_list = get_peer_list(&conn).unwrap(); 781 + 782 + assert!(!user_list.is_empty()) 783 + } 784 + 785 + #[test] 786 + fn test_unlink_valid_peer() { 787 + let conn = setup_db_schema(); 788 + let _local_user = create_user(&conn, ACCOUNT::LOCAL); 789 + save_self_account_db(&conn, "did:jey:varathan", "varathan").unwrap(); 790 + let user_list = get_peer_list(&conn).unwrap(); 791 + 792 + assert!(!user_list.is_empty()); 793 + 794 + unlink(&conn, "did:jey:varathan").unwrap(); 795 + let user_list = get_peer_list(&conn).unwrap(); 796 + assert!(user_list.is_empty()); 797 + } 798 + 799 + #[test] 800 + fn test_try_unlink_local() { 801 + let conn = setup_db_schema(); 802 + let local_user = create_user(&conn, ACCOUNT::LOCAL); 803 + 804 + assert!(unlink(&conn, &local_user.user_id).is_err()) 668 805 } 669 806 }
+239 -66
tiles/src/core/network/mod.rs
··· 5 5 io, 6 6 str::FromStr, 7 7 sync::{Arc, Mutex}, 8 + time::Duration, 8 9 }; 9 10 10 11 use anyhow::Result; 11 - use futures_util::TryStreamExt; 12 + use futures_util::{StreamExt, TryStreamExt}; 12 13 use iroh::{ 13 - Endpoint, EndpointId, SecretKey, 14 + Endpoint, EndpointId, NET_REPORT_TIMEOUT, SecretKey, 15 + address_lookup::{self, MdnsAddressLookup, mdns}, 14 16 endpoint::{BindError, presets}, 17 + endpoint_info::UserData, 15 18 protocol::Router, 16 19 }; 17 20 use iroh_gossip::{ ··· 23 26 use rusqlite::Connection; 24 27 use tilekit::accounts::{get_did_from_public_key, get_random_bytes, get_secret_key}; 25 28 use tokio::task::spawn_blocking; 29 + use uuid::Uuid; 26 30 27 31 use crate::core::{ 28 - accounts::{self, get_current_user, get_user_by_user_id, save_self_account_db}, 32 + accounts::{self, get_current_user, save_self_account_db}, 29 33 network::ticket::LinkTicket, 30 34 storage::db::{DBTYPE, get_db_conn}, 31 35 }; 32 36 use sha2::{Digest, Sha256}; 33 37 38 + const DEVICE_LINK_TOPIC: &str = "com.tilesprivacy.tiles.link"; 34 39 #[derive(serde::Serialize, serde::Deserialize)] 35 40 struct NetworkMessage { 36 41 body: MessageBody, ··· 57 62 #[derive(serde::Serialize, serde::Deserialize)] 58 63 #[allow(clippy::enum_variant_names)] 59 64 enum MessageBody { 60 - LinkRequest { did: String, nickname: String }, 61 - LinkAccepted { did: String, nickname: String }, 62 - LinkRejected { did: String, nickname: String }, 65 + LinkRequest { 66 + did: String, 67 + nickname: String, 68 + is_online: bool, 69 + ticket: String, 70 + }, 71 + LinkAccepted { 72 + did: String, 73 + nickname: String, 74 + is_online: bool, 75 + }, 76 + LinkRejected { 77 + did: String, 78 + nickname: String, 79 + is_online: bool, 80 + reason: String, 81 + }, 63 82 } 64 83 65 84 // Entrypoint of network connection ··· 102 121 pub async fn link(ticket: Option<String>) -> Result<()> { 103 122 let user_db_conn = get_db_conn(DBTYPE::COMMON)?; 104 123 let user = get_current_user(&user_db_conn)?; 124 + let endpoint = create_endpoint(&user).await?; 125 + let is_online = is_online(&endpoint).await; 126 + let mut bootstrap_ids: Vec<EndpointId> = vec![]; 127 + // if ticket's there, then this is link enable sender's command, e;se receiver end 105 128 if let Some(ticket) = ticket { 106 - let link_ticket = LinkTicket::from_str(&ticket)?; 107 - if get_user_by_user_id(&user_db_conn, &link_ticket.did).is_ok() { 108 - println!( 109 - "Device {}({}) already linked", 110 - link_ticket.nickname, link_ticket.did 111 - ); 112 - return Ok(()); 113 - } 114 - let endpoint = create_endpoint(&user).await?; 115 - endpoint.online().await; 116 - let gossip = Gossip::builder().spawn(endpoint.clone()); 129 + let (endpoint_id, mut did, mut nickname) = parse_link_ticket(&ticket)?; 130 + 131 + // comments while doing same machine testing 132 + 133 + // if get_user_by_user_id(&user_db_conn, &link_ticket.did).is_ok() { 134 + // println!( 135 + // "Device {}({}) already linked", 136 + // link_ticket.nickname, link_ticket.did 137 + // ); 138 + // return Ok(()); 139 + // } 117 140 118 - let recv_router = Router::builder(endpoint.clone()) 119 - .accept(iroh_gossip::ALPN, gossip.clone()) 120 - .spawn(); 141 + let topic_id = create_topic_id(DEVICE_LINK_TOPIC); 142 + 143 + if is_online { 144 + bootstrap_ids.push(endpoint_id.expect("Expected an EndpointId as bootstrapId ")) 145 + } else { 146 + println!("Searching for peers in the local network.."); 147 + let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?; 148 + let (new_bootstrap_ids, user_data) = 149 + find_offline_bootstrap_peers(&endpoint, mdns).await?; 150 + bootstrap_ids = new_bootstrap_ids; 151 + let user_data_str = user_data.to_string(); 152 + let metadata_list = user_data_str.split(',').collect::<Vec<&str>>(); 153 + did = metadata_list[0].to_owned(); 154 + nickname = metadata_list[1].to_owned(); 155 + }; 156 + 157 + let (sender, mut receiver, recv_router) = 158 + create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?; 121 159 122 - let (sender, mut receiver) = gossip 123 - .subscribe(link_ticket.topic_id, vec![link_ticket.addr.id]) 124 - .await? 125 - .split(); 160 + println!("\nConnecting to {}({}).....", nickname, did); 126 161 127 - println!( 128 - "Connecting to {}({}).....", 129 - link_ticket.nickname, link_ticket.did 130 - ); 131 162 receiver.joined().await?; 163 + 132 164 tokio::spawn(subsribe_loop( 133 165 receiver, 134 166 sender.clone(), 135 167 user.clone(), 136 168 user_db_conn, 169 + None, 137 170 )); 138 171 139 172 let link_req_msg = NetworkMessage::new(MessageBody::LinkRequest { 140 173 did: user.user_id, 141 174 nickname: user.username, 175 + is_online, 176 + ticket, 142 177 }); 143 178 sender.broadcast(link_req_msg.to_bytes().into()).await?; 144 179 145 - println!( 146 - "Sent link request to {}({})", 147 - link_ticket.nickname, link_ticket.did 148 - ); 180 + println!("\nSent link request to {}({})", nickname, did); 149 181 150 - println!("Waiting for response..."); 182 + println!("\nWaiting for response..."); 183 + 151 184 tokio::signal::ctrl_c().await?; 152 185 recv_router.shutdown().await?; 153 - endpoint.close().await; 154 186 } else { 155 - let endpoint = create_endpoint(&user).await?; 156 - endpoint.online().await; 187 + if !is_online { 188 + let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?; 189 + endpoint.address_lookup()?.add(mdns.clone()); 190 + } 157 191 158 - let gossip = Gossip::builder().spawn(endpoint.clone()); 192 + let topic_id = create_topic_id(DEVICE_LINK_TOPIC); 159 193 160 - let recv_router = Router::builder(endpoint.clone()) 161 - .accept(iroh_gossip::ALPN, gossip.clone()) 162 - .spawn(); 194 + let (sender, receiver, recv_router) = 195 + create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?; 163 196 164 - let topic_id = create_topic_id("com.tilesprivacy.tiles.link"); 197 + let generated_ticket = if is_online { 198 + let ticket = LinkTicket::new( 199 + topic_id, 200 + endpoint.addr(), 201 + user.user_id.clone(), 202 + user.username.clone(), 203 + ); 204 + println!("Generated link ticket: \n{:?}\n", ticket.to_string()); 165 205 166 - let (sender, receiver) = gossip.subscribe(topic_id, vec![]).await?.split(); 206 + println!( 207 + "Use this ticket with `tiles link enable <ticket>` on the system you want to connect to\n" 208 + ); 209 + ticket.to_string() 210 + } else { 211 + // generate a code 212 + let uuid = Uuid::new_v4().to_string(); 167 213 168 - let ticket = LinkTicket::new( 169 - topic_id, 170 - endpoint.addr(), 171 - user.user_id.clone(), 172 - user.username.clone(), 173 - ); 214 + let ticket = uuid.split('-').collect::<Vec<&str>>()[0]; 215 + 216 + println!("Generated link code: {}\n", ticket); 174 217 175 - println!("Link Ticket: {:?}\n", ticket.to_string()); 176 - println!( 177 - "Use this link ticket with `tiles link <ticket>` on the system you want to connect to\n" 178 - ); 218 + println!( 219 + "Use this link code with `tiles link enable {}` on the system you want to connect to\n", 220 + ticket 221 + ); 222 + ticket.to_string() 223 + }; 179 224 180 225 println!("Don't close this session until the link process is done\n"); 181 226 ··· 184 229 sender.clone(), 185 230 user.clone(), 186 231 user_db_conn, 232 + Some(generated_ticket), 187 233 )); 188 234 189 235 // TODO: Maybe a better way is to use a oneshot channel to exit 190 236 // the terminal instead of SIGINT 191 237 tokio::signal::ctrl_c().await?; 192 238 recv_router.shutdown().await?; 193 - endpoint.close().await; 194 239 } 240 + endpoint.close().await; 195 241 Ok(()) 196 242 } 197 243 ··· 200 246 sender: GossipSender, 201 247 user: accounts::User, 202 248 db_conn: Connection, 249 + generated_ticket: Option<String>, 203 250 ) -> Result<()> { 204 251 while let Some(event) = receiver.try_next().await? { 205 - // println!("some event {:?}", event); 252 + if cfg!(debug_assertions) { 253 + println!("In {}:, some event {:?}", user.username, event); 254 + } 206 255 if let Event::Received(msg) = event { 207 256 match NetworkMessage::from_bytes(&msg.content)?.body { 208 - MessageBody::LinkRequest { did, nickname } => { 257 + MessageBody::LinkRequest { 258 + did, 259 + nickname, 260 + is_online, 261 + ticket, 262 + } => { 209 263 println!( 210 264 "Received link request from {}({}), Do you want to link Y/N ?", 211 265 nickname, did ··· 222 276 let input_resp = input.lock().unwrap().trim().to_owned(); 223 277 224 278 let link_res_resp = if input_resp.to_lowercase() == "y" { 225 - save_self_account_db(&db_conn, &did, &nickname)?; 279 + if let Some(gen_ticket) = &generated_ticket 280 + && !is_online 281 + && *gen_ticket != ticket.to_lowercase() 282 + { 283 + println!("\nVerifying code does not match, please try again"); 284 + let response = NetworkMessage::new(MessageBody::LinkRejected { 285 + did: user.user_id.clone(), 286 + nickname: user.username.clone(), 287 + is_online, 288 + reason: String::from("Link code mismatch"), 289 + }); 290 + sender.broadcast(response.to_bytes().into()).await?; 291 + continue; 292 + } 293 + 294 + if let Err(err) = save_self_account_db(&db_conn, &did, &nickname) { 295 + println!("Failed to add the peer locally due to {:?}", err); 296 + 297 + continue; 298 + } 299 + 226 300 println!( 227 301 "Device {}({}) is now linked\nYou can exit now by ctrl-c", 228 302 nickname, did ··· 230 304 NetworkMessage::new(MessageBody::LinkAccepted { 231 305 did: user.user_id.clone(), 232 306 nickname: user.username.clone(), 307 + is_online, 233 308 }) 234 309 } else { 235 310 println!("You can exit now by ctrl-c"); 236 311 NetworkMessage::new(MessageBody::LinkRejected { 237 312 did: user.user_id.clone(), 238 313 nickname: user.username.clone(), 314 + is_online, 315 + reason: String::from("Peer rejected the request"), 239 316 }) 240 317 }; 241 318 input.lock().unwrap().clear(); 242 319 243 320 sender.broadcast(link_res_resp.to_bytes().into()).await?; 244 321 } 245 - MessageBody::LinkAccepted { did, nickname } => { 246 - save_self_account_db(&db_conn, &did, &nickname)?; 247 - println!("Link accepted by {}({})", nickname, did); 322 + MessageBody::LinkAccepted { 323 + did, 324 + nickname, 325 + is_online: _, 326 + } => { 327 + println!("\nLink accepted by {}({})", nickname, did); 328 + 329 + if let Err(err) = save_self_account_db(&db_conn, &did, &nickname) { 330 + println!("Failed to add the peer locally due to {:?}", err); 331 + return Ok(()); 332 + } 248 333 249 - println!("You can exit now by ctrl-c"); 334 + println!("\nYou can exit now by ctrl-c"); 250 335 251 - return Ok(()); 336 + continue; 252 337 } 253 - MessageBody::LinkRejected { did, nickname } => { 338 + MessageBody::LinkRejected { 339 + did, 340 + nickname, 341 + is_online: _, 342 + reason, 343 + } => { 254 344 println!( 255 - "Oops looks like your link request has been rejected by {}({}), exit (ctrl-c) and try again", 256 - nickname, did 345 + "Oops looks like your link request has been rejected by {}({}),\nreason: {},\nexit (ctrl-c) and try again", 346 + nickname, did, reason 257 347 ); 258 348 } 259 349 } ··· 269 359 let signing_key = get_secret_key("tiles", &user.user_id)?; 270 360 271 361 let secret_key = SecretKey::from_bytes(&signing_key); 272 - 273 362 Endpoint::builder(presets::N0) 363 + .user_data_for_address_lookup(UserData::try_from(format!( 364 + "{},{}", 365 + user.user_id, user.username 366 + ))?) 274 367 .secret_key(secret_key) 275 368 .bind() 276 369 .await 277 370 .map_err(<BindError as Into<anyhow::Error>>::into) 278 371 } else { 279 - Endpoint::bind(presets::N0) 372 + Endpoint::builder(presets::N0) 373 + .user_data_for_address_lookup(UserData::try_from(format!( 374 + "{},{}", 375 + user.user_id, user.username 376 + ))?) 377 + .bind() 280 378 .await 281 379 .map_err(<BindError as Into<anyhow::Error>>::into) 282 380 } ··· 293 391 get_did_from_public_key(endpoint_id.as_bytes()) 294 392 } 295 393 394 + async fn is_online(endpoint: &Endpoint) -> bool { 395 + tokio::select! { 396 + _ = endpoint.online() => { 397 + true 398 + } 399 + _ = tokio::time::sleep(Duration::from_secs(NET_REPORT_TIMEOUT)) => { 400 + false 401 + } 402 + } 403 + } 404 + 405 + // As of now we exit asap when we see a peer. This is subjected to change 406 + // as the scale 407 + async fn find_offline_bootstrap_peers( 408 + endpoint: &Endpoint, 409 + mdns: MdnsAddressLookup, 410 + ) -> Result<(Vec<EndpointId>, UserData)> { 411 + let mut bootstrap_ids: Vec<EndpointId> = vec![]; 412 + endpoint.address_lookup()?.add(mdns.clone()); 413 + let mut mdns_event = mdns.subscribe().await; 414 + let mut user_data = UserData::from_str("")?; 415 + while let Some(event) = mdns_event.next().await { 416 + match event { 417 + mdns::DiscoveryEvent::Discovered { 418 + endpoint_info, 419 + last_updated: _, 420 + } => { 421 + if cfg!(debug_assertions) { 422 + println!("peer discoverd {:?}", endpoint_info); 423 + } 424 + bootstrap_ids.push(endpoint_info.endpoint_id); 425 + user_data = endpoint_info.user_data().unwrap().clone(); 426 + break; 427 + } 428 + mdns::DiscoveryEvent::Expired { endpoint_id } => { 429 + if cfg!(debug_assertions) { 430 + println!("peer left {:?}", endpoint_id) 431 + } 432 + } 433 + } 434 + } 435 + 436 + Ok((bootstrap_ids, user_data)) 437 + } 438 + 439 + async fn create_gossip_network( 440 + endpoint: &Endpoint, 441 + topic_id: TopicId, 442 + bootstrap_ids: Vec<iroh::PublicKey>, 443 + ) -> Result<(GossipSender, GossipReceiver, Router)> { 444 + let gossip = Gossip::builder().spawn(endpoint.clone()); 445 + let recv_router = Router::builder(endpoint.clone()) 446 + .accept(iroh_gossip::ALPN, gossip.clone()) 447 + .spawn(); 448 + 449 + let (goss_sender, goss_receiver) = gossip.subscribe(topic_id, bootstrap_ids).await?.split(); 450 + 451 + Ok((goss_sender, goss_receiver, recv_router)) 452 + } 453 + 454 + // We handle the parsing in this way since ticket can be an encoded `LinkTicket` 455 + // or just a 5 byte hex if linking over mDNS 456 + fn parse_link_ticket(ticket: &str) -> Result<(Option<EndpointId>, String, String)> { 457 + if let Ok(parsed_ticket) = LinkTicket::from_str(ticket) { 458 + Ok(( 459 + Some(parsed_ticket.addr.id), 460 + parsed_ticket.did, 461 + parsed_ticket.nickname, 462 + )) 463 + } else { 464 + Ok((None, String::from(""), String::from(""))) 465 + } 466 + } 467 + 468 + // fn subsribe_mdns_events(mdns_events) {} 296 469 //TODO: Add tests, can we get some from iroh reference?
+33 -8
tiles/src/main.rs
··· 8 8 utils::installer, 9 9 }; 10 10 11 + use crate::commands::{show_peers, unlink_peer}; 12 + 11 13 mod commands; 12 14 #[derive(Debug, Parser)] 13 15 #[command(name = "tiles")] ··· 63 65 Daemon(DaemonArgs), 64 66 65 67 /// Link with other devices p2p 66 - Link { 67 - /// The ticket from a peer which can be used to link to it 68 - ticket: Option<String>, 69 - }, 68 + Link(LinkArgs), 70 69 } 71 70 72 71 #[derive(Debug, Args)] ··· 150 149 /// Stops the daemon 151 150 Stop, 152 151 } 152 + 153 + #[derive(Debug, Args)] 154 + #[command(args_conflicts_with_subcommands = true)] 155 + #[command(flatten_help = true)] 156 + struct LinkArgs { 157 + #[command(subcommand)] 158 + command: LinkCommands, 159 + } 160 + 161 + #[derive(Debug, Subcommand)] 162 + enum LinkCommands { 163 + /// Produce link ticket and wait or send link request with ticket 164 + Enable { 165 + ticket: Option<String>, 166 + }, 167 + 168 + // Unlink give device 169 + Disable { 170 + did: String, 171 + }, 172 + /// Start the daemon 173 + ListPeers, 174 + } 153 175 #[tokio::main] 154 176 pub async fn main() -> Result<(), Box<dyn Error>> { 155 177 let cli = Cli::parse(); ··· 233 255 .inspect(|_| println!("Daemon stopped successfully"))?, 234 256 _ => start_server(None).await?, 235 257 }, 236 - Some(Commands::Link { ticket }) => { 237 - // TODO: Move these direct call to core 238 - link(ticket).await? 239 - } 258 + Some(Commands::Link(link_args)) => match link_args.command { 259 + LinkCommands::Enable { ticket } => link(ticket).await?, 260 + LinkCommands::Disable { did } => unlink_peer(&did)?, 261 + LinkCommands::ListPeers => { 262 + show_peers()?; 263 + } 264 + }, 240 265 } 241 266 Ok(()) 242 267 }