Move toju-app into own its folder

This commit is contained in:
2026-03-29 23:30:37 +02:00
parent 0467a7b612
commit 8162e0444a
287 changed files with 42 additions and 34 deletions

View File

@@ -0,0 +1 @@
export const CLIENT_UPDATE_REQUIRED_MESSAGE = 'Update the client in order to connect to other users';

View File

@@ -0,0 +1,133 @@
export type ServerEndpointStatus = 'online' | 'offline' | 'checking' | 'unknown' | 'incompatible';
export interface ServerInfo {
id: string;
name: string;
description?: string;
topic?: string;
hostName: string;
ownerId?: string;
ownerName?: string;
ownerPublicKey?: string;
userCount: number;
maxUsers: number;
hasPassword?: boolean;
isPrivate: boolean;
tags?: string[];
createdAt: number;
sourceId?: string;
sourceName?: string;
sourceUrl?: string;
}
export interface ConfiguredDefaultServerDefinition {
key?: string;
name?: string;
url?: string;
}
export interface DefaultServerDefinition {
key: string;
name: string;
url: string;
}
export interface ServerEndpointVersions {
serverVersion?: string | null;
clientVersion?: string | null;
}
export interface ServerEndpoint {
id: string;
name: string;
url: string;
isActive: boolean;
isDefault: boolean;
defaultKey?: string;
status: ServerEndpointStatus;
latency?: number;
serverVersion?: string;
clientVersion?: string;
}
export type DefaultEndpointTemplate = Omit<ServerEndpoint, 'id' | 'defaultKey'> & {
defaultKey: string;
};
export interface ServerSourceSelector {
sourceId?: string;
sourceUrl?: string;
}
export interface ServerJoinAccessRequest {
roomId: string;
userId: string;
userPublicKey: string;
displayName: string;
password?: string;
inviteId?: string;
}
export interface ServerJoinAccessResponse {
success: boolean;
signalingUrl: string;
joinedBefore: boolean;
via: 'membership' | 'password' | 'invite' | 'public';
server: ServerInfo;
}
export interface CreateServerInviteRequest {
requesterUserId: string;
requesterDisplayName?: string;
requesterRole?: string;
}
export interface ServerInviteInfo {
id: string;
serverId: string;
createdAt: number;
expiresAt: number;
inviteUrl: string;
browserUrl: string;
appUrl: string;
sourceUrl: string;
createdBy?: string;
createdByDisplayName?: string;
isExpired: boolean;
server: ServerInfo;
}
export interface KickServerMemberRequest {
actorUserId: string;
actorRole?: string;
targetUserId: string;
}
export interface BanServerMemberRequest extends KickServerMemberRequest {
banId?: string;
displayName?: string;
reason?: string;
expiresAt?: number;
}
export interface UnbanServerMemberRequest {
actorUserId: string;
actorRole?: string;
banId?: string;
targetUserId?: string;
}
export interface ServerVersionCompatibilityResult {
isCompatible: boolean;
serverVersion: string | null;
}
export interface ServerHealthCheckPayload {
serverVersion?: unknown;
}
export interface ServerEndpointHealthResult {
status: ServerEndpointStatus;
latency?: number;
versions?: ServerEndpointVersions;
}

View File

