[WIP] A (somewhat barebones) atproto app for creating custom sites without hosting!

upload: add support for oauth based sessions when app password is not supplied

vielle.dev 8ecd3532 f42eef1b

verified
Changed files
+89 -36
upload
src
+89 -36
upload/src/main.rs
··· 1 use clap::{ArgAction, Parser}; 2 - use jacquard::api::com_atproto::repo::apply_writes::{self, ApplyWrites, ApplyWritesWritesItem}; 3 - use jacquard::atproto; 4 use jacquard::client::MemorySessionStore; 5 - use jacquard::prelude::XrpcClient; 6 use jacquard::types::string::{AtStrError, RecordKey, Rkey}; 7 use jacquard::{ 8 Data, 9 api::com_atproto::{self, repo::list_records::ListRecords}, ··· 13 types::{ident::AtIdentifier, nsid::Nsid, string::AtprotoStr, uri::Uri}, 14 xrpc::XrpcExt, 15 }; 16 - use miette::{ErrReport, IntoDiagnostic, Result}; 17 use std::{collections::HashMap, fs, path::PathBuf}; 18 19 use crate::sitemap::{BlobRef, Sitemap, SitemapNode}; ··· 21 mod sitemap; 22 mod utils; 23 24 - #[derive(Parser, Debug)] 25 #[command(version, about, long_about = None)] 26 struct Config { 27 /// Handle or DID to authenticate 28 - #[arg(verbatim_doc_comment, short, long)] 29 user: String, 30 /// App password to authenticate the client 31 /// Normal passwords also work but are not advised 32 - #[arg(verbatim_doc_comment, short, long)] 33 - password: String, 34 35 /// Include dotfiles in upload 36 /// Default: false ··· 48 dir: PathBuf, 49 } 50 51 - #[tokio::main] 52 - async fn main() -> Result<(), miette::Error> { 53 - env_logger::init(); 54 - // get config items 55 - let config = Config::parse(); 56 - 57 - // get local site info 58 - let local_sitemap = sitemap::local_sitemap(config.dir, config.all_files, config.git_ignore)?; 59 - 60 - // create session 61 - let client = JacquardResolver::default(); 62 - let store = MemorySessionStore::default(); 63 - let session = CredentialSession::new(store.into(), client.into()); 64 - 65 - let auth = session 66 - .login(config.user.into(), config.password.into(), None, None, None) 67 - .await?; 68 - println!("Authenticated as {}", auth.did); 69 - 70 - 71 - let agent = Agent::from(session); 72 - 73 // find live site records 74 let mut cursor = None; 75 let mut remote_records = Vec::new(); 76 loop { 77 let req = com_atproto::repo::list_records::ListRecords::new() 78 .collection( 79 Nsid::new("dev.atcities.route").expect("failed to generate dev.atcities.route nsid"), 80 ) 81 - .repo(AtIdentifier::Did(auth.did.clone())) 82 .limit(100) 83 .maybe_cursor(cursor) 84 .build(); ··· 120 break; 121 } 122 } 123 124 // upload local site blobs 125 let mut new_sitemap: Sitemap = HashMap::new(); 126 for (k, v) in local_sitemap { ··· 131 } 132 }; 133 let blob = fs::read(blob).into_diagnostic()?; 134 - // let res = agent 135 - // .upload_blob(blob, v.mime_type.clone().into()) 136 - // .await?; 137 138 let req = com_atproto::repo::upload_blob::UploadBlob::new() 139 .body(blob.into()) ··· 149 ); 150 } 151 152 // batch delete/upload records 153 let mut writes = Vec::new(); 154 let mut delete_records = remote_records ··· 198 writes.append(&mut create_records); 199 200 let req = com_atproto::repo::apply_writes::ApplyWrites::new() 201 - .repo(AtIdentifier::Did(auth.did.clone())) 202 .writes(writes) 203 .build(); 204 ··· 208 .await? 209 .into_output()?; 210 211 - println!("res: {res:#?}"); 212 213 Ok(()) 214 }
··· 1 use clap::{ArgAction, Parser}; 2 + use jacquard::api::com_atproto::repo::apply_writes::{ 3 + self, ApplyWrites, ApplyWritesOutput, ApplyWritesWritesItem, 4 + }; 5 use jacquard::client::MemorySessionStore; 6 + use jacquard::client::{AgentSessionExt}; 7 + use jacquard::oauth::loopback::LoopbackConfig; 8 + use jacquard::oauth::types::AuthorizeOptions; 9 use jacquard::types::string::{AtStrError, RecordKey, Rkey}; 10 + use jacquard::{atproto, oauth}; 11 use jacquard::{ 12 Data, 13 api::com_atproto::{self, repo::list_records::ListRecords}, ··· 17 types::{ident::AtIdentifier, nsid::Nsid, string::AtprotoStr, uri::Uri}, 18 xrpc::XrpcExt, 19 }; 20 + use miette::{IntoDiagnostic, Result}; 21 use std::{collections::HashMap, fs, path::PathBuf}; 22 23 use crate::sitemap::{BlobRef, Sitemap, SitemapNode}; ··· 25 mod sitemap; 26 mod utils; 27 28 + #[derive(Parser, Debug, Clone)] 29 #[command(version, about, long_about = None)] 30 struct Config { 31 /// Handle or DID to authenticate 32 + #[arg(verbatim_doc_comment)] 33 user: String, 34 + 35 /// App password to authenticate the client 36 /// Normal passwords also work but are not advised 37 + /// If ommited, oauth will be used instead 38 + /// Oauth is reccomended where possible. 39 + #[arg(verbatim_doc_comment, short = 'p', long = "password")] 40 + password: Option<String>, 41 42 /// Include dotfiles in upload 43 /// Default: false ··· 55 dir: PathBuf, 56 } 57 58 + async fn live_records(agent: &impl AgentSessionExt, config: Config) -> Result<Vec<String>> { 59 // find live site records 60 let mut cursor = None; 61 let mut remote_records = Vec::new(); 62 + let user = config.user.clone(); 63 + let user = if user.contains(":") { 64 + AtIdentifier::Did(user.into()) 65 + } else { 66 + AtIdentifier::Handle(user.into()) 67 + }; 68 loop { 69 let req = com_atproto::repo::list_records::ListRecords::new() 70 .collection( 71 Nsid::new("dev.atcities.route").expect("failed to generate dev.atcities.route nsid"), 72 ) 73 + .repo(user.clone()) 74 .limit(100) 75 .maybe_cursor(cursor) 76 .build(); ··· 112 break; 113 } 114 } 115 + Ok(remote_records) 116 + } 117 118 + async fn upload_site_blobs( 119 + agent: &impl AgentSessionExt, 120 + _config: Config, 121 + local_sitemap: Sitemap, 122 + ) -> Result<Sitemap> { 123 // upload local site blobs 124 let mut new_sitemap: Sitemap = HashMap::new(); 125 for (k, v) in local_sitemap { ··· 130 } 131 }; 132 let blob = fs::read(blob).into_diagnostic()?; 133 134 let req = com_atproto::repo::upload_blob::UploadBlob::new() 135 .body(blob.into()) ··· 145 ); 146 } 147 148 + Ok(new_sitemap) 149 + } 150 + 151 + async fn update_remote_site( 152 + agent: &impl AgentSessionExt, 153 + config: Config, 154 + remote_records: Vec<String>, 155 + new_sitemap: Sitemap, 156 + ) -> Result<ApplyWritesOutput<'static>> { 157 // batch delete/upload records 158 let mut writes = Vec::new(); 159 let mut delete_records = remote_records ··· 203 writes.append(&mut create_records); 204 205 let req = com_atproto::repo::apply_writes::ApplyWrites::new() 206 + .repo(AtIdentifier::Did(config.user.into())) 207 .writes(writes) 208 .build(); 209 ··· 213 .await? 214 .into_output()?; 215 216 + Ok(res) 217 + } 218 + 219 + #[tokio::main] 220 + async fn main() -> Result<(), miette::Error> { 221 + env_logger::init(); 222 + // get config items 223 + let config = Config::parse(); 224 + 225 + // get local site info 226 + let local_sitemap = 227 + sitemap::local_sitemap(config.dir.clone(), config.all_files, config.git_ignore)?; 228 + 229 + // create session 230 + if let Some(password) = config.password.clone() { 231 + let password = password.into(); 232 + let client = JacquardResolver::default(); 233 + let store = MemorySessionStore::default(); 234 + let session = CredentialSession::new(store.into(), client.into()); 235 + 236 + let _ = session 237 + .login(config.user.clone().into(), password, None, None, None) 238 + .await?; 239 + 240 + let agent = Agent::from(session); 241 + 242 + let remote_sitemap = live_records(&agent, config.clone()).await?; 243 + let new_sitemap = upload_site_blobs(&agent, config.clone(), local_sitemap).await?; 244 + let writes_output = 245 + update_remote_site(&agent, config.clone(), remote_sitemap, new_sitemap).await?; 246 + println!("{writes_output:#?}"); 247 + } else { 248 + let oauth = oauth::client::OAuthClient::with_memory_store(); 249 + let session = oauth 250 + .login_with_local_server( 251 + config.user.clone(), 252 + AuthorizeOptions::default(), 253 + LoopbackConfig::default(), 254 + ) 255 + .await?; 256 + 257 + let agent = Agent::from(session); 258 + 259 + let remote_sitemap = live_records(&agent, config.clone()).await?; 260 + let new_sitemap = upload_site_blobs(&agent, config.clone(), local_sitemap).await?; 261 + let writes_output = 262 + update_remote_site(&agent, config.clone(), remote_sitemap, new_sitemap).await?; 263 + println!("{writes_output:#?}"); 264 + }; 265 266 Ok(()) 267 }