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:
253
client/src-ui/src/lib/api.ts
Normal file
253
client/src-ui/src/lib/api.ts
Normal 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)}`);
|
||||
}
|
||||
111
client/src-ui/src/lib/store.ts
Normal file
111
client/src-ui/src/lib/store.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user