Initial commit: EifelDC - Discord-like Matrix chat platform
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Rust Tests (push) Has been cancelled
CI / Frontend Check (push) Has been cancelled
CI / Build Tauri (macOS) (push) Has been cancelled
CI / Build Tauri (macOS Intel) (push) Has been cancelled
CI / Build Tauri (Linux) (push) Has been cancelled
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Rust Tests (push) Has been cancelled
CI / Frontend Check (push) Has been cancelled
CI / Build Tauri (macOS) (push) Has been cancelled
CI / Build Tauri (macOS Intel) (push) Has been cancelled
CI / Build Tauri (Linux) (push) Has been cancelled
Includes server (Rust/Axum API proxy with voice management), Tauri desktop client with Svelte UI, bot-sdk, Docker infra (Synapse, PostgreSQL, Coturn, Nginx), and CI/CD pipeline.
This commit is contained in:
22
bot-sdk/Cargo.toml
Normal file
22
bot-sdk/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "eifeldc-bot-sdk"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
matrix-sdk = { workspace = true }
|
||||
matrix-sdk-base = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
url = { workspace = true }
|
||||
async-trait = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "1"
|
||||
24
bot-sdk/src/auth.rs
Normal file
24
bot-sdk/src/auth.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
pub struct BotAuth {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl BotAuth {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_credentials(username: &str, password: &str) -> Self {
|
||||
Self {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_configured(&self) -> bool {
|
||||
!self.username.is_empty() && !self.password.is_empty()
|
||||
}
|
||||
}
|
||||
132
bot-sdk/src/client.rs
Normal file
132
bot-sdk/src/client.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use matrix_sdk::Client;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::auth::BotAuth;
|
||||
use crate::commands::CommandRegistry;
|
||||
use crate::event::EventHandler;
|
||||
use crate::room::RoomManager;
|
||||
|
||||
pub struct BotClient {
|
||||
client: Arc<RwLock<Option<Client>>>,
|
||||
auth: Arc<RwLock<BotAuth>>,
|
||||
commands: Arc<RwLock<CommandRegistry>>,
|
||||
event_handler: Arc<RwLock<EventHandler>>,
|
||||
room_manager: Arc<RwLock<RoomManager>>,
|
||||
homeserver: String,
|
||||
}
|
||||
|
||||
impl BotClient {
|
||||
pub fn new(homeserver: &str) -> Self {
|
||||
Self {
|
||||
client: Arc::new(RwLock::new(None)),
|
||||
auth: Arc::new(RwLock::new(BotAuth::new())),
|
||||
commands: Arc::new(RwLock::new(CommandRegistry::new())),
|
||||
event_handler: Arc::new(RwLock::new(EventHandler::new())),
|
||||
room_manager: Arc::new(RwLock::new(RoomManager::new())),
|
||||
homeserver: homeserver.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_auth(self, username: &str, password: &str) -> Self {
|
||||
let auth = BotAuth::with_credentials(username, password);
|
||||
self.auth = Arc::new(RwLock::new(auth));
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> anyhow::Result<()> {
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&self.homeserver)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
{
|
||||
let mut guard = self.client.write().await;
|
||||
*guard = Some(client.clone());
|
||||
}
|
||||
|
||||
let auth = self.auth.read().await;
|
||||
client
|
||||
.matrix_auth()
|
||||
.login_username(&auth.username, &auth.password)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
tracing::info!("Bot logged in as {}", auth.username);
|
||||
drop(auth);
|
||||
|
||||
let mut sync_token: Option<String> = None;
|
||||
loop {
|
||||
let mut settings = matrix_sdk::config::SyncSettings::new();
|
||||
if let Some(token) = sync_token.as_ref() {
|
||||
settings = settings.token(token.clone());
|
||||
}
|
||||
match client.sync_once(settings).await {
|
||||
Ok(response) => {
|
||||
sync_token = Some(response.next_batch);
|
||||
|
||||
let handler = self.event_handler.read().await;
|
||||
handler.dispatch("sync");
|
||||
drop(handler);
|
||||
|
||||
let rooms = client.joined_rooms();
|
||||
let mut room_mgr = self.room_manager.write().await;
|
||||
for room in rooms {
|
||||
let name = room.display_name().await.map(|n| n.to_string()).unwrap_or_default();
|
||||
room_mgr.add_room(crate::room::RoomInfo {
|
||||
room_id: room.room_id().to_string(),
|
||||
name,
|
||||
is_encrypted: room.is_encrypted().await.unwrap_or(false),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Sync error: {}", e);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(&self) -> anyhow::Result<()> {
|
||||
let mut guard = self.client.write().await;
|
||||
*guard = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_message(&self, room_id: &str, message: &str) -> anyhow::Result<()> {
|
||||
let guard = self.client.read().await;
|
||||
let client = guard.as_ref().ok_or(anyhow::anyhow!("Not connected"))?;
|
||||
|
||||
let rid = matrix_sdk::ruma::room_id!(room_id)
|
||||
.map_err(|_| anyhow::anyhow!("Invalid room ID"))?;
|
||||
let room = client.get_room(&rid)
|
||||
.ok_or(anyhow::anyhow!("Room not found"))?;
|
||||
|
||||
let content = matrix_sdk::ruma::events::room::message::RoomMessageEventContent::text_plain(message);
|
||||
let txn_id = matrix_sdk::ruma::TransactionId::new();
|
||||
room.send(content, Some(&txn_id)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn on_command(&self, name: &str, handler: Box<dyn Fn(&str, &str) + Send + Sync>) {
|
||||
let mut commands = self.commands.write().await;
|
||||
commands.register(name, handler);
|
||||
}
|
||||
|
||||
pub async fn on_event(&self, handler: Box<dyn Fn(&str) + Send + Sync>) {
|
||||
let mut event_handler = self.event_handler.write().await;
|
||||
event_handler.add_handler(handler);
|
||||
}
|
||||
|
||||
pub async fn get_rooms(&self) -> Vec<crate::room::RoomInfo> {
|
||||
let room_mgr = self.room_manager.read().await;
|
||||
room_mgr.list_rooms().into_iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub async fn handle_message(&self, room_id: &str, sender: &str, body: &str) {
|
||||
let commands = self.commands.read().await;
|
||||
commands.parse_and_execute(body, sender);
|
||||
}
|
||||
}
|
||||
57
bot-sdk/src/commands.rs
Normal file
57
bot-sdk/src/commands.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
type CommandHandler = Box<dyn Fn(&str, &str) + Send + Sync>;
|
||||
|
||||
pub struct CommandRegistry {
|
||||
commands: HashMap<String, CommandHandler>,
|
||||
prefix: String,
|
||||
}
|
||||
|
||||
impl CommandRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
commands: HashMap::new(),
|
||||
prefix: "!".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_prefix(mut self, prefix: &str) -> Self {
|
||||
self.prefix = prefix.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_prefix(&mut self, prefix: &str) {
|
||||
self.prefix = prefix.to_string();
|
||||
}
|
||||
|
||||
pub fn register(&mut self, name: &str, handler: CommandHandler) {
|
||||
self.commands.insert(name.to_string(), handler);
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, name: &str) {
|
||||
self.commands.remove(name);
|
||||
}
|
||||
|
||||
pub fn parse_and_execute(&self, message: &str, sender: &str) {
|
||||
if !message.starts_with(&self.prefix) {
|
||||
return;
|
||||
}
|
||||
|
||||
let content = &message[self.prefix.len()..];
|
||||
let parts: Vec<&str> = content.splitn(2, ' ').collect();
|
||||
let command = parts[0];
|
||||
let args = parts.get(1).unwrap_or(&"");
|
||||
|
||||
if let Some(handler) = self.commands.get(command) {
|
||||
handler(args, sender);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_commands(&self) -> Vec<String> {
|
||||
self.commands.keys().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn has_command(&self, name: &str) -> bool {
|
||||
self.commands.contains_key(name)
|
||||
}
|
||||
}
|
||||
25
bot-sdk/src/event.rs
Normal file
25
bot-sdk/src/event.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
type EventCallback = Box<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
pub struct EventHandler {
|
||||
handlers: Vec<EventCallback>,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub fn new() -> Self {
|
||||
Self { handlers: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add_handler(&mut self, handler: EventCallback) {
|
||||
self.handlers.push(handler);
|
||||
}
|
||||
|
||||
pub fn dispatch(&self, event: &str) {
|
||||
for handler in &self.handlers {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handler_count(&self) -> usize {
|
||||
self.handlers.len()
|
||||
}
|
||||
}
|
||||
8
bot-sdk/src/lib.rs
Normal file
8
bot-sdk/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod event;
|
||||
pub mod room;
|
||||
pub mod auth;
|
||||
|
||||
pub use client::BotClient;
|
||||
pub use auth::BotAuth;
|
||||
38
bot-sdk/src/room.rs
Normal file
38
bot-sdk/src/room.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RoomInfo {
|
||||
pub room_id: String,
|
||||
pub name: String,
|
||||
pub is_encrypted: bool,
|
||||
}
|
||||
|
||||
pub struct RoomManager {
|
||||
rooms: HashMap<String, RoomInfo>,
|
||||
}
|
||||
|
||||
impl RoomManager {
|
||||
pub fn new() -> Self {
|
||||
Self { rooms: HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn add_room(&mut self, room: RoomInfo) {
|
||||
self.rooms.insert(room.room_id.clone(), room);
|
||||
}
|
||||
|
||||
pub fn remove_room(&mut self, room_id: &str) {
|
||||
self.rooms.remove(room_id);
|
||||
}
|
||||
|
||||
pub fn get_room(&self, room_id: &str) -> Option<&RoomInfo> {
|
||||
self.rooms.get(room_id)
|
||||
}
|
||||
|
||||
pub fn list_rooms(&self) -> Vec<&RoomInfo> {
|
||||
self.rooms.values().collect()
|
||||
}
|
||||
|
||||
pub fn room_count(&self) -> usize {
|
||||
self.rooms.len()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user