add mcp post bot
This commit is contained in:
@@ -15,7 +15,7 @@ path = "src/main.rs"
|
|||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
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"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "time"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ fn load_user_limit() -> u32 {
|
|||||||
token::load_config()
|
token::load_config()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|c| c["bot"]["limit"].as_u64())
|
.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.
|
/// Check if a user is within daily rate limit. Returns true if allowed.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::fs;
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use crate::commands::token;
|
use crate::commands::token;
|
||||||
|
use crate::commands::oauth;
|
||||||
use crate::tid;
|
use crate::tid;
|
||||||
|
|
||||||
const BUNDLE_ID: &str = "ai.syui.log";
|
const BUNDLE_ID: &str = "ai.syui.log";
|
||||||
@@ -375,6 +376,67 @@ fn handle_get_character() -> Result<String> {
|
|||||||
Ok(result)
|
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> {
|
fn get_tools() -> Vec<Tool> {
|
||||||
vec![
|
vec![
|
||||||
Tool {
|
Tool {
|
||||||
@@ -444,6 +506,20 @@ fn get_tools() -> Vec<Tool> {
|
|||||||
"properties": {}
|
"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 {
|
Tool {
|
||||||
name: "get_character".to_string(),
|
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(),
|
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_list" => handle_chat_list(),
|
||||||
"chat_new" => handle_chat_new(),
|
"chat_new" => handle_chat_new(),
|
||||||
|
"post_create" => {
|
||||||
|
let text = arguments["text"].as_str().unwrap_or("");
|
||||||
|
handle_post_create(text)
|
||||||
|
}
|
||||||
"get_character" => handle_get_character(),
|
"get_character" => handle_get_character(),
|
||||||
_ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)),
|
_ => Err(anyhow::anyhow!("Unknown tool: {}", tool_name)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,3 +27,13 @@ NEVER include the following in your responses:
|
|||||||
- Server configuration details
|
- Server configuration details
|
||||||
|
|
||||||
When unsure if information is sensitive, omit it.
|
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