From 72a3d343951b594175badbfb7e0fc2f150954e04 Mon Sep 17 00:00:00 2001 From: syui Date: Tue, 24 Mar 2026 20:39:32 +0900 Subject: [PATCH] add chat post --- src/commands/chat_post.rs | 75 +++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + src/main.rs | 15 ++++++++ 3 files changed, 91 insertions(+) create mode 100644 src/commands/chat_post.rs diff --git a/src/commands/chat_post.rs b/src/commands/chat_post.rs new file mode 100644 index 0000000..bb313cd --- /dev/null +++ b/src/commands/chat_post.rs @@ -0,0 +1,75 @@ +use anyhow::{Context, Result}; +use serde_json::{json, Value}; + +use super::auth; +use super::token; +use crate::xrpc::XrpcClient; + +const CHAT_PROXY_HEADER: &str = "did:web:bsky.syu.is#bsky_chat"; + +/// Load chat proxy DID from config or use default +fn load_chat_proxy() -> Result { + let config = token::load_config()?; + if let Some(network) = config["network"].as_str() { + Ok(format!("did:web:bsky.{}#bsky_chat", network)) + } else { + Ok(CHAT_PROXY_HEADER.to_string()) + } +} + +/// Send a DM via chat.bsky.convo.sendMessage +pub async fn post(text: &str, is_bot: bool, did: &str) -> Result<()> { + let session = if is_bot { + auth::refresh_bot_session().await? + } else { + auth::refresh_session().await? + }; + let pds = session.pds.as_deref().unwrap_or("syu.is"); + let client = if is_bot { + XrpcClient::new_bot(pds) + } else { + XrpcClient::new(pds) + }; + let proxy = load_chat_proxy()?; + + // Get or create convo with the target DID + let convo_resp: Value = client + .query_auth_proxy( + "chat.bsky.convo.getConvoForMembers", + &[("members", did)], + &session.access_jwt, + &proxy, + ) + .await + .context("Failed to get convo for members")?; + + let convo_id = convo_resp["convo"]["id"] + .as_str() + .context("No convo id in response")?; + + // Send message + let send_body = json!({ + "convoId": convo_id, + "message": { + "text": text + } + }); + + let result: Value = client + .call_proxy( + "chat.bsky.convo.sendMessage", + &send_body, + &session.access_jwt, + &proxy, + ) + .await + .context("Failed to send message")?; + + let msg_id = result["id"].as_str().unwrap_or("unknown"); + println!("{}", json!({ + "id": msg_id, + "convoId": convo_id, + })); + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 55b2eda..a8cef8c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -15,3 +15,4 @@ pub mod gpt; pub mod oauth; pub mod setup; pub mod twofa; +pub mod chat_post; diff --git a/src/main.rs b/src/main.rs index db1c34e..705686c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -169,6 +169,18 @@ enum Commands { new: bool, }, + /// Send a DM via chat.bsky.convo + ChatPost { + /// Message text + text: String, + /// Target user DID + #[arg(short, long)] + did: String, + /// Send as bot (uses bot.json) + #[arg(long)] + bot: bool, + }, + /// Run MCP server (for Claude Code integration) #[command(name = "mcp-serve")] McpServe, @@ -388,6 +400,9 @@ async fn main() -> Result<()> { Commands::Chat { message, new } => { lms::chat::run(message.as_deref(), new).await?; } + Commands::ChatPost { text, did, bot } => { + commands::chat_post::post(&text, bot, &did).await?; + } Commands::McpServe => { mcp::serve()?; }