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

View File

@@ -0,0 +1,253 @@
export interface LoginResult {
success: boolean;
user_id: string;
error: string | null;
token?: string;
}
export interface RoomInfo {
room_id: string;
name: string;
avatar_url: string | null;
is_encrypted: boolean;
member_count: number;
topic: string | null;
}
export interface MessageInfo {
event_id: string;
sender: string;
body: string;
timestamp: number;
reply_to: string | null;
}
export interface RoleInfo {
id: string;
name: string;
color: string;
permissions: string[];
position: number;
}
export interface PermissionsInfo {
can_send_messages: boolean;
can_delete_messages: boolean;
can_manage_channels: boolean;
can_manage_roles: boolean;
can_kick: boolean;
can_ban: boolean;
can_manage_emoji: boolean;
can_manage_threads: boolean;
can_voice_connect: boolean;
can_voice_stream: boolean;
}
export interface VoiceStateInfo {
room_id: string;
muted: boolean;
deafened: boolean;
streaming: boolean;
}
export interface PresenceInfo {
user_id: string;
status: string;
status_msg: string | null;
last_active: number | null;
}
function isTauri(): boolean {
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
}
function getApiBase(): string {
return window.location.origin;
}
function getToken(): string | null {
return localStorage.getItem('eifeldc_token');
}
function setToken(token: string) {
localStorage.setItem('eifeldc_token', token);
}
function clearToken() {
localStorage.removeItem('eifeldc_token');
}
async function tauriInvoke(cmd: string, args: Record<string, unknown>): Promise<any> {
const { invoke } = await import('@tauri-apps/api/tauri');
return invoke(cmd, args);
}
async function httpGet(path: string): Promise<any> {
const res = await fetch(`${getApiBase()}${path}`, {
headers: { 'Authorization': `Bearer ${getToken()}` },
});
if (res.status === 401) { clearToken(); throw new Error('Unauthorized'); }
return res.json();
}
async function httpPost(path: string, body?: unknown): Promise<any> {
const res = await fetch(`${getApiBase()}${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${getToken()}`,
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 401) { clearToken(); throw new Error('Unauthorized'); }
return res.json();
}
export async function login(homeserver: string, username: string, password: string): Promise<LoginResult> {
if (isTauri()) {
return tauriInvoke('login', { homeserver, username, password });
}
const result = await httpPost('/api/login', { homeserver, username, password });
if (result.token) { setToken(result.token); }
return result;
}
export async function logout(): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('logout', {});
}
clearToken();
return httpPost('/api/logout');
}
export async function register(homeserver: string, username: string, password: string): Promise<LoginResult> {
if (isTauri()) {
return tauriInvoke('register', { homeserver, username, password });
}
const result = await httpPost('/api/register', { homeserver, username, password });
if (result.token) { setToken(result.token); }
return result;
}
export async function getJoinedRooms(): Promise<RoomInfo[]> {
if (isTauri()) {
return tauriInvoke('get_joined_rooms', {});
}
return httpGet('/api/rooms');
}
export async function getRoomMessages(roomId: string, limit: number, from?: string): Promise<MessageInfo[]> {
if (isTauri()) {
return tauriInvoke('get_room_messages', { roomId, limit, from });
}
let url = `/api/rooms/${encodeURIComponent(roomId)}/messages?limit=${limit}`;
if (from) url += `&from=${encodeURIComponent(from)}`;
return httpGet(url);
}
export async function sendMessage(roomId: string, message: string): Promise<string> {
if (isTauri()) {
return tauriInvoke('send_message', { roomId, message });
}
return httpPost(`/api/rooms/${encodeURIComponent(roomId)}/send`, { message });
}
export async function createRoom(name: string, topic?: string, visibility?: string): Promise<string> {
if (isTauri()) {
return tauriInvoke('create_room', { name, topic, visibility: visibility || 'private' });
}
const result = await httpPost('/api/rooms/create', { name, topic, visibility: visibility || 'private' });
return result;
}
export async function joinRoom(roomIdOrAlias: string): Promise<string> {
if (isTauri()) {
return tauriInvoke('join_room', { roomIdOrAlias });
}
const result = await httpPost('/api/rooms/join', { room_id_or_alias: roomIdOrAlias });
return result;
}
export async function leaveRoom(roomId: string): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('leave_room', { roomId });
}
return httpPost(`/api/rooms/${encodeURIComponent(roomId)}/leave`);
}
export async function getRoomMembers(roomId: string): Promise<string[]> {
if (isTauri()) {
return tauriInvoke('get_room_members', { roomId });
}
return httpGet(`/api/rooms/${encodeURIComponent(roomId)}/members`);
}
export async function setPresence(status: string, statusMsg?: string): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('set_presence', { status, statusMsg });
}
return httpPost('/api/presence/set', { status, status_msg: statusMsg });
}
export async function getPresence(userId: string): Promise<PresenceInfo> {
if (isTauri()) {
return tauriInvoke('get_presence', { userId });
}
return httpGet(`/api/presence/${encodeURIComponent(userId)}`);
}
export async function joinVoiceChannel(roomId: string): Promise<VoiceStateInfo> {
if (isTauri()) {
return tauriInvoke('join_voice_channel', { roomId });
}
return httpPost('/api/voice/join', { room_id: roomId });
}
export async function leaveVoiceChannel(roomId: string): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('leave_voice_channel', { roomId });
}
return httpPost('/api/voice/leave', { room_id: roomId });
}
export async function toggleMute(roomId: string): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('toggle_mute', { roomId });
}
return httpPost('/api/voice/toggle-mute', { room_id: roomId });
}
export async function toggleDeafen(roomId: string): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('toggle_deafen', { roomId });
}
return httpPost('/api/voice/toggle-deafen', { room_id: roomId });
}
export async function getRoles(roomId: string): Promise<RoleInfo[]> {
if (isTauri()) {
return tauriInvoke('get_roles', { roomId });
}
return httpGet(`/api/rooms/${encodeURIComponent(roomId)}/roles`);
}
export async function assignRole(roomId: string, userId: string, roleId: string): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('assign_role', { roomId, userId, roleId });
}
return httpPost(`/api/rooms/${encodeURIComponent(roomId)}/roles/assign`, { user_id: userId, role_id: roleId });
}
export async function removeRole(roomId: string, userId: string, roleId: string): Promise<boolean> {
if (isTauri()) {
return tauriInvoke('remove_role', { roomId, userId, roleId });
}
return httpPost(`/api/rooms/${encodeURIComponent(roomId)}/roles/remove`, { user_id: userId, role_id: roleId });
}
export async function getPermissions(roomId: string, userId: string): Promise<PermissionsInfo> {
if (isTauri()) {
return tauriInvoke('get_permissions', { roomId, userId });
}
return httpGet(`/api/rooms/${encodeURIComponent(roomId)}/permissions/${encodeURIComponent(userId)}`);
}

