Sebas Tin angelegt und Synology Chat connected
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
pub mod types;
|
||||
pub mod synology;
|
||||
@@ -0,0 +1,203 @@
|
||||
// crates/nazarick/src/chat/synology.rs
|
||||
//
|
||||
// Synology Chat Bot — Webhook Handler.
|
||||
//
|
||||
// Flow:
|
||||
// 1. Synology POST → handle_incoming
|
||||
// 2. Sofort 200 OK zurück (Synology happy)
|
||||
// 3. Async: Auth → Agent → Antwort via Webhook
|
||||
|
||||
use axum::{extract::State, http::StatusCode, Form};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::spawn;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use sebas_tian::Sebas;
|
||||
use crate::chat::types::{AgentChatConfig, AuthResult};
|
||||
|
||||
// ─── Synology Form-Payload ────────────────────────────────────────────────────
|
||||
//
|
||||
// Synology sendet application/x-www-form-urlencoded — kein JSON.
|
||||
// axum::Form<T> deserialisiert das automatisch.
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SynologyIncoming {
|
||||
/// Bot-Token — identifiziert welcher Agent angesprochen wird
|
||||
pub token: String,
|
||||
/// Numerische User-ID in Synology Chat
|
||||
pub user_id: u64,
|
||||
/// Anzeigename des Users
|
||||
pub username: String,
|
||||
/// Die eigentliche Nachricht
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
// ─── Outgoing ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Synology erwartet Form-encoded payload mit JSON drin.
|
||||
// user_ids bestimmt wer die Nachricht bekommt.
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SynologyOutgoing {
|
||||
/// Nachrichtentext
|
||||
text: String,
|
||||
/// Empfänger als Liste von Synology User-IDs
|
||||
user_ids: Vec<u64>,
|
||||
}
|
||||
|
||||
// ─── Shared State ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Wird beim Start einmal gebaut und an alle Handler weitergegeben.
|
||||
// Arc = mehrere Threads teilen sich den State (lesen).
|
||||
// Mutex = exklusiver Zugriff auf Sebas beim Schreiben (chat() braucht &mut self).
|
||||
|
||||
pub struct AppState {
|
||||
/// Alle konfigurierten Bot-Agenten aus config.toml
|
||||
pub agents: Vec<AgentChatConfig>,
|
||||
/// Synology User-ID des Admins — bekommt System-Benachrichtigungen
|
||||
pub admin_user_id: u64,
|
||||
/// Basis Webhook URL für Admin-Nachrichten — ohne user_ids
|
||||
pub admin_webhook_url: String,
|
||||
/// HTTP Client — geteilt für alle ausgehenden Requests
|
||||
pub http: Client,
|
||||
/// Sebas Tian — Mutex weil chat() &mut self braucht
|
||||
pub sebas: Mutex<Sebas>,
|
||||
}
|
||||
|
||||
// ─── Handler ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// POST /chat/synology
|
||||
//
|
||||
// Antwortet sofort 200 OK damit Synology nicht auf Timeout läuft.
|
||||
// Verarbeitung läuft im Hintergrund via tokio::spawn.
|
||||
|
||||
pub async fn handle_incoming(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Form(payload): Form<SynologyIncoming>,
|
||||
) -> StatusCode {
|
||||
info!(
|
||||
user_id = payload.user_id,
|
||||
username = %payload.username,
|
||||
"Nachricht empfangen"
|
||||
);
|
||||
|
||||
// Agent anhand des Bot-Tokens identifizieren
|
||||
let agent = match state.agents.iter().find(|a| a.bot_token == payload.token) {
|
||||
Some(a) => a.clone(),
|
||||
None => {
|
||||
// Unbekannter Token — kein Hinweis nach außen
|
||||
warn!(token = %payload.token, "Unbekannter Token");
|
||||
return StatusCode::OK;
|
||||
}
|
||||
};
|
||||
|
||||
// Async verarbeiten — Caller bekommt sofort 200
|
||||
let state = Arc::clone(&state);
|
||||
spawn(async move {
|
||||
process(state, payload, agent).await;
|
||||
});
|
||||
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
// ─── Verarbeitung ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Läuft im Hintergrund nach dem 200 OK.
|
||||
// Reihenfolge: Auth → Agent aufrufen → Antwort senden.
|
||||
|
||||
async fn process(state: Arc<AppState>, payload: SynologyIncoming, agent: AgentChatConfig) {
|
||||
// 1. Auth prüfen
|
||||
let auth = if agent.allowed_user_ids.contains(&payload.user_id) {
|
||||
AuthResult::Allowed
|
||||
} else {
|
||||
AuthResult::Denied {
|
||||
user_id: payload.user_id,
|
||||
username: payload.username.clone(),
|
||||
}
|
||||
};
|
||||
|
||||
match auth {
|
||||
AuthResult::Denied { user_id, ref username } => {
|
||||
// Unbekannten User informieren
|
||||
send(
|
||||
&state.http,
|
||||
&agent.incoming_webhook_url,
|
||||
user_id,
|
||||
"Zugriff verweigert. Bitte wende dich an den Administrator.",
|
||||
).await;
|
||||
|
||||
// Admin benachrichtigen
|
||||
send(
|
||||
&state.http,
|
||||
&state.admin_webhook_url,
|
||||
state.admin_user_id,
|
||||
&format!(
|
||||
"⚠️ Unbekannter User **{}** (ID: `{}`) hat **{}** kontaktiert.",
|
||||
username, user_id, agent.agent_id
|
||||
),
|
||||
).await;
|
||||
|
||||
warn!(user_id, username = %username, agent = %agent.agent_id, "Zugriff verweigert");
|
||||
return;
|
||||
}
|
||||
AuthResult::Allowed => {
|
||||
info!(user_id = payload.user_id, "Auth OK");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Richtigen Agent aufrufen anhand agent_id
|
||||
let response = match agent.agent_id.as_str() {
|
||||
"sebas_tian" => {
|
||||
// Mutex lock — exklusiver Zugriff auf Sebas
|
||||
let mut sebas = state.sebas.lock().await;
|
||||
match sebas.chat(&payload.text).await {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
error!(error = %e, "Sebas Fehler");
|
||||
"Entschuldigung, es ist ein interner Fehler aufgetreten.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
unknown => {
|
||||
// Später: weitere Agenten hier einhängen
|
||||
warn!(agent_id = %unknown, "Unbekannter Agent");
|
||||
"Dieser Agent ist noch nicht verfügbar.".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Antwort zurückschicken
|
||||
send(&state.http, &agent.incoming_webhook_url, payload.user_id, &response).await;
|
||||
}
|
||||
|
||||
// ─── HTTP Sender ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Sendet eine Nachricht an einen Synology Chat User.
|
||||
// Synology erwartet Form-encoded payload mit JSON — kein reines JSON.
|
||||
// user_id wird dynamisch angehängt — Basis-URL bleibt sauber in config.toml.
|
||||
|
||||
async fn send(client: &Client, base_url: &str, user_id: u64, text: &str) {
|
||||
let body = SynologyOutgoing {
|
||||
text: text.to_string(),
|
||||
user_ids: vec![user_id],
|
||||
};
|
||||
|
||||
// JSON serialisieren und als Form-Parameter verpacken
|
||||
let payload = serde_json::to_string(&body).unwrap_or_default();
|
||||
|
||||
match client
|
||||
.post(base_url)
|
||||
.form(&[("payload", payload.as_str())])
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) if r.status().is_success() => {
|
||||
let response_body = r.text().await.unwrap_or_default();
|
||||
info!("Nachricht gesendet an user_id={} body={}", user_id, response_body);
|
||||
}
|
||||
Ok(r) => error!(status = %r.status(), "Synology hat abgelehnt"),
|
||||
Err(e) => error!(error = %e, "Senden fehlgeschlagen"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// crates/nazarick/src/chat/types.rs
|
||||
//
|
||||
// Gemeinsame Typen für alle Chat-Kanäle.
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
// ─── Auth ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthResult {
|
||||
Allowed,
|
||||
Denied { user_id: u64, username: String },
|
||||
}
|
||||
|
||||
// ─── Agent-Config ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Wird aus config.toml geladen.
|
||||
// Jeder Agent hat seinen eigenen Bot-Token und Webhook-URL.
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct AgentChatConfig {
|
||||
pub agent_id: String,
|
||||
pub bot_token: String,
|
||||
pub incoming_webhook_url: String,
|
||||
pub allowed_user_ids: Vec<u64>,
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// crates/nazarick/src/config.rs
|
||||
|
||||
use serde::Deserialize;
|
||||
use crate::chat::types::AgentChatConfig;
|
||||
|
||||
/// Wurzel der gesamten Nazarick-Konfiguration.
|
||||
/// Entspricht dem obersten Level in config.toml.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NazarickConfig {
|
||||
/// Alles unter [chat] in config.toml
|
||||
pub chat: ChatConfig,
|
||||
}
|
||||
|
||||
/// Konfiguration für den Chat-Connector.
|
||||
/// Entspricht dem [chat]-Block in config.toml.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChatConfig {
|
||||
/// Port auf dem Nazarick auf eingehende Webhooks lauscht
|
||||
pub listen_port: u16,
|
||||
|
||||
/// Synology User-ID des Admins — bekommt System-Benachrichtigungen
|
||||
pub admin_user_id: u64,
|
||||
|
||||
/// Basis Webhook URL für Admin-Benachrichtigungen — ohne user_ids Parameter
|
||||
pub admin_webhook_url: String,
|
||||
|
||||
/// Liste aller konfigurierten Bot-Agenten
|
||||
pub agents: Vec<AgentChatConfig>,
|
||||
}
|
||||
|
||||
/// Lädt die Konfiguration aus config.toml im Arbeitsverzeichnis.
|
||||
/// Gibt einen Fehler zurück wenn die Datei fehlt oder ungültig ist.
|
||||
pub fn load() -> anyhow::Result<NazarickConfig> {
|
||||
let content = std::fs::read_to_string("config.toml")?;
|
||||
let config = toml::from_str(&content)?;
|
||||
Ok(config)
|
||||
}
|
||||
@@ -1,3 +1,70 @@
|
||||
fn main() {
|
||||
println!("Nazarick starting...");
|
||||
// crates/nazarick/src/main.rs
|
||||
//
|
||||
// Nazarick — Einstiegspunkt.
|
||||
// Initialisiert alle Komponenten und startet den HTTP-Server.
|
||||
|
||||
mod chat;
|
||||
mod config;
|
||||
|
||||
use std::sync::Arc;
|
||||
use axum::{routing::post, Router};
|
||||
use reqwest::Client;
|
||||
use tokio::sync::Mutex;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::info;
|
||||
|
||||
use api::llm::lmstudio::LmStudioProvider;
|
||||
use sebas_tian::Sebas;
|
||||
use chat::synology::{handle_incoming, AppState};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Logging initialisieren
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter("nazarick=info,tower_http=debug")
|
||||
.init();
|
||||
|
||||
info!("Nazarick erwacht...");
|
||||
|
||||
// Config laden
|
||||
let cfg = config::load()?;
|
||||
let port = cfg.chat.listen_port;
|
||||
|
||||
// LM Studio Provider initialisieren
|
||||
let llm = Box::new(LmStudioProvider::new(
|
||||
"http://localhost:1234",
|
||||
"dolphin3.0-llama3.1-8b-abliterated",
|
||||
));
|
||||
|
||||
// Sebas Tian initialisieren
|
||||
let sebas = Sebas::new(
|
||||
"crates/sebas-tian/config/soul_core.md",
|
||||
"crates/sebas-tian/config/soul_personality.md",
|
||||
llm,
|
||||
);
|
||||
|
||||
// Shared State aufbauen
|
||||
let state = Arc::new(AppState {
|
||||
agents: cfg.chat.agents,
|
||||
admin_user_id: cfg.chat.admin_user_id,
|
||||
admin_webhook_url: cfg.chat.admin_webhook_url,
|
||||
http: Client::new(),
|
||||
sebas: Mutex::new(sebas),
|
||||
});
|
||||
|
||||
// Routes registrieren
|
||||
let app = Router::new()
|
||||
.route("/chat/synology", post(handle_incoming))
|
||||
.with_state(state)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
// Server starten
|
||||
let addr = format!("0.0.0.0:{}", port);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
|
||||
info!("Lausche auf {}", addr);
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user