[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 1 use clap::{ArgAction, Parser}; 2 - use jacquard::api::com_atproto::repo::apply_writes::{self, ApplyWrites, ApplyWritesWritesItem}; 3 - use jacquard::atproto; 2 + use jacquard::api::com_atproto::repo::apply_writes::{ 3 + self, ApplyWrites, ApplyWritesOutput, ApplyWritesWritesItem, 4 + }; 4 5 use jacquard::client::MemorySessionStore; 5 - use jacquard::prelude::XrpcClient; 6 + use jacquard::client::{AgentSessionExt}; 7 + use jacquard::oauth::loopback::LoopbackConfig; 8 + use jacquard::oauth::types::AuthorizeOptions; 6 9 use jacquard::types::string::{AtStrError, RecordKey, Rkey}; 10 + use jacquard::{atproto, oauth}; 7 11 use jacquard::{ 8 12 Data, 9 13 api::com_atproto::{self, repo::list_records::ListRecords}, ··· 13 17 types::{ident::AtIdentifier, nsid::Nsid, string::AtprotoStr, uri::Uri}, 14 18 xrpc::XrpcExt, 15 19 }; 16 - use miette::{ErrReport, IntoDiagnostic, Result}; 20 + use miette::{IntoDiagnostic, Result}; 17 21 use std::{collections::HashMap, fs, path::PathBuf}; 18 22 19 23 use crate::sitemap::{BlobRef, Sitemap, SitemapNode}; ··· 21 25 mod sitemap; 22 26 mod utils; 23 27 24 - #[derive(Parser, Debug)] 28 + #[derive(Parser, Debug, Clone)] 25 29 #[command(version, about, long_about = None)] 26 30 struct Config { 27 31 /// Handle or DID to authenticate 28 - #[arg(verbatim_doc_comment, short, long)] 32 + #[arg(verbatim_doc_comment)] 29 33 user: String, 34 + 30 35 /// App password to authenticate the client 31 36 /// Normal passwords also work but are not advised 32 - #[arg(verbatim_doc_comment, short, long)] 33 - password: String, 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>, 34 41 35 42 /// Include dotfiles in upload 36 43 /// Default: false ··· 48 55 dir: PathBuf, 49 56 } 50 57 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 - 58 + async fn live_records(agent: &impl AgentSessionExt, config: Config) -> Result<Vec<String>> { 73 59 // find live site records 74 60 let mut cursor = None; 75 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 + }; 76 68 loop { 77 69 let req = com_atproto::repo::list_records::ListRecords::new() 78 70 .collection( 79 71 Nsid::new("dev.atcities.route").expect("failed to generate dev.atcities.route nsid"), 80 72 ) 81 - .repo(AtIdentifier::Did(auth.did.clone())) 73 + .repo(user.clone()) 82 74 .limit(100) 83 75 .maybe_cursor(cursor) 84 76 .build(); ··· 120 112 break; 121 113 } 122 114 } 115 + Ok(remote_records) 116 + } 123 117 118 + async fn upload_site_blobs( 119 + agent: &impl AgentSessionExt, 120 + _config: Config, 121 + local_sitemap: Sitemap, 122 + ) -> Result<Sitemap> { 124 123 // upload local site blobs 125 124 let mut new_sitemap: Sitemap = HashMap::new(); 126 125 for (k, v) in local_sitemap { ··· 131 130 } 132 131 }; 133 132 let blob = fs::read(blob).into_diagnostic()?; 134 - // let res = agent 135 - // .upload_blob(blob, v.mime_type.clone().into()) 136 - // .await?; 137 133 138 134 let req = com_atproto::repo::upload_blob::UploadBlob::new() 139 135 .body(blob.into()) ··· 149 145 ); 150 146 } 151 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>> { 152 157 // batch delete/upload records 153 158 let mut writes = Vec::new(); 154 159 let mut delete_records = remote_records ··· 198 203 writes.append(&mut create_records); 199 204 200 205 let req = com_atproto::repo::apply_writes::ApplyWrites::new() 201 - .repo(AtIdentifier::Did(auth.did.clone())) 206 + .repo(AtIdentifier::Did(config.user.into())) 202 207 .writes(writes) 203 208 .build(); 204 209 ··· 208 213 .await? 209 214 .into_output()?; 210 215 211 - println!("res: {res:#?}"); 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 + }; 212 265 213 266 Ok(()) 214 267 }