View File

@@ -0,0 +1,111 @@
import { writable, derived } from 'svelte/store';
import { invoke } from '@tauri-apps/api/tauri';
interface User {
id: string;
username: string;
avatarUrl: string | null;
status: 'online' | 'idle' | 'dnd' | 'offline';
}
interface Room {
id: string;
name: string;
avatarUrl: string | null;
unreadCount: number;
isVoice: boolean;
}
interface Channel {
id: string;
name: string;
type: 'text' | 'voice' | 'announcement';
topic: string | null;
parentId: string | null;
}
interface Message {
id: string;
sender: string;
content: string;
timestamp: number;
replyTo: string | null;
isBot: boolean;
reactions: Record<string, string[]>;
}
interface Server {
id: string;
name: string;
iconUrl: string | null;
roles: Role[];
}
interface Role {
id: string;
name: string;
color: string;
permissions: string[];
position: number;
}
interface Member {
id: string;
username: string;
avatarUrl: string | null;
status: 'online' | 'idle' | 'dnd' | 'offline';
roles: string[];
}
export const currentUser = writable<User | null>(null);
export const currentServer = writable<Server | null>(null);
export const currentChannel = writable<Channel | null>(null);
export const servers = writable<Server[]>([]);
export const channels = writable<Channel[]>([]);
export const messages = writable<Message[]>([]);
export const members = writable<Member[]>([]);
export const voiceState = writable<{
channelId: string | null;
muted: boolean;
deafened: boolean;
streaming: boolean;
}>({
channelId: null,
muted: false,
deafened: false,
streaming: false,
});
export const sortedMembers = derived(members, ($members) => {
return [...$members].sort((a, b) => {
const statusOrder = { online: 0, idle: 1, dnd: 2, offline: 3 };
return statusOrder[a.status] - statusOrder[b.status];
});
});
export const textChannels = derived(channels, ($channels) =>
$channels.filter((c) => c.type === 'text')
);
export const voiceChannels = derived(channels, ($channels) =>
$channels.filter((c) => c.type === 'voice')
);
export async function getJoinedRooms(): Promise<import('./api').RoomInfo[]> {
return invoke('get_joined_rooms');
}
export async function refreshChannels() {
try {
const rooms = await getJoinedRooms();
channels.set(rooms.map(r => ({
id: r.room_id,
name: r.name || r.room_id,
type: 'text' as const,
topic: null,
parentId: null,
})));
} catch (e) {
console.error('Failed to refresh channels', e);
}
}