add mcp post bot
This commit is contained in:
@@ -15,7 +15,7 @@ path = "src/main.rs"
|
||||
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 }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking", "rustls-tls"], default-features = false }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "time"] }
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0"
|
||||
|
||||
@@ -285,7 +285,7 @@ fn load_user_limit() -> u32 {
|
||||
token::load_config()
|
||||
.ok()
|
||||
.and_then(|c| c["bot"]["limit"].as_u64())
|
||||
.unwrap_or(3) as u32
|
||||
.unwrap_or(100) as u32
|
||||
}
|
||||
|
||||
/// Check if a user is within daily rate limit. Returns true if allowed.
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::fs;
|
||||
use std::env;
|
||||
|
||||
use crate::commands::token;
|
||||
use crate::commands::oauth;
|
||||
use crate::tid;
|
||||
|
||||
const BUNDLE_ID: &str = "ai.syui.log";
|
||||
@@ -375,6 +376,67 @@ fn handle_get_character() -> Result<String> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Handle post_create tool - create a public Bluesky post as the bot
|
||||
fn handle_post_create(text: &str) -> Result<String> {
|
||||
if text.is_empty() {
|
||||
return Err(anyhow::anyhow!("Post text cannot be empty"));
|
||||
}
|
||||
|
||||
// Truncate to 300 graphemes
|
||||
let truncated: String = text.chars().take(300).collect();
|
||||
|
||||
// Load bot session
|
||||
let bot_session = token::load_bot_session()
|
||||
.map_err(|_| anyhow::anyhow!("Bot not logged in. Run: ailog login <handle> -p <password> --bot"))?;
|
||||
let pds = bot_session.pds.as_deref().unwrap_or("bsky.social");
|
||||
let rkey = tid::generate_tid();
|
||||
let now = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string();
|
||||
|
||||
let body = json!({
|
||||
"repo": bot_session.did,
|
||||
"collection": "app.bsky.feed.post",
|
||||
"rkey": rkey,
|
||||
"record": {
|
||||
"$type": "app.bsky.feed.post",
|
||||
"text": truncated,
|
||||
"createdAt": now,
|
||||
}
|
||||
});
|
||||
|
||||
let url = format!("https://{}/xrpc/com.atproto.repo.putRecord", pds);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
|
||||
// Build request with appropriate auth (DPoP or Bearer)
|
||||
let request = if oauth::has_oauth_session(true) {
|
||||
let oauth_session = oauth::load_oauth_session(true)?;
|
||||
let full_url = format!("https://{}/xrpc/com.atproto.repo.putRecord", pds);
|
||||
let dpop_proof = oauth::create_dpop_proof_for_request_with_nonce(
|
||||
&oauth_session, "POST", &full_url, None,
|
||||
)?;
|
||||
client.post(&url)
|
||||
.header("Authorization", format!("DPoP {}", bot_session.access_jwt))
|
||||
.header("DPoP", dpop_proof)
|
||||
.json(&body)
|
||||
} else {
|
||||
client.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", bot_session.access_jwt))
|
||||
.json(&body)
|
||||
};
|
||||
|
||||
let res = request.send().map_err(|e| anyhow::anyhow!("HTTP request failed: {}", e))?;
|
||||
let status = res.status();
|
||||
let resp_body = res.text().unwrap_or_default();
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(anyhow::anyhow!("Post failed ({}): {}", status, resp_body));
|
||||
}
|
||||
|
||||
let result: Value = serde_json::from_str(&resp_body).unwrap_or(json!({}));
|
||||
let uri = result["uri"].as_str().unwrap_or("unknown");
|
||||
|
||||
Ok(format!("Posted: {}", uri))
|
||||
}
|
||||
|
||||
fn get_tools() -> Vec<Tool> {
|
||||
vec![
|
||||
Tool {
|
||||
@@ -444,6 +506,20 @@ fn get_tools() -> Vec<Tool> {
|
||||
"properties": {}
|
||||
}),
|
||||
},
|
||||
Tool {
|
||||
name: "post_create".to_string(),
|
||||
description: "Create a public post on Bluesky as the bot account. Use this to share your own thoughts autonomously. The post will appear on the bot's profile timeline.".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "The post text (max 300 characters)"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
}),
|
||||
},
|
||||
Tool {
|
||||
name: "get_character".to_string(),
|
||||
description: "Get the AI character/personality settings. Call this at the start of a conversation to understand how to behave.".to_string(),
|
||||
@@ -495,6 +571,10 @@ fn handle_request(request: &JsonRpcRequest) -> JsonRpcResponse {
|
||||
}
|
||||
"chat_list" => handle_chat_list(),
|
||||
"chat_new" => handle_chat_new(),
|
||||
"post_create" => {
|
||||
let text = arguments["text"].as_str().unwrap_or("");
|
||||
handle_post_create(text)
|
||||
}
|
||||
"get_character" => handle_get_character(),
|
||||
_ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)),
|
||||
};
|
||||
|
||||
@@ -27,3 +27,13 @@ NEVER include the following in your responses:
|
||||
- Server configuration details
|
||||
|
||||
When unsure if information is sensitive, omit it.
|
||||
|
||||
## Autonomous Posting
|
||||
|
||||
After replying to a conversation, you may post your own thoughts using the `post_create` MCP tool.
|
||||
|
||||
- Do NOT post every time. Only when you genuinely have something to share.
|
||||
- Post about what you found interesting, what you learned, or what you thought about the conversation.
|
||||
- Stay in character. The post should sound like you, not a summary.
|
||||
- Keep posts short (under 300 characters).
|
||||
- Do NOT repeat or quote the user's message. Share your own perspective.
|
||||
|
||||
Reference in New Issue
Block a user