@@ -0,0 +1,187 @@
import type {
ConfiguredDefaultServerDefinition,
DefaultEndpointTemplate,
DefaultServerDefinition,
ServerEndpoint
} from './server-directory.models';
export function sanitiseServerBaseUrl(rawUrl: string): string {
let cleaned = rawUrl.trim().replace(/\/+$/, '');
if (cleaned.toLowerCase().endsWith('/api')) {
cleaned = cleaned.slice(0, -4);
}
return cleaned;
}
export function normaliseConfiguredServerUrl(
rawUrl: string,
defaultProtocol: 'http' | 'https'
): string {
let cleaned = rawUrl.trim();
if (!cleaned) {
return '';
}
if (cleaned.toLowerCase().startsWith('ws://')) {
cleaned = `http://${cleaned.slice(5)}`;
} else if (cleaned.toLowerCase().startsWith('wss://')) {
cleaned = `https://${cleaned.slice(6)}`;
} else if (cleaned.startsWith('//')) {
cleaned = `${defaultProtocol}:${cleaned}`;
} else if (!/^[a-z][a-z\d+.-]*:\/\//i.test(cleaned)) {
cleaned = `${defaultProtocol}://${cleaned}`;
}
return sanitiseServerBaseUrl(cleaned);
}
export function buildFallbackDefaultServerUrl(
configuredUrl: string | undefined,
defaultProtocol: 'http' | 'https'
): string {
if (configuredUrl?.trim()) {
return normaliseConfiguredServerUrl(configuredUrl, defaultProtocol);
}
return `${defaultProtocol}://localhost:3001`;
}
export function buildDefaultServerDefinitions(
configuredDefaults: ConfiguredDefaultServerDefinition[] | undefined,
configuredUrl: string | undefined,
defaultProtocol: 'http' | 'https'
): DefaultServerDefinition[] {
const seenKeys = new Set<string>();
const seenUrls = new Set<string>();
const definitions = (configuredDefaults ?? [])
.map((server, index) => {
const key = server.key?.trim() || `default-${index + 1}`;
const url = normaliseConfiguredServerUrl(server.url ?? '', defaultProtocol);
if (!key || !url || seenKeys.has(key) || seenUrls.has(url)) {
return null;
}
seenKeys.add(key);
seenUrls.add(url);
return {
key,
name: server.name?.trim() || (index === 0 ? 'Default Server' : `Default Server ${index + 1}`),
url
} satisfies DefaultServerDefinition;
})
.filter((definition): definition is DefaultServerDefinition => definition !== null);
if (definitions.length > 0) {
return definitions;
}
return [
{
key: 'default',
name: 'Default Server',
url: buildFallbackDefaultServerUrl(configuredUrl, defaultProtocol)
}
];
}
export function buildDefaultEndpointTemplates(
definitions: DefaultServerDefinition[]
): DefaultEndpointTemplate[] {
return definitions.map((definition) => ({
name: definition.name,
url: definition.url,
isActive: true,
isDefault: true,
defaultKey: definition.key,
status: 'unknown'
}));
}
export function hasEndpointForDefault(
endpoints: ServerEndpoint[],
defaultEndpoint: DefaultEndpointTemplate
): boolean {
return endpoints.some((endpoint) =>
endpoint.defaultKey === defaultEndpoint.defaultKey
|| sanitiseServerBaseUrl(endpoint.url) === defaultEndpoint.url
);
}
export function matchDefaultEndpointTemplate(
defaultEndpoints: DefaultEndpointTemplate[],
endpoint: ServerEndpoint,
sanitisedUrl: string,
claimedDefaultKeys: Set<string>
): DefaultEndpointTemplate | null {
if (endpoint.defaultKey) {
return defaultEndpoints.find(
(candidate) => candidate.defaultKey === endpoint.defaultKey && !claimedDefaultKeys.has(candidate.defaultKey)
) ?? null;
}
if (!endpoint.isDefault) {
return null;
}
const matchingCurrentDefault = defaultEndpoints.find(
(candidate) => candidate.url === sanitisedUrl && !claimedDefaultKeys.has(candidate.defaultKey)
);
if (matchingCurrentDefault) {
return matchingCurrentDefault;
}
return defaultEndpoints.find(
(candidate) => !claimedDefaultKeys.has(candidate.defaultKey)
) ?? null;
}
export function findDefaultEndpointKeyByUrl(
defaultEndpoints: DefaultEndpointTemplate[],
url: string
): string | null {
const sanitisedUrl = sanitiseServerBaseUrl(url);
return defaultEndpoints.find((endpoint) => endpoint.url === sanitisedUrl)?.defaultKey ?? null;
}
export function ensureAnyActiveEndpoint(endpoints: ServerEndpoint[]): ServerEndpoint[] {
if (endpoints.length === 0 || endpoints.some((endpoint) => endpoint.isActive)) {
return endpoints;
}
const nextEndpoints = [...endpoints];
nextEndpoints[0] = {
...nextEndpoints[0],
isActive: true
};
return nextEndpoints;
}
export function ensureCompatibleActiveEndpoint(endpoints: ServerEndpoint[]): ServerEndpoint[] {
if (endpoints.length === 0 || endpoints.some((endpoint) => endpoint.isActive)) {
return endpoints;
}
const fallbackIndex = endpoints.findIndex((endpoint) => endpoint.status !== 'incompatible');
if (fallbackIndex < 0) {
return endpoints;
}
const nextEndpoints = [...endpoints];
nextEndpoints[fallbackIndex] = {
...nextEndpoints[fallbackIndex],
isActive: true
};
return nextEndpoints;
}