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.

feat: mDNS peer discovery with short codes + more link utility commands

- Added mDNS peer discovery with hex shortcode of 8 digit
- Added link utility commands such as link disable <DID> and link list-peers
- Refactored such that linking with tickets and codes coexist

madclaws 75613c5f 5b1d846b

+433 -120
+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};
+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 }
+230 -110
tiles/src/core/network/mod.rs
··· 9 9 }; 10 10 11 11 use anyhow::Result; 12 - use futures_util::{Stream, StreamExt, TryStreamExt}; 12 + use futures_util::{StreamExt, TryStreamExt}; 13 13 use iroh::{ 14 14 Endpoint, EndpointId, NET_REPORT_TIMEOUT, SecretKey, 15 - address_lookup::{self, mdns}, 15 + address_lookup::{self, MdnsAddressLookup, mdns}, 16 16 endpoint::{BindError, presets}, 17 + endpoint_info::UserData, 17 18 protocol::Router, 18 19 }; 19 20 use iroh_gossip::{ ··· 25 26 use rusqlite::Connection; 26 27 use tilekit::accounts::{get_did_from_public_key, get_random_bytes, get_secret_key}; 27 28 use tokio::task::spawn_blocking; 29 + use uuid::Uuid; 28 30 29 31 use crate::core::{ 30 - accounts::{self, get_current_user, get_user_by_user_id, save_self_account_db}, 32 + accounts::{self, get_current_user, save_self_account_db}, 31 33 network::ticket::LinkTicket, 32 34 storage::db::{DBTYPE, get_db_conn}, 33 35 }; 34 36 use sha2::{Digest, Sha256}; 35 37 38 + const DEVICE_LINK_TOPIC: &str = "com.tilesprivacy.tiles.link"; 36 39 #[derive(serde::Serialize, serde::Deserialize)] 37 40 struct NetworkMessage { 38 41 body: MessageBody, ··· 59 62 #[derive(serde::Serialize, serde::Deserialize)] 60 63 #[allow(clippy::enum_variant_names)] 61 64 enum MessageBody { 62 - LinkRequest { did: String, nickname: String }, 63 - LinkAccepted { did: String, nickname: String }, 64 - 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 + }, 65 82 } 66 83 67 84 // Entrypoint of network connection ··· 104 121 pub async fn link(ticket: Option<String>) -> Result<()> { 105 122 let user_db_conn = get_db_conn(DBTYPE::COMMON)?; 106 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 107 128 if let Some(ticket) = ticket { 108 - let link_ticket = LinkTicket::from_str(&ticket)?; 109 - if get_user_by_user_id(&user_db_conn, &link_ticket.did).is_ok() { 110 - println!( 111 - "Device {}({}) already linked", 112 - link_ticket.nickname, link_ticket.did 113 - ); 114 - return Ok(()); 115 - } 116 - let endpoint = create_endpoint(&user).await?; 117 - let mut is_online = false; 118 - tokio::select! { 119 - _ = endpoint.online() => { 120 - is_online = true; 121 - println!("Yep online") 122 - } 123 - _ = tokio::time::sleep(Duration::from_secs(NET_REPORT_TIMEOUT)) => { 124 - is_online = false; 125 - println!("ur offline") 126 - } 127 - }; 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 + // } 128 140 129 - let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?; 141 + let topic_id = create_topic_id(DEVICE_LINK_TOPIC); 130 142 131 - if !is_online { 132 - endpoint.address_lookup()?.add(mdns.clone()); 133 - // let mut mdns_event = mdns.subscribe().await; 134 - // while let Some(event) = mdns_event.next().await { 135 - // match event { 136 - // mdns::DiscoveryEvent::Discovered { 137 - // endpoint_info, 138 - // last_updated, 139 - // } => { 140 - // println!("peer discoverd {:?}", endpoint_info) 141 - // } 142 - // mdns::DiscoveryEvent::Expired { endpoint_id } => { 143 - // println!("peer left {:?}", endpoint_id) 144 - // } 145 - // } 146 - // } 147 - // mdns_event 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(); 148 155 }; 149 - let gossip = Gossip::builder().spawn(endpoint.clone()); 150 156 151 - let recv_router = Router::builder(endpoint.clone()) 152 - .accept(iroh_gossip::ALPN, gossip.clone()) 153 - .spawn(); 157 + let (sender, mut receiver, recv_router) = 158 + create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?; 154 159 155 - let (sender, mut receiver) = gossip 156 - .subscribe(link_ticket.topic_id, vec![link_ticket.addr.id]) 157 - .await? 158 - .split(); 160 + println!("\nConnecting to {}({}).....", nickname, did); 159 161 160 - println!( 161 - "Connecting to {}({}).....", 162 - link_ticket.nickname, link_ticket.did 163 - ); 164 162 receiver.joined().await?; 163 + 165 164 tokio::spawn(subsribe_loop( 166 165 receiver, 167 166 sender.clone(), 168 167 user.clone(), 169 168 user_db_conn, 169 + None, 170 170 )); 171 171 172 172 let link_req_msg = NetworkMessage::new(MessageBody::LinkRequest { 173 173 did: user.user_id, 174 174 nickname: user.username, 175 + is_online, 176 + ticket, 175 177 }); 176 178 sender.broadcast(link_req_msg.to_bytes().into()).await?; 177 179 178 - println!( 179 - "Sent link request to {}({})", 180 - link_ticket.nickname, link_ticket.did 181 - ); 180 + println!("\nSent link request to {}({})", nickname, did); 181 + 182 + println!("\nWaiting for response..."); 182 183 183 - println!("Waiting for response..."); 184 184 tokio::signal::ctrl_c().await?; 185 185 recv_router.shutdown().await?; 186 - endpoint.close().await; 187 186 } else { 188 - let endpoint = create_endpoint(&user).await?; 189 - let mut is_online = false; 190 - tokio::select! { 191 - _ = endpoint.online() => { 192 - is_online = true; 193 - println!("Yep online") 194 - } 195 - _ = tokio::time::sleep(Duration::from_secs(NET_REPORT_TIMEOUT)) => { 196 - is_online = false; 197 - println!("ur offline") 198 - } 199 - }; 200 - 201 - let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?; 202 - 203 187 if !is_online { 188 + let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?; 204 189 endpoint.address_lookup()?.add(mdns.clone()); 205 190 } 206 - let gossip = Gossip::builder().spawn(endpoint.clone()); 207 191 208 - let recv_router = Router::builder(endpoint.clone()) 209 - .accept(iroh_gossip::ALPN, gossip.clone()) 210 - .spawn(); 192 + let topic_id = create_topic_id(DEVICE_LINK_TOPIC); 211 193 212 - let topic_id = create_topic_id("com.tilesprivacy.tiles.link"); 194 + let (sender, receiver, recv_router) = 195 + create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?; 213 196 214 - let (sender, receiver) = gossip.subscribe(topic_id, vec![]).await?.split(); 215 - let ticket = LinkTicket::new( 216 - topic_id, 217 - endpoint.addr(), 218 - user.user_id.clone(), 219 - user.username.clone(), 220 - ); 221 - println!("{:?}", endpoint.addr()); 222 - println!("Link Ticket: {:?}\n", ticket.to_string()); 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()); 223 205 224 - println!( 225 - "Use this link ticket with `tiles link <ticket>` on the system you want to connect to\n" 226 - ); 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(); 213 + 214 + let ticket = uuid.split('-').collect::<Vec<&str>>()[0]; 215 + 216 + println!("Generated link code: {}\n", ticket); 217 + 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 + }; 227 224 228 225 println!("Don't close this session until the link process is done\n"); 229 226 ··· 232 229 sender.clone(), 233 230 user.clone(), 234 231 user_db_conn, 232 + Some(generated_ticket), 235 233 )); 236 234 237 235 // TODO: Maybe a better way is to use a oneshot channel to exit 238 236 // the terminal instead of SIGINT 239 237 tokio::signal::ctrl_c().await?; 240 238 recv_router.shutdown().await?; 241 - endpoint.close().await; 242 239 } 240 + endpoint.close().await; 243 241 Ok(()) 244 242 } 245 243 ··· 248 246 sender: GossipSender, 249 247 user: accounts::User, 250 248 db_conn: Connection, 249 + generated_ticket: Option<String>, 251 250 ) -> Result<()> { 252 251 while let Some(event) = receiver.try_next().await? { 253 - // println!("some event {:?}", event); 252 + if cfg!(debug_assertions) { 253 + println!("In {}:, some event {:?}", user.username, event); 254 + } 254 255 if let Event::Received(msg) = event { 255 256 match NetworkMessage::from_bytes(&msg.content)?.body { 256 - MessageBody::LinkRequest { did, nickname } => { 257 + MessageBody::LinkRequest { 258 + did, 259 + nickname, 260 + is_online, 261 + ticket, 262 + } => { 257 263 println!( 258 264 "Received link request from {}({}), Do you want to link Y/N ?", 259 265 nickname, did ··· 270 276 let input_resp = input.lock().unwrap().trim().to_owned(); 271 277 272 278 let link_res_resp = if input_resp.to_lowercase() == "y" { 273 - 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 + 274 300 println!( 275 301 "Device {}({}) is now linked\nYou can exit now by ctrl-c", 276 302 nickname, did ··· 278 304 NetworkMessage::new(MessageBody::LinkAccepted { 279 305 did: user.user_id.clone(), 280 306 nickname: user.username.clone(), 307 + is_online, 281 308 }) 282 309 } else { 283 310 println!("You can exit now by ctrl-c"); 284 311 NetworkMessage::new(MessageBody::LinkRejected { 285 312 did: user.user_id.clone(), 286 313 nickname: user.username.clone(), 314 + is_online, 315 + reason: String::from("Peer rejected the request"), 287 316 }) 288 317 }; 289 318 input.lock().unwrap().clear(); 290 319 291 320 sender.broadcast(link_res_resp.to_bytes().into()).await?; 292 321 } 293 - MessageBody::LinkAccepted { did, nickname } => { 294 - save_self_account_db(&db_conn, &did, &nickname)?; 295 - println!("Link accepted by {}({})", nickname, did); 322 + MessageBody::LinkAccepted { 323 + did, 324 + nickname, 325 + is_online: _, 326 + } => { 327 + println!("\nLink accepted by {}({})", nickname, did); 296 328 297 - println!("You can exit now by ctrl-c"); 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 + } 333 + 334 + println!("\nYou can exit now by ctrl-c"); 298 335 299 - return Ok(()); 336 + continue; 300 337 } 301 - MessageBody::LinkRejected { did, nickname } => { 338 + MessageBody::LinkRejected { 339 + did, 340 + nickname, 341 + is_online: _, 342 + reason, 343 + } => { 302 344 println!( 303 - "Oops looks like your link request has been rejected by {}({}), exit (ctrl-c) and try again", 304 - nickname, did 345 + "Oops looks like your link request has been rejected by {}({}),\nreason: {},\nexit (ctrl-c) and try again", 346 + nickname, did, reason 305 347 ); 306 348 } 307 349 } ··· 317 359 let signing_key = get_secret_key("tiles", &user.user_id)?; 318 360 319 361 let secret_key = SecretKey::from_bytes(&signing_key); 320 - let mdns = address_lookup::mdns::MdnsAddressLookup::builder(); 321 362 Endpoint::builder(presets::N0) 322 - // .address_lookup(mdns) 363 + .user_data_for_address_lookup(UserData::try_from(format!( 364 + "{},{}", 365 + user.user_id, user.username 366 + ))?) 323 367 .secret_key(secret_key) 324 368 .bind() 325 369 .await 326 370 .map_err(<BindError as Into<anyhow::Error>>::into) 327 371 } else { 328 - let mdns = address_lookup::mdns::MdnsAddressLookup::builder(); 329 372 Endpoint::builder(presets::N0) 330 - // .address_lookup(mdns) 373 + .user_data_for_address_lookup(UserData::try_from(format!( 374 + "{},{}", 375 + user.user_id, user.username 376 + ))?) 331 377 .bind() 332 378 .await 333 379 .map_err(<BindError as Into<anyhow::Error>>::into) ··· 343 389 344 390 fn _get_did_from_endpoint(endpoint_id: EndpointId) -> Result<String> { 345 391 get_did_from_public_key(endpoint_id.as_bytes()) 392 + } 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 + } 346 466 } 347 467 348 468 // fn subsribe_mdns_events(mdns_events) {}
+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 }