Two additions on top of the MVP serve binary:
1. Whitelist by chat_id (KEI_BUDDY_ALLOWED_CHAT_IDS env, CSV).
* BuddyContext gains Arc<Option<Vec<i64>>> allowed_chat_ids
* chat_allowed() check fires before process_text
* Non-whitelisted chats: warn-log + ignore (no response sent)
* None or empty list = accept all (back-compat with prior behaviour)
2. Real LLM wiring (KEI_BUDDY_LLM_PROXY / _LLM_KEY / _LLM_MODEL).
* When extractor-openai feature compiled in AND both proxy+key set,
run_serve instantiates OpenAiExtractor instead of MockExtractor
* Defaults: proxy=https://api.openai.com, key=OPENAI_API_KEY env,
model=gpt-4o-mini
* Fallback: warns + MockExtractor (state machine still walks, but
LLM-extracted fields are empty)
* extractor::OpenAiExtractor gains new_with_model(proxy, key, model);
model is now per-instance instead of compile-time DEFAULT_MODEL
3. start_listener extracted as helper — keeps run_serve readable across
the two feature-gated branches.
Verify-before-commit:
* cargo check -p kei-buddy (default): PASS
* cargo check -p kei-buddy --features extractor-openai: PASS
* cargo test -p kei-buddy --lib: 20/0 unchanged
144 lines
4.2 KiB
Rust
144 lines
4.2 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
//! kei-buddy binary — 4 subcommands: serve / migrate / webhook-set / webhook-delete.
|
|
|
|
use clap::{Parser, Subcommand};
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "kei-buddy",
|
|
about = "KeiBuddy personal-assistant Telegram bot",
|
|
version
|
|
)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
/// Start the Telegram webhook HTTP listener.
|
|
Serve,
|
|
/// Apply the SQLite schema (idempotent). Useful before first run.
|
|
Migrate,
|
|
/// Register a webhook URL with Telegram.
|
|
WebhookSet {
|
|
/// Public HTTPS URL for the /webhook route.
|
|
url: String,
|
|
},
|
|
/// Delete the registered Telegram webhook (revert to polling).
|
|
WebhookDelete,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
let cli = Cli::parse();
|
|
match cli.command {
|
|
Command::Serve => cmd_serve().await,
|
|
Command::Migrate => cmd_migrate(),
|
|
Command::WebhookSet { url } => cmd_webhook_set(url).await,
|
|
Command::WebhookDelete => cmd_webhook_delete().await,
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "serve")]
|
|
async fn cmd_serve() -> anyhow::Result<()> {
|
|
use kei_buddy::serve::{run_serve, ServeConfig};
|
|
let cfg = ServeConfig {
|
|
port: port_from_env(),
|
|
db_path: db_path_from_env(),
|
|
bot_token: require_env("TELEGRAM_BOT_TOKEN")?,
|
|
webhook_secret: require_env("TELEGRAM_WEBHOOK_SECRET")?,
|
|
allowed_chat_ids: allowed_chat_ids_from_env(),
|
|
llm_proxy_url: std::env::var("KEI_BUDDY_LLM_PROXY")
|
|
.ok()
|
|
.or_else(|| Some("https://api.openai.com".to_string())),
|
|
llm_api_key: std::env::var("KEI_BUDDY_LLM_KEY")
|
|
.ok()
|
|
.or_else(|| std::env::var("OPENAI_API_KEY").ok()),
|
|
llm_model: std::env::var("KEI_BUDDY_LLM_MODEL").ok(),
|
|
};
|
|
run_serve(cfg).await
|
|
}
|
|
|
|
/// Parse `KEI_BUDDY_ALLOWED_CHAT_IDS` CSV → Some(Vec<i64>); empty/missing → None.
|
|
fn allowed_chat_ids_from_env() -> Option<Vec<i64>> {
|
|
let raw = std::env::var("KEI_BUDDY_ALLOWED_CHAT_IDS").ok()?;
|
|
let list: Vec<i64> = raw
|
|
.split(',')
|
|
.map(|s| s.trim())
|
|
.filter(|s| !s.is_empty())
|
|
.filter_map(|s| s.parse::<i64>().ok())
|
|
.collect();
|
|
if list.is_empty() {
|
|
None
|
|
} else {
|
|
Some(list)
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "serve"))]
|
|
async fn cmd_serve() -> anyhow::Result<()> {
|
|
anyhow::bail!("kei-buddy was compiled without the `serve` feature. Rebuild with --features serve.");
|
|
}
|
|
|
|
fn cmd_migrate() -> anyhow::Result<()> {
|
|
let path = db_path_from_env();
|
|
let _store = kei_buddy::store::SqliteBuddyStore::from_path(&path)?;
|
|
init_log();
|
|
tracing::info!(path = %path, "schema applied");
|
|
Ok(())
|
|
}
|
|
|
|
fn init_log() {
|
|
#[cfg(feature = "serve")]
|
|
{
|
|
use tracing_subscriber::{fmt, EnvFilter};
|
|
let _ = fmt().with_env_filter(EnvFilter::from_default_env()).try_init();
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "serve")]
|
|
async fn cmd_webhook_set(url: String) -> anyhow::Result<()> {
|
|
use kei_buddy::serve_telegram::set_webhook;
|
|
let token = require_env("TELEGRAM_BOT_TOKEN")?;
|
|
let secret = require_env("TELEGRAM_WEBHOOK_SECRET")?;
|
|
let http = reqwest::Client::new();
|
|
set_webhook(&token, &url, &secret, &http).await?;
|
|
tracing::info!("webhook registered");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(feature = "serve"))]
|
|
async fn cmd_webhook_set(_url: String) -> anyhow::Result<()> {
|
|
anyhow::bail!("compile with --features serve");
|
|
}
|
|
|
|
#[cfg(feature = "serve")]
|
|
async fn cmd_webhook_delete() -> anyhow::Result<()> {
|
|
use kei_buddy::serve_telegram::delete_webhook;
|
|
let token = require_env("TELEGRAM_BOT_TOKEN")?;
|
|
let http = reqwest::Client::new();
|
|
delete_webhook(&token, &http).await?;
|
|
tracing::info!("webhook deleted");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(feature = "serve"))]
|
|
async fn cmd_webhook_delete() -> anyhow::Result<()> {
|
|
anyhow::bail!("compile with --features serve");
|
|
}
|
|
|
|
fn require_env(name: &str) -> anyhow::Result<String> {
|
|
std::env::var(name).map_err(|_| anyhow::anyhow!("env var {name} is required"))
|
|
}
|
|
|
|
fn port_from_env() -> u16 {
|
|
std::env::var("KEI_BUDDY_PORT")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(8080)
|
|
}
|
|
|
|
fn db_path_from_env() -> String {
|
|
std::env::var("KEI_BUDDY_DB_PATH").unwrap_or_else(|_| "./kei-buddy.db".into())
|
|
}
|