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: Added linked device to DB + added no_repl flag

- Now the linked device are added to local DB w an account type of SELF
- Prevent redundant linking
- Added a new flag `-x` or `--no_repl` to tiles command, which will
prevent the tool from going directly to repl. This is useful if somebody
want to setup tiles config w/0 downloading model. Particularly useful
in p2p testing

madclaws acaf1ef7 ad1cb1c4

+121 -21
+8 -1
tilekit/src/accounts.rs
··· 4 4 5 5 use anyhow::Result; 6 6 use ed25519_dalek::{ 7 - SecretKey, SigningKey, 7 + SecretKey, SigningKey, VerifyingKey, 8 8 ed25519::signature::rand_core::{OsRng, RngCore}, 9 9 }; 10 10 use keyring::Entry; ··· 49 49 pub fn get_public_key_from_did(did: &str) -> Result<[u8; 32]> { 50 50 let ed_did = Ed25519Did::from_str(did)?; 51 51 Ok(ed_did.0.to_bytes()) 52 + } 53 + 54 + pub fn get_did_from_public_key(publick_key: &[u8; 32]) -> Result<String> { 55 + let verifying_key = VerifyingKey::from_bytes(publick_key)?; 56 + 57 + let ed_did = Ed25519Did::from(verifying_key); 58 + Ok(ed_did.to_string()) 52 59 } 53 60 54 61 pub fn get_random_bytes() -> [u8; 16] {
+51 -3
tiles/src/core/accounts.rs
··· 23 23 pub nickname: String, 24 24 } 25 25 26 + // Type of User account 26 27 #[derive(Debug, Clone)] 27 28 pub enum ACCOUNT { 29 + // root account, created in the system 28 30 LOCAL, 31 + 32 + // remote account but same previlege as your local account 33 + SELF, 29 34 } 30 35 31 36 #[derive(Debug)] ··· 55 60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 56 61 match self { 57 62 Self::LOCAL => write!(f, "{}", String::from("local")), 63 + Self::SELF => write!(f, "{}", String::from("self")), 58 64 } 59 65 } 60 66 } ··· 218 224 }) 219 225 .map_err(<rusqlite::Error as Into<anyhow::Error>>::into) 220 226 } 221 - // TODO: when we support multiple accounts 222 - // make sure that there can't be multiple rows with 223 - // root true. 227 + 224 228 pub fn save_root_account_db() -> Result<()> { 225 229 let conn = get_db_conn(DBTYPE::COMMON)?; 226 230 let config = get_or_create_config()?; ··· 253 257 } 254 258 Err(_err) => Err(anyhow!("Fetching user from db failed")), 255 259 _ => Ok(()), 260 + } 261 + } 262 + 263 + // TODO: We could add unique user_id constraints, but 264 + // we will wait for it until we solve the sync part 265 + pub fn save_self_account_db(db_conn: &Connection, user_id: &str, nickname: &str) -> Result<()> { 266 + let user = User { 267 + id: Uuid::now_v7(), 268 + user_id: String::from(user_id), 269 + username: String::from(nickname), 270 + account_type: ACCOUNT::SELF, 271 + active_profile: false, 272 + root: false, 273 + created_at: SystemTime::now() 274 + .duration_since(UNIX_EPOCH) 275 + .expect("time went backwards") 276 + .as_secs(), 277 + updated_at: SystemTime::now() 278 + .duration_since(UNIX_EPOCH) 279 + .expect("time went backwards") 280 + .as_secs(), 281 + }; 282 + db_conn.execute( 283 + "insert into users (id, user_id, username, active_profile, account_type, root) values 284 + (?1, ?2, ?3,?4, ?5, ?6)", 285 + ( 286 + &user.id.to_string(), 287 + &user.user_id, 288 + &user.username, 289 + &user.active_profile, 290 + user.account_type.to_string(), 291 + &user.root, 292 + ), 293 + )?; 294 + Ok(()) 295 + } 296 + 297 + pub fn get_user_by_user_id(conn: &Connection, user_id: &str) -> Result<()> { 298 + let mut fetch_root_user = conn.prepare("select id from users where user_id = ?1")?; 299 + 300 + match fetch_root_user.query_one([user_id], |_row| Ok(())) { 301 + Ok(_) => Ok(()), 302 + Err(rusqlite::Error::QueryReturnedNoRows) => Err(anyhow!("User doesnt exist")), 303 + Err(_err) => Err(anyhow!("Fetching user from db failed")), 256 304 } 257 305 } 258 306
+50 -14
tiles/src/core/network/mod.rs
··· 6 6 use anyhow::Result; 7 7 use futures_util::TryStreamExt; 8 8 use iroh::{ 9 - Endpoint, SecretKey, 9 + Endpoint, EndpointId, SecretKey, 10 10 endpoint::{BindError, presets}, 11 11 protocol::Router, 12 12 }; ··· 16 16 }; 17 17 use iroh_ping::Ping; 18 18 use iroh_tickets::endpoint::EndpointTicket; 19 - use tilekit::accounts::{get_random_bytes, get_secret_key}; 19 + use rusqlite::Connection; 20 + use tilekit::accounts::{get_did_from_public_key, get_random_bytes, get_secret_key}; 20 21 21 22 use crate::core::{ 22 - accounts::{self, get_current_user}, 23 + accounts::{self, get_current_user, get_user_by_user_id, save_self_account_db}, 23 24 network::ticket::LinkTicket, 24 25 storage::db::{DBTYPE, get_db_conn}, 25 26 }; ··· 49 50 } 50 51 51 52 #[derive(serde::Serialize, serde::Deserialize)] 53 + #[allow(clippy::enum_variant_names)] 52 54 enum MessageBody { 53 55 LinkRequest { did: String, nickname: String }, 54 56 LinkAccepted { did: String, nickname: String }, ··· 97 99 let user = get_current_user(&user_db_conn)?; 98 100 if let Some(ticket) = ticket { 99 101 let link_ticket = LinkTicket::from_str(&ticket)?; 100 - 101 - let endpoint = create_endpoint(false, &user).await?; 102 + if get_user_by_user_id(&user_db_conn, &link_ticket.did).is_err() { 103 + println!( 104 + "Device {}({}) already linked", 105 + link_ticket.nickname, link_ticket.did 106 + ); 107 + return Ok(()); 108 + } 109 + let endpoint = create_endpoint(&user).await?; 102 110 endpoint.online().await; 103 111 let gossip = Gossip::builder().spawn(endpoint.clone()); 104 112 ··· 116 124 link_ticket.nickname, link_ticket.did 117 125 ); 118 126 receiver.joined().await?; 119 - tokio::spawn(subsribe_loop(receiver, sender.clone(), user.clone())); 127 + tokio::spawn(subsribe_loop( 128 + receiver, 129 + sender.clone(), 130 + user.clone(), 131 + user_db_conn, 132 + )); 120 133 121 134 let link_req_msg = NetworkMessage::new(MessageBody::LinkRequest { 122 135 did: user.user_id, ··· 134 147 recv_router.shutdown().await?; 135 148 endpoint.close().await; 136 149 } else { 137 - let endpoint = create_endpoint(false, &user).await?; 150 + let endpoint = create_endpoint(&user).await?; 138 151 endpoint.online().await; 139 152 140 153 let gossip = Gossip::builder().spawn(endpoint.clone()); ··· 161 174 162 175 println!("Don't close this session until the link process is done\n"); 163 176 164 - tokio::spawn(subsribe_loop(receiver, sender.clone(), user.clone())); 177 + tokio::spawn(subsribe_loop( 178 + receiver, 179 + sender.clone(), 180 + user.clone(), 181 + user_db_conn, 182 + )); 183 + 184 + // TODO: Maybe a better way is to use a oneshot channel to exit 185 + // the terminal instead of SIGINT 165 186 tokio::signal::ctrl_c().await?; 166 187 recv_router.shutdown().await?; 167 188 endpoint.close().await; ··· 173 194 mut receiver: GossipReceiver, 174 195 sender: GossipSender, 175 196 user: accounts::User, 197 + db_conn: Connection, 176 198 ) -> Result<()> { 177 199 while let Some(event) = receiver.try_next().await? { 178 200 // println!("some event {:?}", event); ··· 180 202 match NetworkMessage::from_bytes(&msg.content)?.body { 181 203 MessageBody::LinkRequest { did, nickname } => { 182 204 println!( 183 - "Received link request from {}({}), Do you want to link Y/N", 205 + "Received link request from {}({}), Do you want to link Y/N ?", 184 206 nickname, did 185 207 ); 186 208 let stdin = io::stdin(); ··· 188 210 stdin.read_line(&mut input)?; 189 211 input = input.trim().to_owned(); 190 212 let link_res_resp = if input.to_lowercase() == "y" { 191 - // TODO: Add the device to DB 192 - 213 + save_self_account_db(&db_conn, &did, &nickname)?; 214 + println!( 215 + "Device {}({}) is now linked\nYou can exit now by ctrl-c", 216 + nickname, did 217 + ); 193 218 NetworkMessage::new(MessageBody::LinkAccepted { 194 219 did: user.user_id.clone(), 195 220 nickname: user.username.clone(), ··· 205 230 sender.broadcast(link_res_resp.to_bytes().into()).await?; 206 231 } 207 232 MessageBody::LinkAccepted { did, nickname } => { 233 + save_self_account_db(&db_conn, &did, &nickname)?; 208 234 println!("Link accepted by {}({})", nickname, did); 209 - // But shouldn't we add the incoming user to DB? 235 + 210 236 println!("You can exit now by ctrl-c"); 237 + 238 + return Ok(()); 211 239 } 212 240 MessageBody::LinkRejected { did, nickname } => { 213 241 println!( ··· 221 249 Ok(()) 222 250 } 223 251 224 - async fn create_endpoint(use_app_key: bool, user: &accounts::User) -> Result<Endpoint> { 225 - if use_app_key { 252 + async fn create_endpoint(user: &accounts::User) -> Result<Endpoint> { 253 + // In release mode, we will build the endpoint using 254 + // tiles keypair in keychain 255 + if !cfg!(debug_assertions) { 226 256 let signing_key = get_secret_key("tiles", &user.user_id)?; 227 257 228 258 let secret_key = SecretKey::from_bytes(&signing_key); ··· 245 275 let topic_id_bytes = hasher.finalize(); 246 276 TopicId::from_bytes(topic_id_bytes.into()) 247 277 } 278 + 279 + fn _get_did_from_endpoint(endpoint_id: EndpointId) -> Result<String> { 280 + get_did_from_public_key(endpoint_id.as_bytes()) 281 + } 282 + 283 + //TODO: Add tests, can we get some from iroh reference?
+2
tiles/src/core/network/ticket.rs
··· 52 52 } 53 53 } 54 54 } 55 + 56 + // TODO: Add tests
+10 -3
tiles/src/main.rs
··· 81 81 // Future flags go here: 82 82 // #[arg(long, default_value_t = 6969)] 83 83 // port: u16, 84 + 85 + // Don't go into the repl 86 + #[arg(short = 'x', long)] 87 + no_repl: bool, 84 88 } 85 89 86 90 #[derive(Debug, Args)] ··· 158 162 relay_count: cli.flags.relay_count, 159 163 memory: cli.flags.memory, 160 164 }; 165 + 161 166 commands::run_setup_for_ftue(&run_args) 162 167 .inspect_err(|e| eprintln!("Failed to setup Tiles due to {:?}", e))?; 163 168 let _ = commands::try_app_update().await; ··· 169 174 }); 170 175 } 171 176 172 - commands::run(&runtime, run_args) 173 - .await 174 - .inspect_err(|e| eprintln!("Tiles failed to run due to {:?}", e))?; 177 + if !cli.flags.no_repl { 178 + commands::run(&runtime, run_args) 179 + .await 180 + .inspect_err(|e| eprintln!("Tiles failed to run due to {:?}", e))?; 181 + } 175 182 } 176 183 Some(Commands::Run { 177 184 modelfile_path,