diff --git a/readme.md b/readme.md index 59b155e..f9c16f1 100644 --- a/readme.md +++ b/readme.md @@ -219,11 +219,87 @@ at-browser ## Tech Stack +- **CLI**: Rust (ailog) - **Frontend**: Vite + TypeScript - **ATProto**: @atproto/api - **OAuth**: @atproto/oauth-client-browser - **Markdown**: marked + highlight.js +## CLI (ailog) + +### Install + +```bash +cargo build --release +cp target/release/ailog ~/.local/bin/ +``` + +### Commands + +```bash +# Login to ATProto PDS +ailog login -p [-s ] + +# Post a record +ailog post -c [-r ] + +# Get records from collection +ailog get -c [-l ] + +# Delete a record +ailog delete -c -r + +# Sync PDS data to local content directory +ailog sync [-o ] + +# Generate lexicon Rust code from ATProto lexicons +ailog gen [-i ] [-o ] +``` + +### Example + +```bash +# Login +ailog login syui.syui.ai -p "app-password" -s syu.is + +# Post +echo '{"title":"Hello","content":"World","createdAt":"2025-01-01T00:00:00Z"}' > post.json +ailog post post.json -c ai.syui.log.post + +# Sync to local +ailog sync -o content +``` + +### Project Structure + +``` +src/ +├── main.rs +├── commands/ +│ ├── mod.rs +│ ├── auth.rs # login, refresh session +│ ├── token.rs # token management +│ ├── post.rs # post, get, delete, sync +│ └── gen.rs # lexicon code generation +└── lexicons/ + └── mod.rs # auto-generated from ATProto lexicons +``` + +### Lexicon Generation + +Generate Rust endpoint definitions from ATProto lexicon JSON files: + +```bash +# Clone atproto repo (if not exists) +git clone https://github.com/bluesky-social/atproto repos/atproto + +# Generate lexicons +ailog gen -i ./repos/atproto/lexicons -o ./src/lexicons + +# Rebuild +cargo build +``` + ## Collection Schema ### ai.syui.log.post diff --git a/src/auth.rs b/src/commands/auth.rs similarity index 91% rename from src/auth.rs rename to src/commands/auth.rs index 45304d7..6a93c82 100644 --- a/src/auth.rs +++ b/src/commands/auth.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use crate::token::{self, Session}; +use super::token::{self, Session}; +use crate::lexicons::{self, com_atproto_server}; #[derive(Debug, Serialize)] struct CreateSessionRequest { @@ -21,7 +22,7 @@ struct CreateSessionResponse { /// Login to ATProto PDS pub async fn login(handle: &str, password: &str, pds: &str) -> Result<()> { let client = reqwest::Client::new(); - let url = format!("https://{}/xrpc/com.atproto.server.createSession", pds); + let url = lexicons::url(pds, &com_atproto_server::CREATE_SESSION); let req = CreateSessionRequest { identifier: handle.to_string(), @@ -65,7 +66,7 @@ pub async fn refresh_session() -> Result { let pds = session.pds.as_deref().unwrap_or("bsky.social"); let client = reqwest::Client::new(); - let url = format!("https://{}/xrpc/com.atproto.server.refreshSession", pds); + let url = lexicons::url(pds, &com_atproto_server::REFRESH_SESSION); let res = client .post(&url) diff --git a/src/commands/gen.rs b/src/commands/gen.rs new file mode 100644 index 0000000..896657d --- /dev/null +++ b/src/commands/gen.rs @@ -0,0 +1,192 @@ +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +#[derive(Debug, Deserialize)] +struct Lexicon { + id: String, + defs: BTreeMap, +} + +#[derive(Debug, Deserialize)] +struct LexiconDef { + #[serde(rename = "type")] + def_type: Option, +} + +struct EndpointInfo { + nsid: String, + method: String, // GET or POST +} + +/// Generate lexicon Rust code from ATProto lexicon JSON files +pub fn generate(input: &str, output: &str) -> Result<()> { + let input_path = Path::new(input); + + if !input_path.exists() { + anyhow::bail!("Input directory does not exist: {}", input); + } + + println!("Scanning lexicons from: {}", input); + + // Collect all endpoints grouped by namespace + let mut namespaces: BTreeMap> = BTreeMap::new(); + + // Scan com/atproto directory + let atproto_path = input_path.join("com/atproto"); + if atproto_path.exists() { + scan_namespace(&atproto_path, "com.atproto", &mut namespaces)?; + } + + // Scan app/bsky directory + let bsky_path = input_path.join("app/bsky"); + if bsky_path.exists() { + scan_namespace(&bsky_path, "app.bsky", &mut namespaces)?; + } + + // Generate Rust code + let code = generate_rust_code(&namespaces); + + // Write output + let output_path = Path::new(output).join("mod.rs"); + fs::create_dir_all(output)?; + fs::write(&output_path, &code)?; + + println!("Generated: {}", output_path.display()); + println!("Total namespaces: {}", namespaces.len()); + let total_endpoints: usize = namespaces.values().map(|v| v.len()).sum(); + println!("Total endpoints: {}", total_endpoints); + + Ok(()) +} + +fn scan_namespace( + base_path: &Path, + prefix: &str, + namespaces: &mut BTreeMap>, +) -> Result<()> { + for entry in fs::read_dir(base_path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + let ns_name = path.file_name() + .and_then(|n| n.to_str()) + .context("Invalid directory name")?; + + let full_ns = format!("{}.{}", prefix, ns_name); + let mut endpoints = Vec::new(); + + // Scan JSON files in this namespace + for file_entry in fs::read_dir(&path)? { + let file_entry = file_entry?; + let file_path = file_entry.path(); + + if file_path.extension().map(|e| e == "json").unwrap_or(false) { + if let Some(endpoint) = parse_lexicon_file(&file_path)? { + endpoints.push(endpoint); + } + } + } + + if !endpoints.is_empty() { + endpoints.sort_by(|a, b| a.nsid.cmp(&b.nsid)); + namespaces.insert(full_ns, endpoints); + } + } + } + + Ok(()) +} + +fn parse_lexicon_file(path: &Path) -> Result> { + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read: {}", path.display()))?; + + let lexicon: Lexicon = serde_json::from_str(&content) + .with_context(|| format!("Failed to parse: {}", path.display()))?; + + // Get the main definition type + let main_def = match lexicon.defs.get("main") { + Some(def) => def, + None => return Ok(None), + }; + + let method = match main_def.def_type.as_deref() { + Some("query") => "GET", + Some("procedure") => "POST", + Some("subscription") => return Ok(None), // Skip websocket subscriptions + _ => return Ok(None), // Skip records, tokens, etc. + }; + + Ok(Some(EndpointInfo { + nsid: lexicon.id, + method: method.to_string(), + })) +} + +fn generate_rust_code(namespaces: &BTreeMap>) -> String { + let mut code = String::new(); + + // Header + code.push_str("//! Auto-generated from ATProto lexicons\n"); + code.push_str("//! Run `ailog gen` to regenerate\n"); + code.push_str("//! Do not edit manually\n\n"); + + // Endpoint struct + code.push_str("#[derive(Debug, Clone, Copy)]\n"); + code.push_str("pub struct Endpoint {\n"); + code.push_str(" pub nsid: &'static str,\n"); + code.push_str(" pub method: &'static str,\n"); + code.push_str("}\n\n"); + + // URL helper function + code.push_str("/// Build XRPC URL for an endpoint\n"); + code.push_str("pub fn url(pds: &str, endpoint: &Endpoint) -> String {\n"); + code.push_str(" format!(\"https://{}/xrpc/{}\", pds, endpoint.nsid)\n"); + code.push_str("}\n\n"); + + // Generate modules for each namespace + for (ns, endpoints) in namespaces { + // Convert namespace to module name: com.atproto.repo -> com_atproto_repo + let mod_name = ns.replace('.', "_"); + + code.push_str(&format!("pub mod {} {{\n", mod_name)); + code.push_str(" use super::Endpoint;\n\n"); + + for endpoint in endpoints { + // Extract the method name from NSID: com.atproto.repo.listRecords -> LIST_RECORDS + let method_name = endpoint.nsid + .rsplit('.') + .next() + .unwrap_or(&endpoint.nsid); + + // Convert camelCase to SCREAMING_SNAKE_CASE + let const_name = to_screaming_snake_case(method_name); + + code.push_str(&format!( + " pub const {}: Endpoint = Endpoint {{ nsid: \"{}\", method: \"{}\" }};\n", + const_name, endpoint.nsid, endpoint.method + )); + } + + code.push_str("}\n\n"); + } + + code +} + +fn to_screaming_snake_case(s: &str) -> String { + let mut result = String::new(); + + for (i, c) in s.chars().enumerate() { + if c.is_uppercase() && i > 0 { + result.push('_'); + } + result.push(c.to_ascii_uppercase()); + } + + result +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..78d475d --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod token; +pub mod post; +pub mod gen; diff --git a/src/post.rs b/src/commands/post.rs similarity index 89% rename from src/post.rs rename to src/commands/post.rs index f560939..378bd4f 100644 --- a/src/post.rs +++ b/src/commands/post.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::fs; -use crate::auth; +use super::auth; +use crate::lexicons::{self, com_atproto_repo, com_atproto_identity}; #[derive(Debug, Serialize)] struct PutRecordRequest { @@ -55,20 +56,17 @@ fn generate_tid() -> String { /// Put a record to ATProto pub async fn put_record(file: &str, collection: &str, rkey: Option<&str>) -> Result<()> { - // Refresh token first let session = auth::refresh_session().await?; let pds = session.pds.as_deref().unwrap_or("bsky.social"); - // Load record from file let content = fs::read_to_string(file) .with_context(|| format!("Failed to read file: {}", file))?; let record: Value = serde_json::from_str(&content)?; - // Generate rkey if not provided let rkey = rkey.map(|s| s.to_string()).unwrap_or_else(generate_tid); let client = reqwest::Client::new(); - let url = format!("https://{}/xrpc/com.atproto.repo.putRecord", pds); + let url = lexicons::url(pds, &com_atproto_repo::PUT_RECORD); let req = PutRecordRequest { repo: session.did.clone(), @@ -103,23 +101,20 @@ pub async fn put_record(file: &str, collection: &str, rkey: Option<&str>) -> Res /// Put a lexicon schema pub async fn put_lexicon(file: &str) -> Result<()> { - // Refresh token first let session = auth::refresh_session().await?; let pds = session.pds.as_deref().unwrap_or("bsky.social"); - // Load lexicon from file let content = fs::read_to_string(file) .with_context(|| format!("Failed to read file: {}", file))?; let lexicon: Value = serde_json::from_str(&content)?; - // Get lexicon id for rkey let lexicon_id = lexicon["id"] .as_str() .context("Lexicon file must have 'id' field")? .to_string(); let client = reqwest::Client::new(); - let url = format!("https://{}/xrpc/com.atproto.repo.putRecord", pds); + let url = lexicons::url(pds, &com_atproto_repo::PUT_RECORD); let req = PutRecordRequest { repo: session.did.clone(), @@ -158,9 +153,10 @@ pub async fn get_records(collection: &str, limit: u32) -> Result<()> { let pds = session.pds.as_deref().unwrap_or("bsky.social"); let client = reqwest::Client::new(); + let base_url = lexicons::url(pds, &com_atproto_repo::LIST_RECORDS); let url = format!( - "https://{}/xrpc/com.atproto.repo.listRecords?repo={}&collection={}&limit={}", - pds, session.did, collection, limit + "{}?repo={}&collection={}&limit={}", + base_url, session.did, collection, limit ); let res = client @@ -194,7 +190,7 @@ pub async fn delete_record(collection: &str, rkey: &str) -> Result<()> { let pds = session.pds.as_deref().unwrap_or("bsky.social"); let client = reqwest::Client::new(); - let url = format!("https://{}/xrpc/com.atproto.repo.deleteRecord", pds); + let url = lexicons::url(pds, &com_atproto_repo::DELETE_RECORD); let req = DeleteRecordRequest { repo: session.did.clone(), @@ -238,7 +234,6 @@ struct DescribeRepoResponse { /// Sync PDS data to local content directory pub async fn sync_to_local(output: &str) -> Result<()> { - // Load config.json let config_content = fs::read_to_string("config.json") .context("config.json not found")?; let config: Config = serde_json::from_str(&config_content)?; @@ -247,9 +242,10 @@ pub async fn sync_to_local(output: &str) -> Result<()> { let client = reqwest::Client::new(); - // Resolve handle to DID and PDS + // Resolve handle to DID let resolve_url = format!( - "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle={}", + "{}?handle={}", + lexicons::url("public.api.bsky.app", &com_atproto_identity::RESOLVE_HANDLE), config.handle ); let res = client.get(&resolve_url).send().await?; @@ -272,14 +268,18 @@ pub async fn sync_to_local(output: &str) -> Result<()> { println!("PDS: {}", pds); + // Remove https:// prefix for lexicons::url + let pds_host = pds.trim_start_matches("https://"); + // Create output directory let did_dir = format!("{}/{}", output, did); fs::create_dir_all(&did_dir)?; // 1. Sync describeRepo let describe_url = format!( - "{}/xrpc/com.atproto.repo.describeRepo?repo={}", - pds, did + "{}?repo={}", + lexicons::url(pds_host, &com_atproto_repo::DESCRIBE_REPO), + did ); let res = client.get(&describe_url).send().await?; let describe: DescribeRepoResponse = res.json().await?; @@ -295,8 +295,9 @@ pub async fn sync_to_local(output: &str) -> Result<()> { // 2. Sync profile let profile_url = format!( - "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=app.bsky.actor.profile&rkey=self", - pds, did + "{}?repo={}&collection=app.bsky.actor.profile&rkey=self", + lexicons::url(pds_host, &com_atproto_repo::GET_RECORD), + did ); let res = client.get(&profile_url).send().await?; if res.status().is_success() { @@ -308,11 +309,12 @@ pub async fn sync_to_local(output: &str) -> Result<()> { println!("Saved: {}", profile_path); } - // 3. Sync collection records (from config or default) + // 3. Sync collection records let collection = config.collection.as_deref().unwrap_or("ai.syui.log.post"); let records_url = format!( - "{}/xrpc/com.atproto.repo.listRecords?repo={}&collection={}&limit=100", - pds, did, collection + "{}?repo={}&collection={}&limit=100", + lexicons::url(pds_host, &com_atproto_repo::LIST_RECORDS), + did, collection ); let res = client.get(&records_url).send().await?; if res.status().is_success() { diff --git a/src/token.rs b/src/commands/token.rs similarity index 100% rename from src/token.rs rename to src/commands/token.rs diff --git a/src/lexicons/mod.rs b/src/lexicons/mod.rs new file mode 100644 index 0000000..f54e4a6 --- /dev/null +++ b/src/lexicons/mod.rs @@ -0,0 +1,301 @@ +//! Auto-generated from ATProto lexicons +//! Run `ailog gen` to regenerate +//! Do not edit manually + +#[derive(Debug, Clone, Copy)] +pub struct Endpoint { + pub nsid: &'static str, + pub method: &'static str, +} + +/// Build XRPC URL for an endpoint +pub fn url(pds: &str, endpoint: &Endpoint) -> String { + format!("https://{}/xrpc/{}", pds, endpoint.nsid) +} + +pub mod app_bsky_actor { + use super::Endpoint; + + pub const GET_PREFERENCES: Endpoint = Endpoint { nsid: "app.bsky.actor.getPreferences", method: "GET" }; + pub const GET_PROFILE: Endpoint = Endpoint { nsid: "app.bsky.actor.getProfile", method: "GET" }; + pub const GET_PROFILES: Endpoint = Endpoint { nsid: "app.bsky.actor.getProfiles", method: "GET" }; + pub const GET_SUGGESTIONS: Endpoint = Endpoint { nsid: "app.bsky.actor.getSuggestions", method: "GET" }; + pub const PUT_PREFERENCES: Endpoint = Endpoint { nsid: "app.bsky.actor.putPreferences", method: "POST" }; + pub const SEARCH_ACTORS: Endpoint = Endpoint { nsid: "app.bsky.actor.searchActors", method: "GET" }; + pub const SEARCH_ACTORS_TYPEAHEAD: Endpoint = Endpoint { nsid: "app.bsky.actor.searchActorsTypeahead", method: "GET" }; +} + +pub mod app_bsky_ageassurance { + use super::Endpoint; + + pub const BEGIN: Endpoint = Endpoint { nsid: "app.bsky.ageassurance.begin", method: "POST" }; + pub const GET_CONFIG: Endpoint = Endpoint { nsid: "app.bsky.ageassurance.getConfig", method: "GET" }; + pub const GET_STATE: Endpoint = Endpoint { nsid: "app.bsky.ageassurance.getState", method: "GET" }; +} + +pub mod app_bsky_bookmark { + use super::Endpoint; + + pub const CREATE_BOOKMARK: Endpoint = Endpoint { nsid: "app.bsky.bookmark.createBookmark", method: "POST" }; + pub const DELETE_BOOKMARK: Endpoint = Endpoint { nsid: "app.bsky.bookmark.deleteBookmark", method: "POST" }; + pub const GET_BOOKMARKS: Endpoint = Endpoint { nsid: "app.bsky.bookmark.getBookmarks", method: "GET" }; +} + +pub mod app_bsky_contact { + use super::Endpoint; + + pub const DISMISS_MATCH: Endpoint = Endpoint { nsid: "app.bsky.contact.dismissMatch", method: "POST" }; + pub const GET_MATCHES: Endpoint = Endpoint { nsid: "app.bsky.contact.getMatches", method: "GET" }; + pub const GET_SYNC_STATUS: Endpoint = Endpoint { nsid: "app.bsky.contact.getSyncStatus", method: "GET" }; + pub const IMPORT_CONTACTS: Endpoint = Endpoint { nsid: "app.bsky.contact.importContacts", method: "POST" }; + pub const REMOVE_DATA: Endpoint = Endpoint { nsid: "app.bsky.contact.removeData", method: "POST" }; + pub const SEND_NOTIFICATION: Endpoint = Endpoint { nsid: "app.bsky.contact.sendNotification", method: "POST" }; + pub const START_PHONE_VERIFICATION: Endpoint = Endpoint { nsid: "app.bsky.contact.startPhoneVerification", method: "POST" }; + pub const VERIFY_PHONE: Endpoint = Endpoint { nsid: "app.bsky.contact.verifyPhone", method: "POST" }; +} + +pub mod app_bsky_draft { + use super::Endpoint; + + pub const CREATE_DRAFT: Endpoint = Endpoint { nsid: "app.bsky.draft.createDraft", method: "POST" }; + pub const DELETE_DRAFT: Endpoint = Endpoint { nsid: "app.bsky.draft.deleteDraft", method: "POST" }; + pub const GET_DRAFTS: Endpoint = Endpoint { nsid: "app.bsky.draft.getDrafts", method: "GET" }; + pub const UPDATE_DRAFT: Endpoint = Endpoint { nsid: "app.bsky.draft.updateDraft", method: "POST" }; +} + +pub mod app_bsky_feed { + use super::Endpoint; + + pub const DESCRIBE_FEED_GENERATOR: Endpoint = Endpoint { nsid: "app.bsky.feed.describeFeedGenerator", method: "GET" }; + pub const GET_ACTOR_FEEDS: Endpoint = Endpoint { nsid: "app.bsky.feed.getActorFeeds", method: "GET" }; + pub const GET_ACTOR_LIKES: Endpoint = Endpoint { nsid: "app.bsky.feed.getActorLikes", method: "GET" }; + pub const GET_AUTHOR_FEED: Endpoint = Endpoint { nsid: "app.bsky.feed.getAuthorFeed", method: "GET" }; + pub const GET_FEED: Endpoint = Endpoint { nsid: "app.bsky.feed.getFeed", method: "GET" }; + pub const GET_FEED_GENERATOR: Endpoint = Endpoint { nsid: "app.bsky.feed.getFeedGenerator", method: "GET" }; + pub const GET_FEED_GENERATORS: Endpoint = Endpoint { nsid: "app.bsky.feed.getFeedGenerators", method: "GET" }; + pub const GET_FEED_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.feed.getFeedSkeleton", method: "GET" }; + pub const GET_LIKES: Endpoint = Endpoint { nsid: "app.bsky.feed.getLikes", method: "GET" }; + pub const GET_LIST_FEED: Endpoint = Endpoint { nsid: "app.bsky.feed.getListFeed", method: "GET" }; + pub const GET_POST_THREAD: Endpoint = Endpoint { nsid: "app.bsky.feed.getPostThread", method: "GET" }; + pub const GET_POSTS: Endpoint = Endpoint { nsid: "app.bsky.feed.getPosts", method: "GET" }; + pub const GET_QUOTES: Endpoint = Endpoint { nsid: "app.bsky.feed.getQuotes", method: "GET" }; + pub const GET_REPOSTED_BY: Endpoint = Endpoint { nsid: "app.bsky.feed.getRepostedBy", method: "GET" }; + pub const GET_SUGGESTED_FEEDS: Endpoint = Endpoint { nsid: "app.bsky.feed.getSuggestedFeeds", method: "GET" }; + pub const GET_TIMELINE: Endpoint = Endpoint { nsid: "app.bsky.feed.getTimeline", method: "GET" }; + pub const SEARCH_POSTS: Endpoint = Endpoint { nsid: "app.bsky.feed.searchPosts", method: "GET" }; + pub const SEND_INTERACTIONS: Endpoint = Endpoint { nsid: "app.bsky.feed.sendInteractions", method: "POST" }; +} + +pub mod app_bsky_graph { + use super::Endpoint; + + pub const GET_ACTOR_STARTER_PACKS: Endpoint = Endpoint { nsid: "app.bsky.graph.getActorStarterPacks", method: "GET" }; + pub const GET_BLOCKS: Endpoint = Endpoint { nsid: "app.bsky.graph.getBlocks", method: "GET" }; + pub const GET_FOLLOWERS: Endpoint = Endpoint { nsid: "app.bsky.graph.getFollowers", method: "GET" }; + pub const GET_FOLLOWS: Endpoint = Endpoint { nsid: "app.bsky.graph.getFollows", method: "GET" }; + pub const GET_KNOWN_FOLLOWERS: Endpoint = Endpoint { nsid: "app.bsky.graph.getKnownFollowers", method: "GET" }; + pub const GET_LIST: Endpoint = Endpoint { nsid: "app.bsky.graph.getList", method: "GET" }; + pub const GET_LIST_BLOCKS: Endpoint = Endpoint { nsid: "app.bsky.graph.getListBlocks", method: "GET" }; + pub const GET_LIST_MUTES: Endpoint = Endpoint { nsid: "app.bsky.graph.getListMutes", method: "GET" }; + pub const GET_LISTS: Endpoint = Endpoint { nsid: "app.bsky.graph.getLists", method: "GET" }; + pub const GET_LISTS_WITH_MEMBERSHIP: Endpoint = Endpoint { nsid: "app.bsky.graph.getListsWithMembership", method: "GET" }; + pub const GET_MUTES: Endpoint = Endpoint { nsid: "app.bsky.graph.getMutes", method: "GET" }; + pub const GET_RELATIONSHIPS: Endpoint = Endpoint { nsid: "app.bsky.graph.getRelationships", method: "GET" }; + pub const GET_STARTER_PACK: Endpoint = Endpoint { nsid: "app.bsky.graph.getStarterPack", method: "GET" }; + pub const GET_STARTER_PACKS: Endpoint = Endpoint { nsid: "app.bsky.graph.getStarterPacks", method: "GET" }; + pub const GET_STARTER_PACKS_WITH_MEMBERSHIP: Endpoint = Endpoint { nsid: "app.bsky.graph.getStarterPacksWithMembership", method: "GET" }; + pub const GET_SUGGESTED_FOLLOWS_BY_ACTOR: Endpoint = Endpoint { nsid: "app.bsky.graph.getSuggestedFollowsByActor", method: "GET" }; + pub const MUTE_ACTOR: Endpoint = Endpoint { nsid: "app.bsky.graph.muteActor", method: "POST" }; + pub const MUTE_ACTOR_LIST: Endpoint = Endpoint { nsid: "app.bsky.graph.muteActorList", method: "POST" }; + pub const MUTE_THREAD: Endpoint = Endpoint { nsid: "app.bsky.graph.muteThread", method: "POST" }; + pub const SEARCH_STARTER_PACKS: Endpoint = Endpoint { nsid: "app.bsky.graph.searchStarterPacks", method: "GET" }; + pub const UNMUTE_ACTOR: Endpoint = Endpoint { nsid: "app.bsky.graph.unmuteActor", method: "POST" }; + pub const UNMUTE_ACTOR_LIST: Endpoint = Endpoint { nsid: "app.bsky.graph.unmuteActorList", method: "POST" }; + pub const UNMUTE_THREAD: Endpoint = Endpoint { nsid: "app.bsky.graph.unmuteThread", method: "POST" }; +} + +pub mod app_bsky_labeler { + use super::Endpoint; + + pub const GET_SERVICES: Endpoint = Endpoint { nsid: "app.bsky.labeler.getServices", method: "GET" }; +} + +pub mod app_bsky_notification { + use super::Endpoint; + + pub const GET_PREFERENCES: Endpoint = Endpoint { nsid: "app.bsky.notification.getPreferences", method: "GET" }; + pub const GET_UNREAD_COUNT: Endpoint = Endpoint { nsid: "app.bsky.notification.getUnreadCount", method: "GET" }; + pub const LIST_ACTIVITY_SUBSCRIPTIONS: Endpoint = Endpoint { nsid: "app.bsky.notification.listActivitySubscriptions", method: "GET" }; + pub const LIST_NOTIFICATIONS: Endpoint = Endpoint { nsid: "app.bsky.notification.listNotifications", method: "GET" }; + pub const PUT_ACTIVITY_SUBSCRIPTION: Endpoint = Endpoint { nsid: "app.bsky.notification.putActivitySubscription", method: "POST" }; + pub const PUT_PREFERENCES: Endpoint = Endpoint { nsid: "app.bsky.notification.putPreferences", method: "POST" }; + pub const PUT_PREFERENCES_V2: Endpoint = Endpoint { nsid: "app.bsky.notification.putPreferencesV2", method: "POST" }; + pub const REGISTER_PUSH: Endpoint = Endpoint { nsid: "app.bsky.notification.registerPush", method: "POST" }; + pub const UNREGISTER_PUSH: Endpoint = Endpoint { nsid: "app.bsky.notification.unregisterPush", method: "POST" }; + pub const UPDATE_SEEN: Endpoint = Endpoint { nsid: "app.bsky.notification.updateSeen", method: "POST" }; +} + +pub mod app_bsky_unspecced { + use super::Endpoint; + + pub const GET_AGE_ASSURANCE_STATE: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getAgeAssuranceState", method: "GET" }; + pub const GET_CONFIG: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getConfig", method: "GET" }; + pub const GET_ONBOARDING_SUGGESTED_STARTER_PACKS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getOnboardingSuggestedStarterPacks", method: "GET" }; + pub const GET_ONBOARDING_SUGGESTED_STARTER_PACKS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getOnboardingSuggestedStarterPacksSkeleton", method: "GET" }; + pub const GET_POPULAR_FEED_GENERATORS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getPopularFeedGenerators", method: "GET" }; + pub const GET_POST_THREAD_OTHER_V2: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getPostThreadOtherV2", method: "GET" }; + pub const GET_POST_THREAD_V2: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getPostThreadV2", method: "GET" }; + pub const GET_SUGGESTED_FEEDS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getSuggestedFeeds", method: "GET" }; + pub const GET_SUGGESTED_FEEDS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getSuggestedFeedsSkeleton", method: "GET" }; + pub const GET_SUGGESTED_STARTER_PACKS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getSuggestedStarterPacks", method: "GET" }; + pub const GET_SUGGESTED_STARTER_PACKS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getSuggestedStarterPacksSkeleton", method: "GET" }; + pub const GET_SUGGESTED_USERS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getSuggestedUsers", method: "GET" }; + pub const GET_SUGGESTED_USERS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getSuggestedUsersSkeleton", method: "GET" }; + pub const GET_SUGGESTIONS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getSuggestionsSkeleton", method: "GET" }; + pub const GET_TAGGED_SUGGESTIONS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getTaggedSuggestions", method: "GET" }; + pub const GET_TRENDING_TOPICS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getTrendingTopics", method: "GET" }; + pub const GET_TRENDS: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getTrends", method: "GET" }; + pub const GET_TRENDS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.getTrendsSkeleton", method: "GET" }; + pub const INIT_AGE_ASSURANCE: Endpoint = Endpoint { nsid: "app.bsky.unspecced.initAgeAssurance", method: "POST" }; + pub const SEARCH_ACTORS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.searchActorsSkeleton", method: "GET" }; + pub const SEARCH_POSTS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.searchPostsSkeleton", method: "GET" }; + pub const SEARCH_STARTER_PACKS_SKELETON: Endpoint = Endpoint { nsid: "app.bsky.unspecced.searchStarterPacksSkeleton", method: "GET" }; +} + +pub mod app_bsky_video { + use super::Endpoint; + + pub const GET_JOB_STATUS: Endpoint = Endpoint { nsid: "app.bsky.video.getJobStatus", method: "GET" }; + pub const GET_UPLOAD_LIMITS: Endpoint = Endpoint { nsid: "app.bsky.video.getUploadLimits", method: "GET" }; + pub const UPLOAD_VIDEO: Endpoint = Endpoint { nsid: "app.bsky.video.uploadVideo", method: "POST" }; +} + +pub mod com_atproto_admin { + use super::Endpoint; + + pub const DELETE_ACCOUNT: Endpoint = Endpoint { nsid: "com.atproto.admin.deleteAccount", method: "POST" }; + pub const DISABLE_ACCOUNT_INVITES: Endpoint = Endpoint { nsid: "com.atproto.admin.disableAccountInvites", method: "POST" }; + pub const DISABLE_INVITE_CODES: Endpoint = Endpoint { nsid: "com.atproto.admin.disableInviteCodes", method: "POST" }; + pub const ENABLE_ACCOUNT_INVITES: Endpoint = Endpoint { nsid: "com.atproto.admin.enableAccountInvites", method: "POST" }; + pub const GET_ACCOUNT_INFO: Endpoint = Endpoint { nsid: "com.atproto.admin.getAccountInfo", method: "GET" }; + pub const GET_ACCOUNT_INFOS: Endpoint = Endpoint { nsid: "com.atproto.admin.getAccountInfos", method: "GET" }; + pub const GET_INVITE_CODES: Endpoint = Endpoint { nsid: "com.atproto.admin.getInviteCodes", method: "GET" }; + pub const GET_SUBJECT_STATUS: Endpoint = Endpoint { nsid: "com.atproto.admin.getSubjectStatus", method: "GET" }; + pub const SEARCH_ACCOUNTS: Endpoint = Endpoint { nsid: "com.atproto.admin.searchAccounts", method: "GET" }; + pub const SEND_EMAIL: Endpoint = Endpoint { nsid: "com.atproto.admin.sendEmail", method: "POST" }; + pub const UPDATE_ACCOUNT_EMAIL: Endpoint = Endpoint { nsid: "com.atproto.admin.updateAccountEmail", method: "POST" }; + pub const UPDATE_ACCOUNT_HANDLE: Endpoint = Endpoint { nsid: "com.atproto.admin.updateAccountHandle", method: "POST" }; + pub const UPDATE_ACCOUNT_PASSWORD: Endpoint = Endpoint { nsid: "com.atproto.admin.updateAccountPassword", method: "POST" }; + pub const UPDATE_ACCOUNT_SIGNING_KEY: Endpoint = Endpoint { nsid: "com.atproto.admin.updateAccountSigningKey", method: "POST" }; + pub const UPDATE_SUBJECT_STATUS: Endpoint = Endpoint { nsid: "com.atproto.admin.updateSubjectStatus", method: "POST" }; +} + +pub mod com_atproto_identity { + use super::Endpoint; + + pub const GET_RECOMMENDED_DID_CREDENTIALS: Endpoint = Endpoint { nsid: "com.atproto.identity.getRecommendedDidCredentials", method: "GET" }; + pub const REFRESH_IDENTITY: Endpoint = Endpoint { nsid: "com.atproto.identity.refreshIdentity", method: "POST" }; + pub const REQUEST_PLC_OPERATION_SIGNATURE: Endpoint = Endpoint { nsid: "com.atproto.identity.requestPlcOperationSignature", method: "POST" }; + pub const RESOLVE_DID: Endpoint = Endpoint { nsid: "com.atproto.identity.resolveDid", method: "GET" }; + pub const RESOLVE_HANDLE: Endpoint = Endpoint { nsid: "com.atproto.identity.resolveHandle", method: "GET" }; + pub const RESOLVE_IDENTITY: Endpoint = Endpoint { nsid: "com.atproto.identity.resolveIdentity", method: "GET" }; + pub const SIGN_PLC_OPERATION: Endpoint = Endpoint { nsid: "com.atproto.identity.signPlcOperation", method: "POST" }; + pub const SUBMIT_PLC_OPERATION: Endpoint = Endpoint { nsid: "com.atproto.identity.submitPlcOperation", method: "POST" }; + pub const UPDATE_HANDLE: Endpoint = Endpoint { nsid: "com.atproto.identity.updateHandle", method: "POST" }; +} + +pub mod com_atproto_label { + use super::Endpoint; + + pub const QUERY_LABELS: Endpoint = Endpoint { nsid: "com.atproto.label.queryLabels", method: "GET" }; +} + +pub mod com_atproto_lexicon { + use super::Endpoint; + + pub const RESOLVE_LEXICON: Endpoint = Endpoint { nsid: "com.atproto.lexicon.resolveLexicon", method: "GET" }; +} + +pub mod com_atproto_moderation { + use super::Endpoint; + + pub const CREATE_REPORT: Endpoint = Endpoint { nsid: "com.atproto.moderation.createReport", method: "POST" }; +} + +pub mod com_atproto_repo { + use super::Endpoint; + + pub const APPLY_WRITES: Endpoint = Endpoint { nsid: "com.atproto.repo.applyWrites", method: "POST" }; + pub const CREATE_RECORD: Endpoint = Endpoint { nsid: "com.atproto.repo.createRecord", method: "POST" }; + pub const DELETE_RECORD: Endpoint = Endpoint { nsid: "com.atproto.repo.deleteRecord", method: "POST" }; + pub const DESCRIBE_REPO: Endpoint = Endpoint { nsid: "com.atproto.repo.describeRepo", method: "GET" }; + pub const GET_RECORD: Endpoint = Endpoint { nsid: "com.atproto.repo.getRecord", method: "GET" }; + pub const IMPORT_REPO: Endpoint = Endpoint { nsid: "com.atproto.repo.importRepo", method: "POST" }; + pub const LIST_MISSING_BLOBS: Endpoint = Endpoint { nsid: "com.atproto.repo.listMissingBlobs", method: "GET" }; + pub const LIST_RECORDS: Endpoint = Endpoint { nsid: "com.atproto.repo.listRecords", method: "GET" }; + pub const PUT_RECORD: Endpoint = Endpoint { nsid: "com.atproto.repo.putRecord", method: "POST" }; + pub const UPLOAD_BLOB: Endpoint = Endpoint { nsid: "com.atproto.repo.uploadBlob", method: "POST" }; +} + +pub mod com_atproto_server { + use super::Endpoint; + + pub const ACTIVATE_ACCOUNT: Endpoint = Endpoint { nsid: "com.atproto.server.activateAccount", method: "POST" }; + pub const CHECK_ACCOUNT_STATUS: Endpoint = Endpoint { nsid: "com.atproto.server.checkAccountStatus", method: "GET" }; + pub const CONFIRM_EMAIL: Endpoint = Endpoint { nsid: "com.atproto.server.confirmEmail", method: "POST" }; + pub const CREATE_ACCOUNT: Endpoint = Endpoint { nsid: "com.atproto.server.createAccount", method: "POST" }; + pub const CREATE_APP_PASSWORD: Endpoint = Endpoint { nsid: "com.atproto.server.createAppPassword", method: "POST" }; + pub const CREATE_INVITE_CODE: Endpoint = Endpoint { nsid: "com.atproto.server.createInviteCode", method: "POST" }; + pub const CREATE_INVITE_CODES: Endpoint = Endpoint { nsid: "com.atproto.server.createInviteCodes", method: "POST" }; + pub const CREATE_SESSION: Endpoint = Endpoint { nsid: "com.atproto.server.createSession", method: "POST" }; + pub const DEACTIVATE_ACCOUNT: Endpoint = Endpoint { nsid: "com.atproto.server.deactivateAccount", method: "POST" }; + pub const DELETE_ACCOUNT: Endpoint = Endpoint { nsid: "com.atproto.server.deleteAccount", method: "POST" }; + pub const DELETE_SESSION: Endpoint = Endpoint { nsid: "com.atproto.server.deleteSession", method: "POST" }; + pub const DESCRIBE_SERVER: Endpoint = Endpoint { nsid: "com.atproto.server.describeServer", method: "GET" }; + pub const GET_ACCOUNT_INVITE_CODES: Endpoint = Endpoint { nsid: "com.atproto.server.getAccountInviteCodes", method: "GET" }; + pub const GET_SERVICE_AUTH: Endpoint = Endpoint { nsid: "com.atproto.server.getServiceAuth", method: "GET" }; + pub const GET_SESSION: Endpoint = Endpoint { nsid: "com.atproto.server.getSession", method: "GET" }; + pub const LIST_APP_PASSWORDS: Endpoint = Endpoint { nsid: "com.atproto.server.listAppPasswords", method: "GET" }; + pub const REFRESH_SESSION: Endpoint = Endpoint { nsid: "com.atproto.server.refreshSession", method: "POST" }; + pub const REQUEST_ACCOUNT_DELETE: Endpoint = Endpoint { nsid: "com.atproto.server.requestAccountDelete", method: "POST" }; + pub const REQUEST_EMAIL_CONFIRMATION: Endpoint = Endpoint { nsid: "com.atproto.server.requestEmailConfirmation", method: "POST" }; + pub const REQUEST_EMAIL_UPDATE: Endpoint = Endpoint { nsid: "com.atproto.server.requestEmailUpdate", method: "POST" }; + pub const REQUEST_PASSWORD_RESET: Endpoint = Endpoint { nsid: "com.atproto.server.requestPasswordReset", method: "POST" }; + pub const RESERVE_SIGNING_KEY: Endpoint = Endpoint { nsid: "com.atproto.server.reserveSigningKey", method: "POST" }; + pub const RESET_PASSWORD: Endpoint = Endpoint { nsid: "com.atproto.server.resetPassword", method: "POST" }; + pub const REVOKE_APP_PASSWORD: Endpoint = Endpoint { nsid: "com.atproto.server.revokeAppPassword", method: "POST" }; + pub const UPDATE_EMAIL: Endpoint = Endpoint { nsid: "com.atproto.server.updateEmail", method: "POST" }; +} + +pub mod com_atproto_sync { + use super::Endpoint; + + pub const GET_BLOB: Endpoint = Endpoint { nsid: "com.atproto.sync.getBlob", method: "GET" }; + pub const GET_BLOCKS: Endpoint = Endpoint { nsid: "com.atproto.sync.getBlocks", method: "GET" }; + pub const GET_CHECKOUT: Endpoint = Endpoint { nsid: "com.atproto.sync.getCheckout", method: "GET" }; + pub const GET_HEAD: Endpoint = Endpoint { nsid: "com.atproto.sync.getHead", method: "GET" }; + pub const GET_HOST_STATUS: Endpoint = Endpoint { nsid: "com.atproto.sync.getHostStatus", method: "GET" }; + pub const GET_LATEST_COMMIT: Endpoint = Endpoint { nsid: "com.atproto.sync.getLatestCommit", method: "GET" }; + pub const GET_RECORD: Endpoint = Endpoint { nsid: "com.atproto.sync.getRecord", method: "GET" }; + pub const GET_REPO: Endpoint = Endpoint { nsid: "com.atproto.sync.getRepo", method: "GET" }; + pub const GET_REPO_STATUS: Endpoint = Endpoint { nsid: "com.atproto.sync.getRepoStatus", method: "GET" }; + pub const LIST_BLOBS: Endpoint = Endpoint { nsid: "com.atproto.sync.listBlobs", method: "GET" }; + pub const LIST_HOSTS: Endpoint = Endpoint { nsid: "com.atproto.sync.listHosts", method: "GET" }; + pub const LIST_REPOS: Endpoint = Endpoint { nsid: "com.atproto.sync.listRepos", method: "GET" }; + pub const LIST_REPOS_BY_COLLECTION: Endpoint = Endpoint { nsid: "com.atproto.sync.listReposByCollection", method: "GET" }; + pub const NOTIFY_OF_UPDATE: Endpoint = Endpoint { nsid: "com.atproto.sync.notifyOfUpdate", method: "POST" }; + pub const REQUEST_CRAWL: Endpoint = Endpoint { nsid: "com.atproto.sync.requestCrawl", method: "POST" }; +} + +pub mod com_atproto_temp { + use super::Endpoint; + + pub const ADD_RESERVED_HANDLE: Endpoint = Endpoint { nsid: "com.atproto.temp.addReservedHandle", method: "POST" }; + pub const CHECK_HANDLE_AVAILABILITY: Endpoint = Endpoint { nsid: "com.atproto.temp.checkHandleAvailability", method: "GET" }; + pub const CHECK_SIGNUP_QUEUE: Endpoint = Endpoint { nsid: "com.atproto.temp.checkSignupQueue", method: "GET" }; + pub const DEREFERENCE_SCOPE: Endpoint = Endpoint { nsid: "com.atproto.temp.dereferenceScope", method: "GET" }; + pub const FETCH_LABELS: Endpoint = Endpoint { nsid: "com.atproto.temp.fetchLabels", method: "GET" }; + pub const REQUEST_PHONE_VERIFICATION: Endpoint = Endpoint { nsid: "com.atproto.temp.requestPhoneVerification", method: "POST" }; + pub const REVOKE_ACCOUNT_CREDENTIALS: Endpoint = Endpoint { nsid: "com.atproto.temp.revokeAccountCredentials", method: "POST" }; +} + diff --git a/src/main.rs b/src/main.rs index a7e965f..2cff85a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ -mod auth; -mod token; -mod post; +mod commands; +mod lexicons; use anyhow::Result; use clap::{Parser, Subcommand}; @@ -76,6 +75,16 @@ enum Commands { #[arg(short, long, default_value = "content")] output: String, }, + + /// Generate lexicon Rust code from ATProto lexicon JSON files + Gen { + /// Input directory containing lexicon JSON files + #[arg(short, long, default_value = "./repos/atproto/lexicons")] + input: String, + /// Output directory for generated Rust code + #[arg(short, long, default_value = "./src/lexicons")] + output: String, + }, } #[tokio::main] @@ -84,22 +93,25 @@ async fn main() -> Result<()> { match cli.command { Commands::Login { handle, password, server } => { - auth::login(&handle, &password, &server).await?; + commands::auth::login(&handle, &password, &server).await?; } Commands::Lexicon { file } => { - post::put_lexicon(&file).await?; + commands::post::put_lexicon(&file).await?; } Commands::Post { file, collection, rkey } => { - post::put_record(&file, &collection, rkey.as_deref()).await?; + commands::post::put_record(&file, &collection, rkey.as_deref()).await?; } Commands::Get { collection, limit } => { - post::get_records(&collection, limit).await?; + commands::post::get_records(&collection, limit).await?; } Commands::Delete { collection, rkey } => { - post::delete_record(&collection, &rkey).await?; + commands::post::delete_record(&collection, &rkey).await?; } Commands::Sync { output } => { - post::sync_to_local(&output).await?; + commands::post::sync_to_local(&output).await?; + } + Commands::Gen { input, output } => { + commands::gen::generate(&input, &output)?; } }