diff --git a/.env.example b/.env.example deleted file mode 100644 index 647e7fa..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -# LMS Translation API -TRANSLATE_URL=http://127.0.0.1:1234/v1 -TRANSLATE_MODEL=plamo-2-translate diff --git a/.github/workflows/cf-pages.yml b/.github/workflows/cf-pages.yml deleted file mode 100644 index 80d3d23..0000000 --- a/.github/workflows/cf-pages.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Deploy to Cloudflare Pages - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - permissions: - contents: read - deployments: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - - - name: Install dependencies - run: npm install - - - name: Build content from ATProto - run: npm run build - - - name: Deploy to Cloudflare Pages - uses: cloudflare/pages-action@v1 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} - directory: dist - gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c132f48..5646c75 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy to GitHub Pages on: push: - branches: [main] + branches: [min] workflow_dispatch: permissions: diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index bc37255..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "ailog" -version = "0.0.1" -edition = "2021" -description = "ATProto blog CLI" -authors = ["syui"] -homepage = "https://syui.ai" -repository = "https://git.syui.ai/ai/log" - -[[bin]] -name = "ailog" -path = "src/main.rs" - -[dependencies] -clap = { version = "4.5", features = ["derive"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -anyhow = "1.0" -dirs = "5.0" -chrono = { version = "0.4", features = ["serde"] } -rand = "0.8" -dotenvy = "0.15" diff --git a/index.html b/index.html deleted file mode 100644 index e172358..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - syui.ai - - - -
- - - diff --git a/lexicons/ai.syui.log.post.json b/lexicons/ai.syui.log.post.json deleted file mode 100644 index ad2f85b..0000000 --- a/lexicons/ai.syui.log.post.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "lexicon": 1, - "id": "ai.syui.log.post", - "defs": { - "main": { - "type": "record", - "description": "Record containing a blog post.", - "key": "tid", - "record": { - "type": "object", - "required": ["title", "content", "createdAt"], - "properties": { - "title": { - "type": "string", - "maxLength": 3000, - "maxGraphemes": 300, - "description": "The title of the post." - }, - "content": { - "type": "string", - "maxLength": 1000000, - "maxGraphemes": 100000, - "description": "The content of the post (markdown)." - }, - "createdAt": { - "type": "string", - "format": "datetime", - "description": "Client-declared timestamp when this post was originally created." - }, - "lang": { - "type": "string", - "maxLength": 10, - "description": "Language code of the original content (e.g., 'ja', 'en')." - }, - "translations": { - "type": "ref", - "ref": "#translationMap", - "description": "Translations of the post in other languages." - } - } - } - }, - "translationMap": { - "type": "object", - "description": "Map of language codes to translations.", - "properties": { - "en": { "type": "ref", "ref": "#translation" }, - "ja": { "type": "ref", "ref": "#translation" } - } - }, - "translation": { - "type": "object", - "description": "A translation of a post.", - "properties": { - "title": { - "type": "string", - "maxLength": 3000, - "maxGraphemes": 300 - }, - "content": { - "type": "string", - "maxLength": 1000000, - "maxGraphemes": 100000 - } - } - } - } -} diff --git a/public/config.json b/public/config.json index 4a11798..16dc5cf 100644 --- a/public/config.json +++ b/public/config.json @@ -1,9 +1,9 @@ { - "title": "syui.ai", - "handle": "syui.syui.ai", - "collection": "ai.syui.log.post", - "network": "syu.is", - "color": "#EF454A", - "siteUrl": "https://syui.ai", - "oauth": true + "title": "ailog", + "handle": "syui.ai", + "collection": "ai.syui.log.post", + "network": "bsky.social", + "color": "#EF454A", + "siteUrl": "https://syui.github.io", + "oauth": false } diff --git a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json b/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json deleted file mode 100644 index 3abbea1..0000000 --- a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "cid": "bafyreigwaeqfluw7btvnmxfogd77gtk4efwomvjsvq2yxmmxr2665zwwbi", - "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s", - "value": { - "$type": "ai.syui.log.post", - "content": "## ailogとは\n\natprotoと連携するサイトジェネレータ。\n\n## ailogの使い方\n\n```sh\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cat public/config.json\n{\n \"title\": \"syui.ai\",\n \"handle\": \"syui.syui.ai\",\n \"collection\": \"ai.syui.log.post\",\n \"network\": \"syu.is\",\n \"color\": \"#0066cc\",\n \"siteUrl\": \"https://syui.ai\"\n}\n---\n$ npm run dev\n```\n\n## ailogのコンセプト\n\n1. at-browserを基本にする\n2. atproto oauthでログインする\n3. ログインしたアカウントで記事をポストする", - "createdAt": "2026-01-18T08:31:52.715Z", - "title": "ailogを作り直した", - "translations": { - "en": { - "content": "## About ailog\n\nA site generator that integrates with atproto.\n\n## How to Use ailog\n\n```sh\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cat public/config.json\n{\n \"title\": \"syui.ai\",\n \"handle\": \"syui.syui.ai\",\n \"collection\": \"ai.syui.log.post\",\n \"network\": \"syu.is\",\n \"color\": \"#0066cc\",\n \"siteUrl\": \"https://syui.ai\"\n}\n---\n$ npm run dev\n```\n\n## ailog's Concept\n\n1. Based on at-browser architecture\n2. Uses atproto oAuth for login\n3. Allows posting articles through the logged-in account", - "title": "recreated ailog" - } - }, - "lang": "ja" - } -} diff --git a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/index.json b/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/index.json deleted file mode 100644 index 506d423..0000000 --- a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/index.json +++ /dev/null @@ -1 +0,0 @@ -["3mchqlshygs2s"] diff --git a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self.json b/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self.json deleted file mode 100644 index b62a032..0000000 --- a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "cid": "bafyreihlch2vdee6wpydo2bwap7nyzszjz6focbtxikz7zljcejxz27npy", - "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/app.bsky.actor.profile/self", - "value": { - "$type": "app.bsky.actor.profile", - "avatar": { - "$type": "blob", - "mimeType": "image/jpeg", - "ref": { - "$link": "bafkreigta4pf5h7uvx6jpfcm3d6aeq4g3qpsiqjdoeytnutwp6vwc2yo7u" - }, - "size": 166370 - }, - "createdAt": "2025-09-19T06:17:42Z", - "description": "", - "displayName": "syui" - } -} \ No newline at end of file diff --git a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/describe.json b/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/describe.json deleted file mode 100644 index 70d3c25..0000000 --- a/public/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/describe.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "collections": [ - "ai.syui.log.post", - "app.bsky.actor.profile", - "app.bsky.feed.post", - "app.bsky.feed.repost", - "app.bsky.graph.follow", - "chat.bsky.actor.declaration", - "com.atproto.lexicon.schema" - ], - "did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", - "handle": "syui.syui.ai" -} \ No newline at end of file diff --git a/readme.md b/readme.md index c40f8bc..a169019 100644 --- a/readme.md +++ b/readme.md @@ -11,181 +11,3 @@ $ cat public/config.json $ npm run dev ``` -## oauth - -Use ATProto OAuth to login from the browser and create, edit, or delete posts. - -### Setup - -#### 1. Edit client-metadata.json - -Modify `public/client-metadata.json` with your own domain: - -```json -{ - "client_id": "https://example.com/client-metadata.json", - "client_name": "example.com", - "client_uri": "https://example.com", - "redirect_uris": ["https://example.com/"], - "scope": "atproto transition:generic", - "grant_types": ["authorization_code", "refresh_token"], - "response_types": ["code"], - "application_type": "web", - "token_endpoint_auth_method": "none", - "dpop_bound_access_tokens": true -} -``` - -**Required changes:** - -| Field | Description | -|-------|-------------| -| `client_id` | URL of this file. Must be `https://yourdomain.com/client-metadata.json` | -| `client_name` | App name (shown on auth screen) | -| `client_uri` | Your site URL | -| `redirect_uris` | Redirect URL after OAuth. Use your site's root URL | - -#### 2. Deploy the file - -`client-metadata.json` must be publicly accessible at: - -``` -https://yourdomain.com/client-metadata.json -``` - -The ATProto PDS fetches this file during authentication, so it **must be accessible via public URL**. - -#### 3. Local development - -No configuration needed for local development (localhost/127.0.0.1). The code automatically uses ATProto's loopback client ID: - -``` -http://localhost?redirect_uri=http://127.0.0.1:5173/&scope=atproto%20transition%3Ageneric -``` - -#### 4. Network configuration - -To support multiple PDS servers, define networks in `public/network.json`: - -```json -{ - "bsky.social": { - "bsky": "https://bsky.social", - "plc": "https://plc.directory" - }, - "syu.is": { - "bsky": "https://bsky.syu.is", - "plc": "https://plc.syu.is", - "web": "https://syu.is" - } -} -``` - -The appropriate PDS is automatically selected based on the handle's domain. - -### Troubleshooting - -- **Auth error**: Verify `client_id` matches the actual file URL -- **Redirect error**: Verify `redirect_uris` matches your site URL -- **CORS error**: Verify `client-metadata.json` is served with correct Content-Type - -## cli - -```sh -$ cargo build -$ ./target/debug/ailog -``` - -### login (l) - -login to atproto pds. - -```sh -$ ailog login -p [-s ] -$ ailog l user.bsky.social -p mypassword -$ ailog l user.syu.is -p mypassword -s syu.is -``` - -### post (p) - -post a record to collection. - -```sh -$ ailog post -c [-r ] -$ ailog p ./post.json -c ai.syui.log.post -$ ailog p ./post.json -c ai.syui.log.post -r 3abc123 -``` - -### get (g) - -get records from collection. - -```sh -$ ailog get -c [-l ] -$ ailog g -c ai.syui.log.post -$ ailog g -c ai.syui.log.post -l 20 -``` - -### delete (d) - -delete a record from collection. - -```sh -$ ailog delete -c -r -$ ailog d -c ai.syui.log.post -r 3abc123 -``` - -### sync (s) - -sync pds data to local content directory. - -```sh -$ ailog sync [-o ] -$ ailog s -$ ailog s -o ./public/content -``` - -### lexicon - -update lexicon schema. - -```sh -$ ailog lexicon -$ ailog lexicon ./lexicons/ai.syui.log.post.json -``` - -```sh -$ ailog did syui.ai -did:plc:uqzpqmrjnptsxezjx4xuh2mn -``` - -```txt -_lexicon.log.syui.ai txt "did=did:plc:uqzpqmrjnptsxezjx4xuh2mn" -``` - -### gen - -generate lexicon code from atproto lexicon json files. - -```sh -$ ailog gen [-i ] [-o ] -$ ailog gen -$ ailog gen -i ./repos/atproto/lexicons -o ./src/lexicons -``` - -### lang - -translate content files using lms. - -```sh -$ ailog lang [-f ] [-t ] -$ ailog lang ./post.json -$ ailog lang ./public/content -f ja -t en -``` - -requires `.env`: - -``` -TRANSLATE_URL=http://127.0.0.1:1234/v1 -TRANSLATE_MODEL=plamo-2-translate -``` diff --git a/src/commands/auth.rs b/src/commands/auth.rs deleted file mode 100644 index 6a93c82..0000000 --- a/src/commands/auth.rs +++ /dev/null @@ -1,97 +0,0 @@ -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; - -use super::token::{self, Session}; -use crate::lexicons::{self, com_atproto_server}; - -#[derive(Debug, Serialize)] -struct CreateSessionRequest { - identifier: String, - password: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CreateSessionResponse { - did: String, - handle: String, - access_jwt: String, - refresh_jwt: String, -} - -/// Login to ATProto PDS -pub async fn login(handle: &str, password: &str, pds: &str) -> Result<()> { - let client = reqwest::Client::new(); - let url = lexicons::url(pds, &com_atproto_server::CREATE_SESSION); - - let req = CreateSessionRequest { - identifier: handle.to_string(), - password: password.to_string(), - }; - - println!("Logging in to {} as {}...", pds, handle); - - let res = client - .post(&url) - .json(&req) - .send() - .await - .context("Failed to send login request")?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - anyhow::bail!("Login failed: {} - {}", status, body); - } - - let session_res: CreateSessionResponse = res.json().await?; - - let session = Session { - did: session_res.did, - handle: session_res.handle, - access_jwt: session_res.access_jwt, - refresh_jwt: session_res.refresh_jwt, - pds: Some(pds.to_string()), - }; - - token::save_session(&session)?; - println!("Logged in as {} ({})", session.handle, session.did); - - Ok(()) -} - -/// Refresh access token -pub async fn refresh_session() -> Result { - let session = token::load_session()?; - let pds = session.pds.as_deref().unwrap_or("bsky.social"); - - let client = reqwest::Client::new(); - let url = lexicons::url(pds, &com_atproto_server::REFRESH_SESSION); - - let res = client - .post(&url) - .header("Authorization", format!("Bearer {}", session.refresh_jwt)) - .send() - .await - .context("Failed to refresh session")?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - anyhow::bail!("Refresh failed: {} - {}. Try logging in again.", status, body); - } - - let new_session: CreateSessionResponse = res.json().await?; - - let session = Session { - did: new_session.did, - handle: new_session.handle, - access_jwt: new_session.access_jwt, - refresh_jwt: new_session.refresh_jwt, - pds: Some(pds.to_string()), - }; - - token::save_session(&session)?; - - Ok(session) -} diff --git a/src/commands/did.rs b/src/commands/did.rs deleted file mode 100644 index a7ee961..0000000 --- a/src/commands/did.rs +++ /dev/null @@ -1,34 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Deserialize; - -use crate::lexicons::{self, com_atproto_identity}; - -#[derive(Debug, Deserialize)] -struct ResolveHandleResponse { - did: String, -} - -/// Resolve handle to DID -pub async fn resolve(handle: &str, server: &str) -> Result<()> { - let client = reqwest::Client::new(); - let url = format!( - "{}?handle={}", - lexicons::url(server, &com_atproto_identity::RESOLVE_HANDLE), - handle - ); - - let res = client.get(&url).send().await?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - anyhow::bail!("Failed to resolve handle: {} - {}", status, body); - } - - let result: ResolveHandleResponse = res.json().await - .context("Failed to parse response")?; - - println!("{}", result.did); - - Ok(()) -} diff --git a/src/commands/gen.rs b/src/commands/gen.rs deleted file mode 100644 index 6f59d23..0000000 --- a/src/commands/gen.rs +++ /dev/null @@ -1,265 +0,0 @@ -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 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 rust_code = generate_rust_code(&namespaces); - let rust_output_path = Path::new(output).join("mod.rs"); - fs::create_dir_all(output)?; - fs::write(&rust_output_path, &rust_code)?; - println!("Generated Rust: {}", rust_output_path.display()); - - // Generate TypeScript code - let ts_output = output.replace("src/lexicons", "src/web/lexicons"); - let ts_code = generate_typescript_code(&namespaces); - let ts_output_path = Path::new(&ts_output).join("index.ts"); - fs::create_dir_all(&ts_output)?; - fs::write(&ts_output_path, &ts_code)?; - println!("Generated TypeScript: {}", ts_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"); - code.push_str("#![allow(dead_code)]\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 generate_typescript_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 type - code.push_str("export interface Endpoint {\n"); - code.push_str(" nsid: string\n"); - code.push_str(" method: 'GET' | 'POST'\n"); - code.push_str("}\n\n"); - - // URL helper function - code.push_str("/** Build XRPC URL for an endpoint */\n"); - code.push_str("export function xrpcUrl(pds: string, endpoint: Endpoint): string {\n"); - code.push_str(" return `https://${pds}/xrpc/${endpoint.nsid}`\n"); - code.push_str("}\n\n"); - - // Generate namespaces - for (ns, endpoints) in namespaces { - // Convert namespace to object name: com.atproto.repo -> comAtprotoRepo - let obj_name = to_camel_case(&ns.replace('.', "_")); - - code.push_str(&format!("export const {} = {{\n", obj_name)); - - for endpoint in endpoints { - // Extract the method name from NSID: com.atproto.repo.listRecords -> listRecords - let method_name = endpoint.nsid - .rsplit('.') - .next() - .unwrap_or(&endpoint.nsid); - - code.push_str(&format!( - " {}: {{ nsid: '{}', method: '{}' }} as Endpoint,\n", - method_name, endpoint.nsid, endpoint.method - )); - } - - code.push_str("} as const\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 -} - -fn to_camel_case(s: &str) -> String { - let mut result = String::new(); - let mut capitalize_next = false; - - for (i, c) in s.chars().enumerate() { - if c == '_' { - capitalize_next = true; - } else if capitalize_next { - result.push(c.to_ascii_uppercase()); - capitalize_next = false; - } else if i == 0 { - result.push(c.to_ascii_lowercase()); - } else { - result.push(c); - } - } - - result -} diff --git a/src/commands/lang.rs b/src/commands/lang.rs deleted file mode 100644 index 84d677b..0000000 --- a/src/commands/lang.rs +++ /dev/null @@ -1,10 +0,0 @@ -use anyhow::Result; -use std::path::Path; - -use crate::lms; - -/// Translate content files from one language to another -pub async fn translate(input: &str, from: &str, to: &str) -> Result<()> { - let path = Path::new(input); - lms::translate::run(path, from, to).await -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs deleted file mode 100644 index 4f66577..0000000 --- a/src/commands/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod auth; -pub mod token; -pub mod post; -pub mod gen; -pub mod lang; -pub mod did; diff --git a/src/commands/post.rs b/src/commands/post.rs deleted file mode 100644 index b8a2291..0000000 --- a/src/commands/post.rs +++ /dev/null @@ -1,351 +0,0 @@ -use anyhow::{Context, Result}; -use rand::Rng; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::fs; - -use super::auth; -use crate::lexicons::{self, com_atproto_repo, com_atproto_identity}; - -#[derive(Debug, Serialize)] -struct PutRecordRequest { - repo: String, - collection: String, - rkey: String, - record: Value, -} - -#[derive(Debug, Serialize)] -struct DeleteRecordRequest { - repo: String, - collection: String, - rkey: String, -} - -#[derive(Debug, Deserialize)] -struct PutRecordResponse { - uri: String, - cid: String, -} - -#[derive(Debug, Deserialize)] -struct ListRecordsResponse { - records: Vec, - #[serde(default)] - #[allow(dead_code)] - cursor: Option, -} - -#[derive(Debug, Deserialize)] -struct Record { - uri: String, - cid: String, - value: Value, -} - -/// Generate TID (timestamp-based ID) -fn generate_tid() -> String { - const CHARSET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; - let mut rng = rand::thread_rng(); - (0..13) - .map(|_| { - let idx = rng.gen_range(0..CHARSET.len()); - CHARSET[idx] as char - }) - .collect() -} - -/// Put a record to ATProto -pub async fn put_record(file: &str, collection: &str, rkey: Option<&str>) -> Result<()> { - let session = auth::refresh_session().await?; - let pds = session.pds.as_deref().unwrap_or("bsky.social"); - - let content = fs::read_to_string(file) - .with_context(|| format!("Failed to read file: {}", file))?; - let record: Value = serde_json::from_str(&content)?; - - let rkey = rkey.map(|s| s.to_string()).unwrap_or_else(generate_tid); - - let client = reqwest::Client::new(); - let url = lexicons::url(pds, &com_atproto_repo::PUT_RECORD); - - let req = PutRecordRequest { - repo: session.did.clone(), - collection: collection.to_string(), - rkey: rkey.clone(), - record, - }; - - println!("Posting to {} with rkey: {}", collection, rkey); - println!("{}", serde_json::to_string_pretty(&req)?); - - let res = client - .post(&url) - .header("Authorization", format!("Bearer {}", session.access_jwt)) - .json(&req) - .send() - .await?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - anyhow::bail!("Put record failed: {} - {}", status, body); - } - - let result: PutRecordResponse = res.json().await?; - println!("Success!"); - println!(" URI: {}", result.uri); - println!(" CID: {}", result.cid); - - Ok(()) -} - -/// Put a lexicon schema -pub async fn put_lexicon(file: &str) -> Result<()> { - let session = auth::refresh_session().await?; - let pds = session.pds.as_deref().unwrap_or("bsky.social"); - - let content = fs::read_to_string(file) - .with_context(|| format!("Failed to read file: {}", file))?; - let lexicon: Value = serde_json::from_str(&content)?; - - let lexicon_id = lexicon["id"] - .as_str() - .context("Lexicon file must have 'id' field")? - .to_string(); - - let client = reqwest::Client::new(); - let url = lexicons::url(pds, &com_atproto_repo::PUT_RECORD); - - let req = PutRecordRequest { - repo: session.did.clone(), - collection: "com.atproto.lexicon.schema".to_string(), - rkey: lexicon_id.clone(), - record: lexicon, - }; - - println!("Putting lexicon: {}", lexicon_id); - println!("{}", serde_json::to_string_pretty(&req)?); - - let res = client - .post(&url) - .header("Authorization", format!("Bearer {}", session.access_jwt)) - .json(&req) - .send() - .await?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - anyhow::bail!("Put lexicon failed: {} - {}", status, body); - } - - let result: PutRecordResponse = res.json().await?; - println!("Success!"); - println!(" URI: {}", result.uri); - println!(" CID: {}", result.cid); - - Ok(()) -} - -/// Get records from a collection -pub async fn get_records(collection: &str, limit: u32) -> Result<()> { - let session = auth::refresh_session().await?; - 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!( - "{}?repo={}&collection={}&limit={}", - base_url, session.did, collection, limit - ); - - let res = client - .get(&url) - .header("Authorization", format!("Bearer {}", session.access_jwt)) - .send() - .await?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - anyhow::bail!("Get records failed: {} - {}", status, body); - } - - let result: ListRecordsResponse = res.json().await?; - - println!("Found {} records in {}", result.records.len(), collection); - for record in &result.records { - println!("---"); - println!("URI: {}", record.uri); - println!("CID: {}", record.cid); - println!("{}", serde_json::to_string_pretty(&record.value)?); - } - - Ok(()) -} - -/// Delete a record -pub async fn delete_record(collection: &str, rkey: &str) -> Result<()> { - let session = auth::refresh_session().await?; - let pds = session.pds.as_deref().unwrap_or("bsky.social"); - - let client = reqwest::Client::new(); - let url = lexicons::url(pds, &com_atproto_repo::DELETE_RECORD); - - let req = DeleteRecordRequest { - repo: session.did.clone(), - collection: collection.to_string(), - rkey: rkey.to_string(), - }; - - println!("Deleting {} from {}", rkey, collection); - - let res = client - .post(&url) - .header("Authorization", format!("Bearer {}", session.access_jwt)) - .json(&req) - .send() - .await?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await.unwrap_or_default(); - anyhow::bail!("Delete failed: {} - {}", status, body); - } - - println!("Deleted successfully"); - - Ok(()) -} - -#[derive(Debug, Deserialize)] -struct Config { - handle: String, - #[serde(default)] - collection: Option, -} - -#[derive(Debug, Deserialize)] -struct DescribeRepoResponse { - did: String, - handle: String, - collections: Vec, -} - -/// Sync PDS data to local content directory -pub async fn sync_to_local(output: &str) -> Result<()> { - let config_content = fs::read_to_string("public/config.json") - .context("config.json not found")?; - let config: Config = serde_json::from_str(&config_content)?; - - println!("Syncing data for {}", config.handle); - - let client = reqwest::Client::new(); - - // Resolve handle to DID - let resolve_url = format!( - "{}?handle={}", - lexicons::url("public.api.bsky.app", &com_atproto_identity::RESOLVE_HANDLE), - config.handle - ); - let res = client.get(&resolve_url).send().await?; - let resolve: serde_json::Value = res.json().await?; - let did = resolve["did"].as_str().context("Could not resolve handle")?; - - println!("DID: {}", did); - - // Get PDS from DID document - let plc_url = format!("https://plc.directory/{}", did); - let res = client.get(&plc_url).send().await?; - let did_doc: serde_json::Value = res.json().await?; - let pds = did_doc["service"] - .as_array() - .and_then(|services| { - services.iter().find(|s| s["type"] == "AtprotoPersonalDataServer") - }) - .and_then(|s| s["serviceEndpoint"].as_str()) - .context("Could not find PDS")?; - - 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!( - "{}?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?; - - let describe_path = format!("{}/describe.json", did_dir); - let describe_json = serde_json::to_string_pretty(&serde_json::json!({ - "did": describe.did, - "handle": describe.handle, - "collections": describe.collections, - }))?; - fs::write(&describe_path, &describe_json)?; - println!("Saved: {}", describe_path); - - // 2. Sync profile - let profile_url = format!( - "{}?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() { - let profile: serde_json::Value = res.json().await?; - let profile_dir = format!("{}/app.bsky.actor.profile", did_dir); - fs::create_dir_all(&profile_dir)?; - let profile_path = format!("{}/self.json", profile_dir); - fs::write(&profile_path, serde_json::to_string_pretty(&profile)?)?; - println!("Saved: {}", profile_path); - } - - // 3. Sync collection records - let collection = config.collection.as_deref().unwrap_or("ai.syui.log.post"); - let records_url = format!( - "{}?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() { - let list: ListRecordsResponse = res.json().await?; - let collection_dir = format!("{}/{}", did_dir, collection); - fs::create_dir_all(&collection_dir)?; - - let mut rkeys: Vec = Vec::new(); - for record in &list.records { - let rkey = record.uri.split('/').last().unwrap_or("unknown"); - rkeys.push(rkey.to_string()); - let record_path = format!("{}/{}.json", collection_dir, rkey); - let record_json = serde_json::json!({ - "uri": record.uri, - "cid": record.cid, - "value": record.value, - }); - fs::write(&record_path, serde_json::to_string_pretty(&record_json)?)?; - println!("Saved: {}", record_path); - } - - // Create index.json with list of rkeys - let index_path = format!("{}/index.json", collection_dir); - fs::write(&index_path, serde_json::to_string_pretty(&rkeys)?)?; - println!("Saved: {}", index_path); - - println!("Synced {} records from {}", list.records.len(), collection); - } - - println!("Sync complete!"); - - Ok(()) -} diff --git a/src/commands/token.rs b/src/commands/token.rs deleted file mode 100644 index f37b5a6..0000000 --- a/src/commands/token.rs +++ /dev/null @@ -1,46 +0,0 @@ -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::path::PathBuf; - -/// Bundle ID for the application -pub const BUNDLE_ID: &str = "ai.syui.log"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Session { - pub did: String, - pub handle: String, - pub access_jwt: String, - pub refresh_jwt: String, - #[serde(default)] - pub pds: Option, -} - -/// Get token file path: ~/Library/Application Support/ai.syui.log/token.json -pub fn token_path() -> Result { - let config_dir = dirs::config_dir() - .context("Could not find config directory")? - .join(BUNDLE_ID); - - fs::create_dir_all(&config_dir)?; - Ok(config_dir.join("token.json")) -} - -/// Load session from token file -pub fn load_session() -> Result { - let path = token_path()?; - let content = fs::read_to_string(&path) - .with_context(|| format!("Token file not found: {:?}. Run 'ailog login' first.", path))?; - let session: Session = serde_json::from_str(&content)?; - Ok(session) -} - -/// Save session to token file -pub fn save_session(session: &Session) -> Result<()> { - let path = token_path()?; - let content = serde_json::to_string_pretty(session)?; - fs::write(&path, content)?; - println!("Token saved to {:?}", path); - Ok(()) -} diff --git a/src/lexicons/mod.rs b/src/lexicons/mod.rs deleted file mode 100644 index 838e8cc..0000000 --- a/src/lexicons/mod.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! Auto-generated from ATProto lexicons -//! Run `ailog gen` to regenerate -//! Do not edit manually - -#![allow(dead_code)] - -#[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/lms/mod.rs b/src/lms/mod.rs deleted file mode 100644 index d6edf7c..0000000 --- a/src/lms/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod translate; diff --git a/src/lms/translate.rs b/src/lms/translate.rs deleted file mode 100644 index a255ec7..0000000 --- a/src/lms/translate.rs +++ /dev/null @@ -1,244 +0,0 @@ -use anyhow::{anyhow, Result}; -use serde::{Deserialize, Serialize}; -use std::env; -use std::fs; -use std::path::Path; - -#[derive(Debug, Serialize)] -struct ChatMessage { - role: String, - content: String, -} - -#[derive(Debug, Serialize)] -struct ChatRequest { - model: String, - messages: Vec, -} - -#[derive(Debug, Deserialize)] -struct ChatChoice { - message: ChatMessageResponse, -} - -#[derive(Debug, Deserialize)] -struct ChatMessageResponse { - content: String, -} - -#[derive(Debug, Deserialize)] -struct ChatResponse { - choices: Vec, -} - -/// Translate a file or folder -pub async fn run(input: &Path, from: &str, to: &str) -> Result<()> { - if input.is_dir() { - translate_folder(input, from, to).await - } else { - translate_file(input, from, to).await - } -} - -async fn translate_text( - client: &reqwest::Client, - url: &str, - model: &str, - text: &str, - from: &str, - to: &str, -) -> Result { - let from_lang = lang_name(from); - let to_lang = lang_name(to); - - let system_content = "<|plamo:op|>dataset\ntranslation".to_string(); - let user_content = format!( - "<|plamo:op|>input lang={}\n{}\n<|plamo:op|>output lang={}", - from_lang, text, to_lang - ); - - let req = ChatRequest { - model: model.to_string(), - messages: vec![ - ChatMessage { - role: "system".to_string(), - content: system_content, - }, - ChatMessage { - role: "user".to_string(), - content: user_content, - }, - ], - }; - - let res = client.post(url).json(&req).send().await?; - - if !res.status().is_success() { - let status = res.status(); - let body = res.text().await?; - return Err(anyhow!("Translation failed ({}): {}", status, body)); - } - - let chat_res: ChatResponse = res.json().await?; - chat_res - .choices - .first() - .map(|c| c.message.content.trim().to_string()) - .ok_or_else(|| anyhow!("No translation result")) -} - -async fn translate_file(input: &Path, from: &str, to: &str) -> Result<()> { - let translate_url = - env::var("TRANSLATE_URL").unwrap_or_else(|_| "http://127.0.0.1:1234/v1".to_string()); - let model = - env::var("TRANSLATE_MODEL").unwrap_or_else(|_| "plamo-2-translate".to_string()); - - println!("Translating: {}", input.display()); - - // Read input JSON - let content = fs::read_to_string(input)?; - let mut record: serde_json::Value = serde_json::from_str(&content)?; - - // Handle both direct format and wrapped format (with "value" field) - let value = if record.get("value").is_some() { - record.get_mut("value").unwrap() - } else { - &mut record - }; - - // Check if already translated - if value - .get("translations") - .and_then(|t| t.get(to)) - .is_some() - { - println!(" Skipped (already has {} translation)", to); - return Ok(()); - } - - let client = reqwest::Client::new(); - let url = format!("{}/chat/completions", translate_url); - - // Translate title if exists - let translated_title = if let Some(title) = value.get("title").and_then(|v| v.as_str()) { - if !title.is_empty() { - Some(translate_text(&client, &url, &model, title, from, to).await?) - } else { - None - } - } else { - None - }; - - // Get and translate content - let text = value - .get("content") - .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow!("No 'content' field in JSON"))?; - - let translated_content = translate_text(&client, &url, &model, text, from, to).await?; - - // Add translation to value - let translations = value - .as_object_mut() - .ok_or_else(|| anyhow!("Invalid JSON"))? - .entry("translations") - .or_insert_with(|| serde_json::json!({})); - - let mut translation_entry = serde_json::json!({ - "content": translated_content - }); - - if let Some(title) = translated_title { - translation_entry - .as_object_mut() - .unwrap() - .insert("title".to_string(), serde_json::json!(title)); - } - - translations - .as_object_mut() - .ok_or_else(|| anyhow!("Invalid translations field"))? - .insert(to.to_string(), translation_entry); - - // Write back - let output = serde_json::to_string_pretty(&record)?; - fs::write(input, output)?; - - println!(" OK"); - - Ok(()) -} - -fn collect_json_files(dir: &Path, files: &mut Vec) -> Result<()> { - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - collect_json_files(&path, files)?; - } else if path.extension().map(|e| e == "json").unwrap_or(false) { - // Skip non-post files (describe.json, self.json, index.json) - let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); - if filename != "describe.json" && filename != "self.json" && filename != "index.json" { - files.push(path); - } - } - } - Ok(()) -} - -async fn translate_folder(dir: &Path, from: &str, to: &str) -> Result<()> { - let mut files = Vec::new(); - collect_json_files(dir, &mut files)?; - files.sort(); - - println!("Translating {} files ({} -> {})", files.len(), from, to); - - let mut success = 0; - let mut skipped = 0; - let mut failed = 0; - - for path in &files { - match translate_file(path, from, to).await { - Ok(_) => { - // Check if it was actually translated or skipped - let content = fs::read_to_string(&path)?; - let record: serde_json::Value = serde_json::from_str(&content)?; - let value = record.get("value").unwrap_or(&record); - if value - .get("translations") - .and_then(|t| t.get(to)) - .is_some() - { - success += 1; - } else { - skipped += 1; - } - } - Err(e) => { - eprintln!(" ERROR {}: {}", path.display(), e); - failed += 1; - } - } - } - - println!( - "\nDone: {} translated, {} skipped, {} failed", - success, skipped, failed - ); - - Ok(()) -} - -fn lang_name(code: &str) -> &str { - match code { - "ja" => "Japanese", - "en" => "English", - "zh" => "Chinese", - "ko" => "Korean", - "fr" => "French", - "de" => "German", - "es" => "Spanish", - _ => code, - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 8b6589e..0000000 --- a/src/main.rs +++ /dev/null @@ -1,150 +0,0 @@ -mod commands; -mod lexicons; -mod lms; - -use anyhow::Result; -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(name = "ailog")] -#[command(about = "ATProto blog CLI")] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Login to ATProto PDS - #[command(alias = "l")] - Login { - /// Handle (e.g., user.bsky.social) - handle: String, - /// Password - #[arg(short, long)] - password: String, - /// PDS server - #[arg(short, long, default_value = "bsky.social")] - server: String, - }, - - /// Update lexicon schema - Lexicon { - /// Lexicon JSON file - file: String, - }, - - /// Post a record - #[command(alias = "p")] - Post { - /// Record JSON file - file: String, - /// Collection (e.g., ai.syui.log.post) - #[arg(short, long)] - collection: String, - /// Record key (auto-generated if not provided) - #[arg(short, long)] - rkey: Option, - }, - - /// Get records from collection - #[command(alias = "g")] - Get { - /// Collection (e.g., ai.syui.log.post) - #[arg(short, long)] - collection: String, - /// Limit - #[arg(short, long, default_value = "10")] - limit: u32, - }, - - /// Delete a record - #[command(alias = "d")] - Delete { - /// Collection (e.g., ai.syui.log.post) - #[arg(short, long)] - collection: String, - /// Record key - #[arg(short, long)] - rkey: String, - }, - - /// Sync PDS data to local content directory - #[command(alias = "s")] - Sync { - /// Output directory - #[arg(short, long, default_value = "public/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, - }, - - /// Translate content files - Lang { - /// Input file or directory - input: String, - /// Source language - #[arg(short, long, default_value = "ja")] - from: String, - /// Target language - #[arg(short, long, default_value = "en")] - to: String, - }, - - /// Resolve handle to DID - Did { - /// Handle (e.g., syui.ai) - handle: String, - /// Server - #[arg(short, long, default_value = "bsky.social")] - server: String, - }, -} - -#[tokio::main] -async fn main() -> Result<()> { - // Load .env file if exists - dotenvy::dotenv().ok(); - - let cli = Cli::parse(); - - match cli.command { - Commands::Login { handle, password, server } => { - commands::auth::login(&handle, &password, &server).await?; - } - Commands::Lexicon { file } => { - commands::post::put_lexicon(&file).await?; - } - Commands::Post { file, collection, rkey } => { - commands::post::put_record(&file, &collection, rkey.as_deref()).await?; - } - Commands::Get { collection, limit } => { - commands::post::get_records(&collection, limit).await?; - } - Commands::Delete { collection, rkey } => { - commands::post::delete_record(&collection, &rkey).await?; - } - Commands::Sync { output } => { - commands::post::sync_to_local(&output).await?; - } - Commands::Gen { input, output } => { - commands::gen::generate(&input, &output)?; - } - Commands::Lang { input, from, to } => { - commands::lang::translate(&input, &from, &to).await?; - } - Commands::Did { handle, server } => { - commands::did::resolve(&handle, &server).await?; - } - } - - Ok(()) -} diff --git a/src/web/types.ts b/src/web/types.ts index 8198271..64cf791 100644 --- a/src/web/types.ts +++ b/src/web/types.ts @@ -6,6 +6,7 @@ export interface AppConfig { network: string color: string siteUrl: string + oauth?: boolean } export interface Networks {