···5566## [Unreleased]
7788+## [0.4.5] - 2026-03-23
99+1010+### Added
1111+- Added P2P device linking v1 in [#106](https://github.com/tilesprivacy/tiles/pull/106).
1212+ - Works both online and in offline networks
1313+ - Utility Commands for device linking
1414+ - `tiles link enable` - creates the ticket and listens for an link requests
1515+ - `tiles link enable <ticket>`- Device that need to join will run this command with the ticket from the sender. **NOTE**: The ticket sharing is out-of-band.
1616+ - `tiles link list-peers` - Shows the info (DID, nickname etc) of the linked devices.
1717+ - `tiles link disable <DID>` - Unlinks a linked device
1818+1919+### Fixed
2020+- Fixed the permission issues while trying to update Tiles using `tiles update` in [$104](https://github.com/tilesprivacy/tiles/pull/104). This was due to new binary location is in `/usr/` instead of `~/.local/`. Running the internal script with `sudo` fixed it.
2121+822## [0.4.4] - 2026-03-16
9231024### Added
···11-FROM mlx-community/Qwen3.5-4B-MLX-4bit
11+FROM mlx-community/Qwen3.5-0.8B-8bit
22+# FROM mlx-community/Qwen3.5-0.8B-MLX-8bit
33+# FROM mlx-community/Qwen3.5-4B-MLX-4bit
24# FROM mlx-community/Qwen3-0.6B-4bit
···11//! Accounts
22// Stuff related to account and identity system
33use anyhow::{Result, anyhow};
44+use iroh::SecretKey;
45use rusqlite::{Connection, types::FromSqlError};
56use std::{
67 fmt::Display,
78 time::{SystemTime, UNIX_EPOCH},
89};
99-use tilekit::accounts::create_identity;
1010+use tilekit::accounts::{create_identity, get_secret_key};
1011use toml::Table;
1112use uuid::Uuid;
1213···2324 pub nickname: String,
2425}
25262626-#[derive(Debug)]
2727+// Type of User account
2828+#[derive(Debug, Clone)]
2729pub enum ACCOUNT {
3030+ // root account, created in the system
2831 LOCAL,
3232+3333+ // remote account
3434+ PEER,
2935}
30363137#[derive(Debug)]
···4551 let value_lower = value.to_lowercase();
4652 match value_lower.as_str() {
4753 "local" => Ok(ACCOUNT::LOCAL),
5454+ "peer" => Ok(ACCOUNT::PEER),
4855 _ => Err(AccountError {
4956 error: "Invalid account type".to_owned(),
5057 }),
···5562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5663 match self {
5764 Self::LOCAL => write!(f, "{}", String::from("local")),
6565+ Self::PEER => write!(f, "{}", String::from("peer")),
5866 }
5967 }
6068}
61696270//TODO: add doc, mirrors user table schema
6371#[allow(dead_code)]
6464-#[derive(Debug)]
7272+#[derive(Debug, Clone)]
6573pub struct User {
6674 pub id: uuid::Uuid,
6775 pub user_id: String,
···218226 })
219227 .map_err(<rusqlite::Error as Into<anyhow::Error>>::into)
220228}
221221-// TODO: when we support multiple accounts
222222-// make sure that there can't be multiple rows with
223223-// root true.
229229+230230+pub fn get_user(conn: &Connection, did: &str) -> Result<User> {
231231+ 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")?;
232232+233233+ fetch_current_user
234234+ .query_one([did], |row| {
235235+ let id: String = row.get(0)?;
236236+ let account_type: String = row.get(3)?;
237237+ let created_at: f64 = row.get(6)?;
238238+ let updated_at: f64 = row.get(7)?;
239239+ Ok(User {
240240+ id: Uuid::try_parse(&id).map_err(FromSqlError::other)?,
241241+ user_id: row.get(1)?,
242242+ username: row.get(2)?,
243243+ account_type: ACCOUNT::try_from(account_type).map_err(FromSqlError::other)?,
244244+ active_profile: row.get(4)?,
245245+ root: row.get(5)?,
246246+247247+ created_at: created_at as u64,
248248+ updated_at: updated_at as u64,
249249+ })
250250+ })
251251+ .map_err(<rusqlite::Error as Into<anyhow::Error>>::into)
252252+}
253253+224254pub fn save_root_account_db() -> Result<()> {
225255 let conn = get_db_conn(DBTYPE::COMMON)?;
226256 let config = get_or_create_config()?;
···256286 }
257287}
258288289289+// TODO: We could add unique user_id constraints, but
290290+// we will wait for it until we solve the sync part
291291+pub fn save_peer_account_db(db_conn: &Connection, user_id: &str, nickname: &str) -> Result<()> {
292292+ let user = User {
293293+ id: Uuid::now_v7(),
294294+ user_id: String::from(user_id),
295295+ username: String::from(nickname),
296296+ account_type: ACCOUNT::PEER,
297297+ active_profile: false,
298298+ root: false,
299299+ created_at: SystemTime::now()
300300+ .duration_since(UNIX_EPOCH)
301301+ .expect("time went backwards")
302302+ .as_secs(),
303303+ updated_at: SystemTime::now()
304304+ .duration_since(UNIX_EPOCH)
305305+ .expect("time went backwards")
306306+ .as_secs(),
307307+ };
308308+ db_conn.execute(
309309+ "insert into users (id, user_id, username, active_profile, account_type, root) values
310310+ (?1, ?2, ?3,?4, ?5, ?6)",
311311+ (
312312+ &user.id.to_string(),
313313+ &user.user_id,
314314+ &user.username,
315315+ &user.active_profile,
316316+ user.account_type.to_string(),
317317+ &user.root,
318318+ ),
319319+ )?;
320320+ Ok(())
321321+}
322322+323323+pub fn get_user_by_user_id(conn: &Connection, user_id: String) -> Result<()> {
324324+ let mut fetch_root_user = conn.prepare("select id from users where user_id = ?1")?;
325325+326326+ match fetch_root_user.query_one([user_id], |_row| Ok(())) {
327327+ Ok(_) => Ok(()),
328328+ Err(rusqlite::Error::QueryReturnedNoRows) => Err(anyhow!("User doesnt exist")),
329329+ Err(_err) => Err(anyhow!("Fetching user from db failed")),
330330+ }
331331+}
332332+259333fn create_root_user(root_user_config: &Table, nickname: Option<String>) -> Result<Table> {
260334 let mut root_user_table = root_user_config.clone();
261261- match create_identity("tiles") {
335335+ let app_name = if cfg!(debug_assertions) {
336336+ "tiles_dev"
337337+ } else {
338338+ "tiles"
339339+ };
340340+ match create_identity(app_name) {
262341 Ok(did) => {
263342 root_user_table.insert("id".to_owned(), toml::Value::String(did));
264343 if let Some(nickname) = nickname {
···270349 }
271350}
272351352352+pub fn get_peer_list(db_conn: &Connection) -> Result<Vec<User>> {
353353+ 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\'")?;
354354+355355+ let user_rows = stmt
356356+ .query_map([], |row| {
357357+ let id: String = row.get(0)?;
358358+ let account_type: String = row.get(3)?;
359359+ let created_at: f64 = row.get(6)?;
360360+ let updated_at: f64 = row.get(7)?;
361361+ Ok(User {
362362+ id: Uuid::try_parse(&id).map_err(FromSqlError::other)?,
363363+ user_id: row.get(1)?,
364364+ username: row.get(2)?,
365365+ account_type: ACCOUNT::try_from(account_type).map_err(FromSqlError::other)?,
366366+ active_profile: row.get(4)?,
367367+ root: row.get(5)?,
368368+369369+ created_at: created_at as u64,
370370+ updated_at: updated_at as u64,
371371+ })
372372+ })
373373+ .map_err(<rusqlite::Error as Into<anyhow::Error>>::into)?;
374374+375375+ let mut peer_list: Vec<User> = vec![];
376376+377377+ for peer in user_rows {
378378+ peer_list.push(peer?);
379379+ }
380380+381381+ Ok(peer_list)
382382+}
383383+384384+pub fn unlink(db_conn: &Connection, user_id: &str) -> Result<()> {
385385+ let user = get_current_user(db_conn)?;
386386+ if user.user_id == user_id {
387387+ return Err(anyhow!("Cannot unlink yourself"));
388388+ }
389389+390390+ match db_conn.execute(
391391+ "delete from users where user_id = ?1 and account_type != \'local\'",
392392+ [user_id],
393393+ ) {
394394+ Ok(0) => Err(anyhow!("A peer with DID {} doesn't exist", user_id)),
395395+ Ok(_) => Ok(()),
396396+ Err(err) => Err(anyhow!("Unable to unlink the peer due to {:?}", err)),
397397+ }
398398+}
399399+400400+pub fn get_app_secret_key(did: &str) -> Result<SecretKey> {
401401+ let app_name = if cfg!(debug_assertions) {
402402+ "tiles_dev"
403403+ } else {
404404+ "tiles"
405405+ };
406406+ let signing_key = get_secret_key(app_name, did)?;
407407+ Ok(SecretKey::from_bytes(&signing_key))
408408+}
409409+273410#[cfg(test)]
274411mod tests {
275412 use super::*;
···617754 .unwrap();
618755619756 assert!(get_current_user(&conn).is_err());
757757+ }
758758+759759+ fn create_user(conn: &Connection, account_type: ACCOUNT) -> User {
760760+ let user = User {
761761+ id: Uuid::now_v7(),
762762+ user_id: String::from("did"),
763763+ username: String::from("nickname"),
764764+ account_type,
765765+ active_profile: true,
766766+ root: true,
767767+ created_at: SystemTime::now()
768768+ .duration_since(UNIX_EPOCH)
769769+ .expect("time went backwards")
770770+ .as_secs(),
771771+ updated_at: SystemTime::now()
772772+ .duration_since(UNIX_EPOCH)
773773+ .expect("time went backwards")
774774+ .as_secs(),
775775+ };
776776+777777+ 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,
778778+ user.account_type.to_string(), &user.root)).unwrap();
779779+ user
780780+ }
781781+782782+ #[test]
783783+ fn test_list_peers_with_atleast_0_peer() {
784784+ let conn = setup_db_schema();
785785+ let _local_user = create_user(&conn, ACCOUNT::LOCAL);
786786+787787+ let user_list = get_peer_list(&conn).unwrap();
788788+789789+ assert!(user_list.is_empty())
790790+ }
791791+792792+ #[test]
793793+ fn test_list_peers_with_more_than_0_peer() {
794794+ let conn = setup_db_schema();
795795+ let _local_user = create_user(&conn, ACCOUNT::LOCAL);
796796+ save_peer_account_db(&conn, "did:jey:varathan", "varathan").unwrap();
797797+ let user_list = get_peer_list(&conn).unwrap();
798798+799799+ assert!(!user_list.is_empty())
800800+ }
801801+802802+ #[test]
803803+ fn test_unlink_valid_peer() {
804804+ let conn = setup_db_schema();
805805+ let _local_user = create_user(&conn, ACCOUNT::LOCAL);
806806+ save_peer_account_db(&conn, "did:jey:varathan", "varathan").unwrap();
807807+ let user_list = get_peer_list(&conn).unwrap();
808808+809809+ assert!(!user_list.is_empty());
810810+811811+ unlink(&conn, "did:jey:varathan").unwrap();
812812+ let user_list = get_peer_list(&conn).unwrap();
813813+ assert!(user_list.is_empty());
814814+ }
815815+816816+ #[test]
817817+ fn test_try_unlink_local() {
818818+ let conn = setup_db_schema();
819819+ let local_user = create_user(&conn, ACCOUNT::LOCAL);
820820+821821+ assert!(unlink(&conn, &local_user.user_id).is_err())
620822 }
621823}
+2
tiles/src/core/mod.rs
···1010pub mod accounts;
1111pub mod chats;
1212pub mod health;
1313+pub mod network;
1314pub mod storage;
1515+1416// Entrypoint of the core
1517pub fn init() -> Result<()> {
1618 init_db()?;
+467
tiles/src/core/network/mod.rs
···11+//! The main module for networking
22+33+pub mod ticket;
44+use std::{
55+ io,
66+ str::FromStr,
77+ sync::{Arc, Mutex},
88+ time::Duration,
99+};
1010+1111+use anyhow::Result;
1212+use futures_util::{StreamExt, TryStreamExt};
1313+use iroh::{
1414+ Endpoint, EndpointId, NET_REPORT_TIMEOUT, PublicKey,
1515+ address_lookup::{self, MdnsAddressLookup, mdns},
1616+ endpoint::{BindError, presets},
1717+ endpoint_info::UserData,
1818+ protocol::Router,
1919+};
2020+use iroh_gossip::{
2121+ Gossip, TopicId,
2222+ api::{Event, GossipReceiver, GossipSender},
2323+};
2424+use iroh_ping::Ping;
2525+use iroh_tickets::endpoint::EndpointTicket;
2626+use rusqlite::Connection;
2727+use tilekit::accounts::{get_did_from_public_key, get_random_bytes, get_random_bytes_32};
2828+use tokio::task::spawn_blocking;
2929+use uuid::Uuid;
3030+3131+use crate::core::{
3232+ accounts::{
3333+ self, get_app_secret_key, get_current_user, get_user_by_user_id, save_peer_account_db,
3434+ },
3535+ network::ticket::{EndpointUserData, LinkTicket},
3636+ storage::db::{DBTYPE, get_db_conn},
3737+};
3838+use sha2::{Digest, Sha256};
3939+4040+const DEVICE_LINK_LOCAL_TOPIC: &str = "com.tilesprivacy.tiles.link";
4141+#[derive(serde::Serialize, serde::Deserialize)]
4242+struct NetworkMessage {
4343+ from_did: String,
4444+ from_nickname: String,
4545+ is_online: bool,
4646+ body: MessageBody,
4747+ // to prevent iroh's deduplication on same msg
4848+ nonce: [u8; 16],
4949+}
5050+5151+impl NetworkMessage {
5252+ fn new(user: &accounts::User, is_online: bool, body: MessageBody) -> Self {
5353+ Self {
5454+ from_did: user.user_id.clone(),
5555+ from_nickname: user.username.clone(),
5656+ is_online,
5757+ body,
5858+ nonce: get_random_bytes(),
5959+ }
6060+ }
6161+ fn from_bytes(bytes: &[u8]) -> Result<Self> {
6262+ postcard::from_bytes(bytes).map_err(Into::into)
6363+ }
6464+ fn to_bytes(&self) -> Vec<u8> {
6565+ postcard::to_stdvec(&self).expect("Failed to convert to bytes w postcard")
6666+ }
6767+}
6868+6969+#[derive(serde::Serialize, serde::Deserialize)]
7070+#[allow(clippy::enum_variant_names)]
7171+enum MessageBody {
7272+ LinkRequest { ticket: String },
7373+ LinkAccepted,
7474+ LinkRejected { reason: String },
7575+}
7676+7777+// Entrypoint of network connection
7878+pub async fn init(ticket: Option<&str>) -> Result<()> {
7979+ if let Some(ticket_addr) = ticket {
8080+ let sender_endpoint = Endpoint::bind(presets::N0).await?;
8181+ println!("{:?}", sender_endpoint.addr());
8282+ let se_clone = sender_endpoint.clone();
8383+ let send_pinger = Ping::new();
8484+ let rtt = send_pinger
8585+ .ping(
8686+ &sender_endpoint,
8787+ EndpointTicket::from_str(ticket_addr)?
8888+ .endpoint_addr()
8989+ .clone(),
9090+ )
9191+ .await?;
9292+9393+ println!("ping took: {:?} to complete", rtt);
9494+ se_clone.close().await;
9595+ } else {
9696+ let endpoint = Endpoint::bind(presets::N0).await?;
9797+ let ep = endpoint.clone();
9898+ let ep2 = endpoint.clone();
9999+ endpoint.online().await;
100100+101101+ let ping = Ping::new();
102102+103103+ let ticket = EndpointTicket::new(endpoint.addr());
104104+105105+ println!("ticket\n{:?}", ticket.to_string());
106106+107107+ let recv_router = Router::builder(ep).accept(iroh_ping::ALPN, ping).spawn();
108108+ ep2.close().await;
109109+ recv_router.shutdown().await?;
110110+ }
111111+ Ok(())
112112+}
113113+114114+pub async fn link(ticket: Option<String>) -> Result<()> {
115115+ let user_db_conn = get_db_conn(DBTYPE::COMMON)?;
116116+ let user = get_current_user(&user_db_conn)?;
117117+ let endpoint = create_endpoint(&user).await?;
118118+ let is_online = is_online(&endpoint).await;
119119+ let mut bootstrap_ids: Vec<EndpointId> = vec![];
120120+ // if ticket's there, then this is link enable sender's command, e;se receiver end
121121+ if let Some(ticket) = ticket {
122122+ let (endpoint_id, mut did, mut nickname, topic_value) = parse_link_ticket(&ticket)?;
123123+124124+ let topic_id = if is_online {
125125+ topic_value.expect("Expected topicId")
126126+ } else {
127127+ create_topic_id(DEVICE_LINK_LOCAL_TOPIC)
128128+ };
129129+130130+ if is_online {
131131+ bootstrap_ids.push(endpoint_id.expect("Expected an EndpointId as bootstrapId "))
132132+ } else {
133133+ println!("Searching for peers in the local network..");
134134+ let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?;
135135+ let (new_bootstrap_ids, user_data) =
136136+ find_offline_bootstrap_peers(&endpoint, mdns).await?;
137137+ bootstrap_ids = new_bootstrap_ids;
138138+ let endpoint_user_data = EndpointUserData::try_from(user_data.to_string())?;
139139+ did = endpoint_user_data.did;
140140+ nickname = endpoint_user_data.nickname;
141141+ };
142142+ if get_user_by_user_id(&user_db_conn, did.to_owned()).is_ok() {
143143+ println!("Device {}({}) already linked", nickname, did);
144144+ return Ok(());
145145+ }
146146+ let (sender, mut receiver, recv_router) =
147147+ create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?;
148148+149149+ println!("\nConnecting to {}({}).....", nickname, did);
150150+151151+ receiver.joined().await?;
152152+153153+ tokio::spawn(subsribe_loop(
154154+ receiver,
155155+ sender.clone(),
156156+ user.clone(),
157157+ user_db_conn,
158158+ None,
159159+ ));
160160+161161+ let link_req_msg =
162162+ NetworkMessage::new(&user, is_online, MessageBody::LinkRequest { ticket });
163163+ sender.broadcast(link_req_msg.to_bytes().into()).await?;
164164+165165+ println!("\nSent link request to {}({})", nickname, did);
166166+167167+ println!("\nWaiting for response...");
168168+169169+ tokio::signal::ctrl_c().await?;
170170+ recv_router.shutdown().await?;
171171+ } else {
172172+ // RECEIVER BLOCK
173173+ if !is_online {
174174+ let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?;
175175+ endpoint.address_lookup()?.add(mdns.clone());
176176+ }
177177+178178+ // Its better to have unique session'ed channels while
179179+ // when the communication is over internet
180180+ let topic_id = if is_online {
181181+ TopicId::from_bytes(get_random_bytes_32())
182182+ } else {
183183+ create_topic_id(DEVICE_LINK_LOCAL_TOPIC)
184184+ };
185185+186186+ let (sender, receiver, recv_router) =
187187+ create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?;
188188+189189+ let generated_ticket = if is_online {
190190+ let ticket = LinkTicket::new(
191191+ topic_id,
192192+ endpoint.addr(),
193193+ user.user_id.clone(),
194194+ user.username.clone(),
195195+ );
196196+ println!("Generated link ticket: \n{:?}\n", ticket.to_string());
197197+198198+ println!(
199199+ "Use this ticket with `tiles link enable <ticket>` on the system you want to connect to\n"
200200+ );
201201+ ticket.to_string()
202202+ } else {
203203+ // generate a code
204204+ let uuid = Uuid::new_v4().to_string();
205205+206206+ let ticket = uuid.split('-').collect::<Vec<&str>>()[0];
207207+208208+ println!("Generated link code: {}\n", ticket);
209209+210210+ println!(
211211+ "Use this link code with `tiles link enable {}` on the system you want to connect to\n",
212212+ ticket
213213+ );
214214+ ticket.to_string()
215215+ };
216216+217217+ println!("Don't close this session until the link process is done\n");
218218+219219+ tokio::spawn(subsribe_loop(
220220+ receiver,
221221+ sender.clone(),
222222+ user.clone(),
223223+ user_db_conn,
224224+ Some(generated_ticket),
225225+ ));
226226+227227+ // TODO: Maybe a better way is to use a oneshot channel to exit
228228+ // the terminal instead of SIGINT
229229+ tokio::signal::ctrl_c().await?;
230230+ recv_router.shutdown().await?;
231231+ }
232232+ endpoint.close().await;
233233+ Ok(())
234234+}
235235+236236+async fn subsribe_loop(
237237+ mut receiver: GossipReceiver,
238238+ sender: GossipSender,
239239+ user: accounts::User,
240240+ db_conn: Connection,
241241+ generated_ticket: Option<String>,
242242+) -> Result<()> {
243243+ while let Some(event) = receiver.try_next().await? {
244244+ if cfg!(debug_assertions) {
245245+ println!("In {}:, some event {:?}", user.username, event);
246246+ }
247247+ if let Event::Received(msg) = event {
248248+ let pub_key = msg.delivered_from;
249249+ let msg = NetworkMessage::from_bytes(&msg.content)?;
250250+ if !is_did_valid(&msg.from_did, pub_key)? {
251251+ eprintln!(
252252+ "Incoming peer DID {} invalid, blocking request",
253253+ msg.from_did
254254+ );
255255+ continue;
256256+ }
257257+ match msg.body {
258258+ MessageBody::LinkRequest { ticket } => {
259259+ println!(
260260+ "Received link request from {}({}), Do you want to link Y/N ?",
261261+ msg.from_nickname, msg.from_did
262262+ );
263263+ let input: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
264264+265265+ let input_clone = input.clone();
266266+ let stdin = io::stdin();
267267+ spawn_blocking(move || {
268268+ let mut input_clone = input_clone.lock().unwrap();
269269+ let _ = stdin.read_line(&mut input_clone);
270270+ })
271271+ .await?;
272272+ let input_resp = input.lock().unwrap().trim().to_owned();
273273+274274+ let link_res_resp = if input_resp.to_lowercase() == "y" {
275275+ if let Some(gen_ticket) = &generated_ticket
276276+ && !msg.is_online
277277+ && *gen_ticket != ticket.to_lowercase()
278278+ {
279279+ println!("\nVerifying code does not match, please try again");
280280+ let response = NetworkMessage::new(
281281+ &user,
282282+ msg.is_online,
283283+ MessageBody::LinkRejected {
284284+ reason: String::from("Link code mismatch"),
285285+ },
286286+ );
287287+ sender.broadcast(response.to_bytes().into()).await?;
288288+ continue;
289289+ }
290290+291291+ if let Err(err) =
292292+ save_peer_account_db(&db_conn, &msg.from_did, &msg.from_nickname)
293293+ {
294294+ println!("Failed to add the peer locally due to {:?}", err);
295295+296296+ continue;
297297+ }
298298+299299+ println!(
300300+ "Device {}({}) is now linked\nYou can exit now by ctrl-c",
301301+ msg.from_nickname, msg.from_did
302302+ );
303303+ NetworkMessage::new(&user, msg.is_online, MessageBody::LinkAccepted)
304304+ } else {
305305+ println!("You can exit now by ctrl-c");
306306+ NetworkMessage::new(
307307+ &user,
308308+ msg.is_online,
309309+ MessageBody::LinkRejected {
310310+ reason: String::from("Peer rejected the request"),
311311+ },
312312+ )
313313+ };
314314+ input.lock().unwrap().clear();
315315+316316+ sender.broadcast(link_res_resp.to_bytes().into()).await?;
317317+ }
318318+ MessageBody::LinkAccepted => {
319319+ println!("\nLink accepted by {}({})", msg.from_nickname, msg.from_did);
320320+321321+ if let Err(err) =
322322+ save_peer_account_db(&db_conn, &msg.from_did, &msg.from_nickname)
323323+ {
324324+ println!("Failed to add the peer locally due to {:?}", err);
325325+ return Ok(());
326326+ }
327327+328328+ println!("\nYou can exit now by ctrl-c");
329329+330330+ continue;
331331+ }
332332+ MessageBody::LinkRejected { reason } => {
333333+ println!(
334334+ "Oops looks like your link request has been rejected by {}({}),\nreason: {},\nexit (ctrl-c) and try again",
335335+ msg.from_nickname, msg.from_did, reason
336336+ );
337337+ }
338338+ }
339339+ }
340340+ }
341341+ Ok(())
342342+}
343343+344344+async fn create_endpoint(user: &accounts::User) -> Result<Endpoint> {
345345+ // In release mode, we will build the endpoint using
346346+ // tiles keypair in keychain
347347+ let usr_data = EndpointUserData::new(&user.user_id, &user.username);
348348+ if !cfg!(debug_assertions) {
349349+ let secret_key = get_app_secret_key(&user.user_id)?;
350350+ Endpoint::builder(presets::N0)
351351+ .user_data_for_address_lookup(UserData::try_from(usr_data.to_string())?)
352352+ .secret_key(secret_key)
353353+ .bind()
354354+ .await
355355+ .map_err(<BindError as Into<anyhow::Error>>::into)
356356+ } else {
357357+ Endpoint::builder(presets::N0)
358358+ .user_data_for_address_lookup(UserData::try_from(usr_data.to_string())?)
359359+ .bind()
360360+ .await
361361+ .map_err(<BindError as Into<anyhow::Error>>::into)
362362+ }
363363+}
364364+365365+fn create_topic_id(topic_name: &str) -> TopicId {
366366+ let mut hasher = Sha256::new();
367367+ hasher.update(topic_name.as_bytes());
368368+ let topic_id_bytes = hasher.finalize();
369369+ TopicId::from_bytes(topic_id_bytes.into())
370370+}
371371+372372+fn _get_did_from_endpoint(endpoint_id: EndpointId) -> Result<String> {
373373+ get_did_from_public_key(endpoint_id.as_bytes())
374374+}
375375+376376+async fn is_online(endpoint: &Endpoint) -> bool {
377377+ tokio::select! {
378378+ _ = endpoint.online() => {
379379+ true
380380+ }
381381+ _ = tokio::time::sleep(Duration::from_secs(NET_REPORT_TIMEOUT)) => {
382382+ false
383383+ }
384384+ }
385385+}
386386+387387+// As of now we exit asap when we see a peer. This is subjected to change
388388+// as the scale
389389+async fn find_offline_bootstrap_peers(
390390+ endpoint: &Endpoint,
391391+ mdns: MdnsAddressLookup,
392392+) -> Result<(Vec<EndpointId>, UserData)> {
393393+ let mut bootstrap_ids: Vec<EndpointId> = vec![];
394394+ endpoint.address_lookup()?.add(mdns.clone());
395395+ let mut mdns_event = mdns.subscribe().await;
396396+ let mut user_data = UserData::from_str("")?;
397397+ while let Some(event) = mdns_event.next().await {
398398+ match event {
399399+ mdns::DiscoveryEvent::Discovered {
400400+ endpoint_info,
401401+ last_updated: _,
402402+ } => {
403403+ if cfg!(debug_assertions) {
404404+ println!("peer discoverd {:?}", endpoint_info);
405405+ }
406406+ bootstrap_ids.push(endpoint_info.endpoint_id);
407407+ user_data = endpoint_info.user_data().unwrap().clone();
408408+ break;
409409+ }
410410+ mdns::DiscoveryEvent::Expired { endpoint_id } => {
411411+ if cfg!(debug_assertions) {
412412+ println!("peer left {:?}", endpoint_id)
413413+ }
414414+ }
415415+ }
416416+ }
417417+418418+ Ok((bootstrap_ids, user_data))
419419+}
420420+421421+async fn create_gossip_network(
422422+ endpoint: &Endpoint,
423423+ topic_id: TopicId,
424424+ bootstrap_ids: Vec<iroh::PublicKey>,
425425+) -> Result<(GossipSender, GossipReceiver, Router)> {
426426+ let gossip = Gossip::builder().spawn(endpoint.clone());
427427+ let recv_router = Router::builder(endpoint.clone())
428428+ .accept(iroh_gossip::ALPN, gossip.clone())
429429+ .spawn();
430430+431431+ let (goss_sender, goss_receiver) = gossip.subscribe(topic_id, bootstrap_ids).await?.split();
432432+433433+ Ok((goss_sender, goss_receiver, recv_router))
434434+}
435435+436436+// We handle the parsing in this way since ticket can be an encoded `LinkTicket`
437437+// or just a 4 byte hex if linking over mDNS
438438+fn parse_link_ticket(
439439+ ticket: &str,
440440+) -> Result<(Option<EndpointId>, String, String, Option<TopicId>)> {
441441+ if let Ok(parsed_ticket) = LinkTicket::from_str(ticket) {
442442+ Ok((
443443+ Some(parsed_ticket.addr.id),
444444+ parsed_ticket.did,
445445+ parsed_ticket.nickname,
446446+ Some(parsed_ticket.topic_id),
447447+ ))
448448+ } else if ticket.len() == 8 {
449449+ // NOTE: We only have len check as a "parser" for the offline code
450450+ // but this will surely change once we fix the code format
451451+ Ok((None, String::from(""), String::from(""), None))
452452+ } else {
453453+ Err(anyhow::anyhow!("Invalid Ticket"))
454454+ }
455455+}
456456+457457+fn is_did_valid(did: &str, pub_key: PublicKey) -> Result<bool> {
458458+ // on debug mode, we skip the auth check, since we will be testing
459459+ // with random endpoitns but w DID from config atp
460460+ if cfg!(debug_assertions) {
461461+ Ok(true)
462462+ } else {
463463+ Ok(get_did_from_public_key(&pub_key)? == did)
464464+ }
465465+}
466466+// fn subsribe_mdns_events(mdns_events) {}
467467+//TODO: Add tests, can we get some from iroh reference?