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

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:
root
2026-04-28 08:23:23 +02:00
commit 0978d0c2e9
82 changed files with 12417 additions and 0 deletions

24
bot-sdk/src/auth.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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()
}
}