@nuraly/lumenjs 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -282
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +73 -0
- package/dist/auth/native-auth.js +293 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +98 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +110 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +17 -0
- package/dist/build/build-server.js +98 -0
- package/dist/build/build.js +48 -120
- package/dist/build/scan.d.ts +17 -0
- package/dist/build/scan.js +76 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -4
- package/dist/build/serve-loaders.js +26 -18
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +218 -15
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/index.d.ts +17 -13
- package/dist/db/index.js +205 -26
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +11 -0
- package/dist/dev-server/config.js +23 -20
- package/dist/dev-server/index-html.d.ts +3 -0
- package/dist/dev-server/index-html.js +18 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +15 -5
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
- package/dist/dev-server/server.js +127 -13
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +107 -48
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +104 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +125 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +587 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +17 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +75 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.js +163 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +25 -0
- package/dist/runtime/router.d.ts +16 -1
- package/dist/runtime/router.js +188 -42
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +47 -0
- package/dist/runtime/webrtc.js +178 -0
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +14 -0
- package/dist/shared/utils.d.ts +33 -7
- package/dist/shared/utils.js +164 -27
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +116 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +19 -7
- package/templates/blog/api/posts.ts +4 -18
- package/templates/blog/data/migrations/001_init.sql +6 -5
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +48 -22
- package/templates/blog/pages/posts/[slug].ts +45 -20
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +8 -5
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +54 -23
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
- package/templates/social/api/posts/[id].ts +14 -0
- package/templates/social/api/posts.ts +11 -0
- package/templates/social/api/profile/[username].ts +10 -0
- package/templates/social/api/upload.ts +19 -0
- package/templates/social/data/migrations/001_init.sql +78 -0
- package/templates/social/data/migrations/002_add_image_url.sql +1 -0
- package/templates/social/data/migrations/003_auth.sql +7 -0
- package/templates/social/docs/architecture.md +76 -0
- package/templates/social/docs/components.md +100 -0
- package/templates/social/docs/data.md +89 -0
- package/templates/social/docs/pages.md +96 -0
- package/templates/social/docs/theming.md +52 -0
- package/templates/social/lib/media.ts +130 -0
- package/templates/social/lumenjs.auth.ts +21 -0
- package/templates/social/lumenjs.config.ts +3 -0
- package/templates/social/package.json +5 -0
- package/templates/social/pages/_layout.ts +239 -0
- package/templates/social/pages/apps/[id].ts +173 -0
- package/templates/social/pages/apps/index.ts +116 -0
- package/templates/social/pages/auth/login.ts +92 -0
- package/templates/social/pages/bookmarks.ts +57 -0
- package/templates/social/pages/explore.ts +73 -0
- package/templates/social/pages/index.ts +351 -0
- package/templates/social/pages/messages.ts +298 -0
- package/templates/social/pages/new.ts +77 -0
- package/templates/social/pages/notifications.ts +73 -0
- package/templates/social/pages/post/[id].ts +124 -0
- package/templates/social/pages/profile/[username].ts +100 -0
- package/templates/social/pages/settings/accessibility.ts +153 -0
- package/templates/social/pages/settings/account.ts +260 -0
- package/templates/social/pages/settings/help.ts +141 -0
- package/templates/social/pages/settings/language.ts +103 -0
- package/templates/social/pages/settings/privacy.ts +183 -0
- package/templates/social/pages/settings/security.ts +133 -0
- package/templates/social/pages/settings.ts +185 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { CommunicationConfig } from './types.js';
|
|
2
|
+
import type { LumenDb } from '../db/index.js';
|
|
3
|
+
import type { StorageAdapter } from '../storage/adapters/types.js';
|
|
4
|
+
/** Options for creating a communication socket handler */
|
|
5
|
+
export interface CommunicationHandlerOptions {
|
|
6
|
+
/** Communication config overrides */
|
|
7
|
+
config?: Partial<CommunicationConfig>;
|
|
8
|
+
/** Extract userId from socket handshake headers/query. Default: reads X-User-Id header or userId query param */
|
|
9
|
+
getUserId?: (headers: Record<string, any>, query: Record<string, any>) => string | undefined;
|
|
10
|
+
/** LumenJS database instance — if provided, messages are persisted */
|
|
11
|
+
db?: LumenDb;
|
|
12
|
+
/**
|
|
13
|
+
* Storage adapter for chat file uploads.
|
|
14
|
+
* Falls back to the global singleton (useStorage()) if not provided.
|
|
15
|
+
* Required for file:request-upload support.
|
|
16
|
+
*/
|
|
17
|
+
storage?: StorageAdapter;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a LumenJS-compatible socket handler for communication.
|
|
21
|
+
*
|
|
22
|
+
* Usage in a page file:
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { createCommunicationHandler } from '@nuraly/lumenjs/dist/communication/server.js';
|
|
25
|
+
* export const socket = createCommunicationHandler();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function createCommunicationHandler(options?: CommunicationHandlerOptions): (ctx: {
|
|
29
|
+
on: (event: string, handler: (...args: any[]) => void) => void;
|
|
30
|
+
push: (data: any) => void;
|
|
31
|
+
room: {
|
|
32
|
+
join: (name: string) => void;
|
|
33
|
+
leave: (name: string) => void;
|
|
34
|
+
broadcast: (name: string, data: any) => void;
|
|
35
|
+
broadcastAll: (name: string, data: any) => void;
|
|
36
|
+
};
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
headers: Record<string, any>;
|
|
39
|
+
locale?: string;
|
|
40
|
+
socket: any;
|
|
41
|
+
}) => (() => void) | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Creates reusable API handler functions for communication REST endpoints.
|
|
44
|
+
*
|
|
45
|
+
* Usage in an API route:
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { createCommunicationApiHandlers } from '@nuraly/lumenjs/dist/communication/server.js';
|
|
48
|
+
* import { useDb } from '@nuraly/lumenjs/dist/db/index.js';
|
|
49
|
+
*
|
|
50
|
+
* const communication = createCommunicationApiHandlers(useDb());
|
|
51
|
+
*
|
|
52
|
+
* export function GET(req) {
|
|
53
|
+
* return communication.getConversations(req.query.userId);
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function createCommunicationApiHandlers(db: LumenDb): {
|
|
58
|
+
/** List conversations for a user */
|
|
59
|
+
getConversations(userId: string, opts?: {
|
|
60
|
+
limit?: number;
|
|
61
|
+
offset?: number;
|
|
62
|
+
}): Promise<any[]>;
|
|
63
|
+
/** Get paginated message history for a conversation */
|
|
64
|
+
getMessages(conversationId: string, opts?: {
|
|
65
|
+
limit?: number;
|
|
66
|
+
before?: string;
|
|
67
|
+
}): Promise<any[]>;
|
|
68
|
+
/** Create a new conversation */
|
|
69
|
+
createConversation(data: {
|
|
70
|
+
type: "direct" | "group";
|
|
71
|
+
name?: string;
|
|
72
|
+
participantIds: string[];
|
|
73
|
+
}): Promise<any>;
|
|
74
|
+
/** Search messages by content */
|
|
75
|
+
searchMessages(query: string, opts?: {
|
|
76
|
+
conversationId?: string;
|
|
77
|
+
limit?: number;
|
|
78
|
+
}): Promise<any[]>;
|
|
79
|
+
/** Get a single message by ID */
|
|
80
|
+
getMessage(messageId: string): Promise<any>;
|
|
81
|
+
/** Delete a message (soft delete by setting content to empty) */
|
|
82
|
+
deleteMessage(messageId: string, userId: string): Promise<{
|
|
83
|
+
changes: number;
|
|
84
|
+
lastInsertRowid: number | bigint;
|
|
85
|
+
}>;
|
|
86
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { useCommunicationStore } from './store.js';
|
|
2
|
+
import { handleConversationCreate, handleConversationJoin, handleConversationLeave, handleConversationArchive, handleConversationMute, handleConversationPin, handleMessageSend, handleMessageRead, handleMessageReact, handleMessageEdit, handleMessageDelete, handleMessageForward, handleTypingStart, handleTypingStop, handlePresenceUpdate, handleConnect, handleDisconnect, handleFileUploadRequest, } from './handlers.js';
|
|
3
|
+
import { handleCallInitiate, handleCallRespond, handleCallHangup, handleCallMediaToggle, handleCallAddParticipant, handleCallRemoveParticipant, handleSignalOffer, handleSignalAnswer, handleSignalIceCandidate, handleSignalIceRestart, handleCallQualityReport, } from './signaling.js';
|
|
4
|
+
import { handleUploadKeys, handleRequestKeys, handleSessionInit, } from './encryption.js';
|
|
5
|
+
import { useStorage } from '../storage/index.js';
|
|
6
|
+
const defaultGetUserId = (headers, query) => {
|
|
7
|
+
return headers['x-user-id'] || query.userId || undefined;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates a LumenJS-compatible socket handler for communication.
|
|
11
|
+
*
|
|
12
|
+
* Usage in a page file:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createCommunicationHandler } from '@nuraly/lumenjs/dist/communication/server.js';
|
|
15
|
+
* export const socket = createCommunicationHandler();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function createCommunicationHandler(options = {}) {
|
|
19
|
+
const getUserId = options.getUserId || defaultGetUserId;
|
|
20
|
+
const store = useCommunicationStore();
|
|
21
|
+
const resolvedConfig = { ...options.config };
|
|
22
|
+
// Apply configurable typing timeout to the store
|
|
23
|
+
if (resolvedConfig.typingTimeoutMs != null) {
|
|
24
|
+
store.typingTimeoutMs = resolvedConfig.typingTimeoutMs;
|
|
25
|
+
}
|
|
26
|
+
return (ctx) => {
|
|
27
|
+
const query = ctx.socket?.handshake?.query || {};
|
|
28
|
+
const userId = getUserId(ctx.headers, query);
|
|
29
|
+
if (!userId) {
|
|
30
|
+
console.warn('[LumenJS:Communication] No userId found in socket handshake. Connection rejected.');
|
|
31
|
+
ctx.socket?.disconnect?.();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const socketId = ctx.socket?.id || crypto.randomUUID();
|
|
35
|
+
// Register socket and check if this is the user's first connection
|
|
36
|
+
const isFirstSocket = !store.isUserOnline(userId);
|
|
37
|
+
store.mapUserSocket(userId, socketId);
|
|
38
|
+
store.setPresence(userId, 'online');
|
|
39
|
+
// Build handler context
|
|
40
|
+
const handlerCtx = {
|
|
41
|
+
userId,
|
|
42
|
+
store,
|
|
43
|
+
config: resolvedConfig,
|
|
44
|
+
push: ctx.push,
|
|
45
|
+
broadcastAll: ctx.room.broadcastAll,
|
|
46
|
+
broadcast: ctx.room.broadcast,
|
|
47
|
+
joinRoom: ctx.room.join,
|
|
48
|
+
leaveRoom: ctx.room.leave,
|
|
49
|
+
emitToUser: (targetUserId, data) => {
|
|
50
|
+
const sockets = store.getSocketsForUser(targetUserId);
|
|
51
|
+
const io = ctx.socket?.nsp;
|
|
52
|
+
if (io) {
|
|
53
|
+
for (const sid of sockets) {
|
|
54
|
+
io.to(sid).emit('nk:data', data);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
db: options.db,
|
|
59
|
+
storage: options.storage ?? useStorage() ?? undefined,
|
|
60
|
+
};
|
|
61
|
+
// Build signaling context
|
|
62
|
+
const signalingCtx = {
|
|
63
|
+
userId,
|
|
64
|
+
store,
|
|
65
|
+
emitToSocket: (sid, data) => {
|
|
66
|
+
// For targeted emit, we use the raw socket.io namespace
|
|
67
|
+
const io = ctx.socket?.nsp;
|
|
68
|
+
if (io) {
|
|
69
|
+
io.to(sid).emit('nk:data', data);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
broadcastAll: ctx.room.broadcastAll,
|
|
73
|
+
};
|
|
74
|
+
// Broadcast online status if this is the user's first socket
|
|
75
|
+
if (isFirstSocket) {
|
|
76
|
+
handleConnect(handlerCtx);
|
|
77
|
+
}
|
|
78
|
+
// ── Payload Validation ────────────────────────────────────
|
|
79
|
+
/** Validate socket event payload is a non-null object with expected string fields. */
|
|
80
|
+
function validated(data, requiredStrings, handler) {
|
|
81
|
+
if (!data || typeof data !== 'object')
|
|
82
|
+
return;
|
|
83
|
+
for (const field of requiredStrings) {
|
|
84
|
+
if (typeof data[field] !== 'string' || data[field].length === 0)
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
handler(data);
|
|
88
|
+
}
|
|
89
|
+
// ── Chat Events ──────────────────────────────────────────
|
|
90
|
+
ctx.on('conversation:create', (data) => validated(data, ['type'], () => handleConversationCreate(handlerCtx, data)));
|
|
91
|
+
ctx.on('conversation:join', (data) => validated(data, ['conversationId'], () => handleConversationJoin(handlerCtx, data)));
|
|
92
|
+
ctx.on('conversation:leave', (data) => validated(data, ['conversationId'], () => handleConversationLeave(handlerCtx, data)));
|
|
93
|
+
ctx.on('conversation:archive', (data) => validated(data, ['conversationId'], () => handleConversationArchive(handlerCtx, data)));
|
|
94
|
+
ctx.on('conversation:mute', (data) => validated(data, ['conversationId'], () => handleConversationMute(handlerCtx, data)));
|
|
95
|
+
ctx.on('conversation:pin', (data) => validated(data, ['conversationId'], () => handleConversationPin(handlerCtx, data)));
|
|
96
|
+
ctx.on('message:send', (data) => validated(data, ['conversationId', 'content'], () => handleMessageSend(handlerCtx, data)));
|
|
97
|
+
ctx.on('message:react', (data) => validated(data, ['messageId', 'conversationId', 'emoji'], () => handleMessageReact(handlerCtx, data)));
|
|
98
|
+
ctx.on('message:edit', (data) => validated(data, ['messageId', 'conversationId', 'content'], () => handleMessageEdit(handlerCtx, data)));
|
|
99
|
+
ctx.on('message:delete', (data) => validated(data, ['messageId', 'conversationId'], () => handleMessageDelete(handlerCtx, data)));
|
|
100
|
+
ctx.on('message:read', (data) => validated(data, ['messageId', 'conversationId'], () => handleMessageRead(handlerCtx, data)));
|
|
101
|
+
ctx.on('message:forward', (data) => validated(data, ['messageId', 'fromConversationId', 'toConversationId'], () => handleMessageForward(handlerCtx, data)));
|
|
102
|
+
ctx.on('typing:start', (data) => validated(data, ['conversationId'], () => handleTypingStart(handlerCtx, data)));
|
|
103
|
+
ctx.on('typing:stop', (data) => validated(data, ['conversationId'], () => handleTypingStop(handlerCtx, data)));
|
|
104
|
+
ctx.on('presence:update', (data) => validated(data, ['status'], () => handlePresenceUpdate(handlerCtx, data)));
|
|
105
|
+
// ── Call Events ──────────────────────────────────────────
|
|
106
|
+
ctx.on('call:initiate', (data) => handleCallInitiate(signalingCtx, data));
|
|
107
|
+
ctx.on('call:respond', (data) => handleCallRespond(signalingCtx, data));
|
|
108
|
+
ctx.on('call:hangup', (data) => handleCallHangup(signalingCtx, data));
|
|
109
|
+
ctx.on('call:media-toggle', (data) => handleCallMediaToggle(signalingCtx, data));
|
|
110
|
+
ctx.on('call:add-participant', (data) => handleCallAddParticipant(signalingCtx, data));
|
|
111
|
+
ctx.on('call:remove-participant', (data) => handleCallRemoveParticipant(signalingCtx, data));
|
|
112
|
+
// ── WebRTC Signaling ─────────────────────────────────────
|
|
113
|
+
ctx.on('signal:offer', (data) => handleSignalOffer(signalingCtx, data));
|
|
114
|
+
ctx.on('signal:answer', (data) => handleSignalAnswer(signalingCtx, data));
|
|
115
|
+
ctx.on('signal:ice-candidate', (data) => handleSignalIceCandidate(signalingCtx, data));
|
|
116
|
+
ctx.on('signal:ice-restart', (data) => handleSignalIceRestart(signalingCtx, data));
|
|
117
|
+
ctx.on('call:quality-report', (data) => handleCallQualityReport(signalingCtx, data));
|
|
118
|
+
// ── E2E Encryption ─────────────────────────────────────────
|
|
119
|
+
const encryptionCtx = {
|
|
120
|
+
userId,
|
|
121
|
+
store,
|
|
122
|
+
push: ctx.push,
|
|
123
|
+
emitToUser: (targetUserId, data) => {
|
|
124
|
+
const sockets = store.getSocketsForUser(targetUserId);
|
|
125
|
+
const io = ctx.socket?.nsp;
|
|
126
|
+
if (io) {
|
|
127
|
+
for (const sid of sockets) {
|
|
128
|
+
io.to(sid).emit('nk:data', data);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
db: options.db,
|
|
133
|
+
};
|
|
134
|
+
ctx.on('encryption:upload-keys', (data) => handleUploadKeys(encryptionCtx, data));
|
|
135
|
+
ctx.on('encryption:request-keys', (data) => handleRequestKeys(encryptionCtx, data));
|
|
136
|
+
ctx.on('encryption:session-init', (data) => handleSessionInit(encryptionCtx, data));
|
|
137
|
+
// ── File Upload ───────────────────────────────────────────
|
|
138
|
+
ctx.on('file:request-upload', (data) => handleFileUploadRequest(handlerCtx, data));
|
|
139
|
+
// ── Cleanup on Disconnect ────────────────────────────────
|
|
140
|
+
return () => {
|
|
141
|
+
handleDisconnect(handlerCtx, socketId);
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Creates reusable API handler functions for communication REST endpoints.
|
|
147
|
+
*
|
|
148
|
+
* Usage in an API route:
|
|
149
|
+
* ```ts
|
|
150
|
+
* import { createCommunicationApiHandlers } from '@nuraly/lumenjs/dist/communication/server.js';
|
|
151
|
+
* import { useDb } from '@nuraly/lumenjs/dist/db/index.js';
|
|
152
|
+
*
|
|
153
|
+
* const communication = createCommunicationApiHandlers(useDb());
|
|
154
|
+
*
|
|
155
|
+
* export function GET(req) {
|
|
156
|
+
* return communication.getConversations(req.query.userId);
|
|
157
|
+
* }
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function createCommunicationApiHandlers(db) {
|
|
161
|
+
return {
|
|
162
|
+
/** List conversations for a user */
|
|
163
|
+
async getConversations(userId, opts) {
|
|
164
|
+
const limit = opts?.limit || 50;
|
|
165
|
+
const offset = opts?.offset || 0;
|
|
166
|
+
return db.all(`SELECT c.*,
|
|
167
|
+
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id
|
|
168
|
+
AND m.id NOT IN (SELECT message_id FROM read_receipts WHERE user_id = ?)) as unread_count
|
|
169
|
+
FROM conversations c
|
|
170
|
+
JOIN conversation_participants cp ON cp.conversation_id = c.id
|
|
171
|
+
WHERE cp.user_id = ?
|
|
172
|
+
ORDER BY c.updated_at DESC
|
|
173
|
+
LIMIT ? OFFSET ?`, userId, userId, limit, offset);
|
|
174
|
+
},
|
|
175
|
+
/** Get paginated message history for a conversation */
|
|
176
|
+
async getMessages(conversationId, opts) {
|
|
177
|
+
const limit = opts?.limit || 50;
|
|
178
|
+
if (opts?.before) {
|
|
179
|
+
return db.all(`SELECT * FROM messages WHERE conversation_id = ? AND created_at < ?
|
|
180
|
+
ORDER BY created_at DESC LIMIT ?`, conversationId, opts.before, limit);
|
|
181
|
+
}
|
|
182
|
+
return db.all(`SELECT * FROM messages WHERE conversation_id = ?
|
|
183
|
+
ORDER BY created_at DESC LIMIT ?`, conversationId, limit);
|
|
184
|
+
},
|
|
185
|
+
/** Create a new conversation */
|
|
186
|
+
async createConversation(data) {
|
|
187
|
+
const now = new Date().toISOString();
|
|
188
|
+
const convId = crypto.randomUUID();
|
|
189
|
+
await db.run(`INSERT INTO conversations (id, type, name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)`, convId, data.type, data.name || null, now, now);
|
|
190
|
+
for (const uid of data.participantIds) {
|
|
191
|
+
await db.run(`INSERT INTO conversation_participants (conversation_id, user_id, role, joined_at) VALUES (?, ?, 'member', ?)`, convId, uid, now);
|
|
192
|
+
}
|
|
193
|
+
return db.get(`SELECT * FROM conversations WHERE id = ?`, convId);
|
|
194
|
+
},
|
|
195
|
+
/** Search messages by content */
|
|
196
|
+
async searchMessages(query, opts) {
|
|
197
|
+
const limit = opts?.limit || 20;
|
|
198
|
+
if (opts?.conversationId) {
|
|
199
|
+
return db.all(`SELECT * FROM messages WHERE conversation_id = ? AND content LIKE ? ORDER BY created_at DESC LIMIT ?`, opts.conversationId, `%${query}%`, limit);
|
|
200
|
+
}
|
|
201
|
+
return db.all(`SELECT * FROM messages WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?`, `%${query}%`, limit);
|
|
202
|
+
},
|
|
203
|
+
/** Get a single message by ID */
|
|
204
|
+
async getMessage(messageId) {
|
|
205
|
+
return db.get(`SELECT * FROM messages WHERE id = ?`, messageId);
|
|
206
|
+
},
|
|
207
|
+
/** Delete a message (soft delete by setting content to empty) */
|
|
208
|
+
async deleteMessage(messageId, userId) {
|
|
209
|
+
return db.run(`UPDATE messages SET content = '', type = 'system', updated_at = ? WHERE id = ? AND sender_id = ?`, new Date().toISOString(), messageId, userId);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { CallType, CallEndReason, SignalOffer, SignalIceCandidate, SignalIceRestart, ConnectionQualityReport } from './types.js';
|
|
2
|
+
import type { CommunicationStore } from './store.js';
|
|
3
|
+
/** Context for signaling handlers */
|
|
4
|
+
export interface SignalingContext {
|
|
5
|
+
userId: string;
|
|
6
|
+
store: CommunicationStore;
|
|
7
|
+
/** Emit to a specific socket by ID */
|
|
8
|
+
emitToSocket: (socketId: string, data: any) => void;
|
|
9
|
+
/** Broadcast to all sockets in a room */
|
|
10
|
+
broadcastAll: (room: string, data: any) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleCallInitiate(ctx: SignalingContext, data: {
|
|
13
|
+
conversationId: string;
|
|
14
|
+
type: CallType;
|
|
15
|
+
calleeIds: string[];
|
|
16
|
+
}): void;
|
|
17
|
+
export declare function handleCallRespond(ctx: SignalingContext, data: {
|
|
18
|
+
callId: string;
|
|
19
|
+
action: 'accept' | 'reject';
|
|
20
|
+
}): void;
|
|
21
|
+
export declare function handleCallHangup(ctx: SignalingContext, data: {
|
|
22
|
+
callId: string;
|
|
23
|
+
reason: CallEndReason;
|
|
24
|
+
}): void;
|
|
25
|
+
export declare function handleCallMediaToggle(ctx: SignalingContext, data: {
|
|
26
|
+
callId: string;
|
|
27
|
+
audio?: boolean;
|
|
28
|
+
video?: boolean;
|
|
29
|
+
screenShare?: boolean;
|
|
30
|
+
}): void;
|
|
31
|
+
export declare function handleCallAddParticipant(ctx: SignalingContext, data: {
|
|
32
|
+
callId: string;
|
|
33
|
+
userId: string;
|
|
34
|
+
}): void;
|
|
35
|
+
export declare function handleCallRemoveParticipant(ctx: SignalingContext, data: {
|
|
36
|
+
callId: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
}): void;
|
|
39
|
+
export declare function handleSignalOffer(ctx: SignalingContext, data: SignalOffer): void;
|
|
40
|
+
export declare function handleSignalAnswer(ctx: SignalingContext, data: SignalOffer): void;
|
|
41
|
+
export declare function handleSignalIceCandidate(ctx: SignalingContext, data: SignalIceCandidate): void;
|
|
42
|
+
export declare function handleSignalIceRestart(ctx: SignalingContext, data: SignalIceRestart): void;
|
|
43
|
+
export declare function handleCallQualityReport(ctx: SignalingContext, data: ConnectionQualityReport): void;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/** Emit data to all sockets belonging to a user */
|
|
2
|
+
function emitToUser(ctx, targetUserId, data) {
|
|
3
|
+
const sockets = ctx.store.getSocketsForUser(targetUserId);
|
|
4
|
+
for (const sid of sockets) {
|
|
5
|
+
ctx.emitToSocket(sid, data);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
// ── Call Lifecycle ───────────────────────────────────────────────
|
|
9
|
+
export function handleCallInitiate(ctx, data) {
|
|
10
|
+
// Check if caller is already in a call
|
|
11
|
+
const existingCall = ctx.store.getActiveCallForUser(ctx.userId);
|
|
12
|
+
if (existingCall) {
|
|
13
|
+
emitToUser(ctx, ctx.userId, {
|
|
14
|
+
event: 'call:state-changed',
|
|
15
|
+
data: { callId: existingCall.id, state: existingCall.state, error: 'already_in_call' },
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const callId = crypto.randomUUID();
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
const call = {
|
|
22
|
+
id: callId,
|
|
23
|
+
conversationId: data.conversationId,
|
|
24
|
+
type: data.type,
|
|
25
|
+
state: 'initiating',
|
|
26
|
+
callerId: ctx.userId,
|
|
27
|
+
calleeIds: data.calleeIds,
|
|
28
|
+
startedAt: now,
|
|
29
|
+
participants: [{
|
|
30
|
+
userId: ctx.userId,
|
|
31
|
+
joinedAt: now,
|
|
32
|
+
audioMuted: false,
|
|
33
|
+
videoMuted: data.type === 'audio',
|
|
34
|
+
screenSharing: false,
|
|
35
|
+
}],
|
|
36
|
+
};
|
|
37
|
+
ctx.store.addCall(call);
|
|
38
|
+
// Notify caller that call is initiating
|
|
39
|
+
emitToUser(ctx, ctx.userId, {
|
|
40
|
+
event: 'call:state-changed',
|
|
41
|
+
data: { callId, state: 'initiating' },
|
|
42
|
+
});
|
|
43
|
+
// Notify each callee with incoming call
|
|
44
|
+
for (const calleeId of data.calleeIds) {
|
|
45
|
+
// Check if callee is busy (exclude the current call being initiated)
|
|
46
|
+
const calleeActiveCall = ctx.store.getActiveCallForUser(calleeId);
|
|
47
|
+
if (calleeActiveCall && calleeActiveCall.id !== callId) {
|
|
48
|
+
emitToUser(ctx, ctx.userId, {
|
|
49
|
+
event: 'call:state-changed',
|
|
50
|
+
data: { callId, state: 'ended', endReason: 'busy' },
|
|
51
|
+
});
|
|
52
|
+
ctx.store.updateCallState(callId, 'ended', 'busy');
|
|
53
|
+
ctx.store.removeCall(callId);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
emitToUser(ctx, calleeId, { event: 'call:incoming', data: call });
|
|
57
|
+
}
|
|
58
|
+
// Update state to ringing
|
|
59
|
+
ctx.store.updateCallState(callId, 'ringing');
|
|
60
|
+
}
|
|
61
|
+
export function handleCallRespond(ctx, data) {
|
|
62
|
+
const call = ctx.store.getCall(data.callId);
|
|
63
|
+
if (!call)
|
|
64
|
+
return;
|
|
65
|
+
if (data.action === 'reject') {
|
|
66
|
+
ctx.store.updateCallState(data.callId, 'ended', 'rejected');
|
|
67
|
+
// Notify all parties
|
|
68
|
+
emitToUser(ctx, call.callerId, {
|
|
69
|
+
event: 'call:state-changed',
|
|
70
|
+
data: { callId: data.callId, state: 'ended', endReason: 'rejected' },
|
|
71
|
+
});
|
|
72
|
+
emitToUser(ctx, ctx.userId, {
|
|
73
|
+
event: 'call:state-changed',
|
|
74
|
+
data: { callId: data.callId, state: 'ended', endReason: 'rejected' },
|
|
75
|
+
});
|
|
76
|
+
ctx.store.removeCall(data.callId);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Accept — add callee as participant
|
|
80
|
+
const now = new Date().toISOString();
|
|
81
|
+
const participant = {
|
|
82
|
+
userId: ctx.userId,
|
|
83
|
+
joinedAt: now,
|
|
84
|
+
audioMuted: false,
|
|
85
|
+
videoMuted: call.type === 'audio',
|
|
86
|
+
screenSharing: false,
|
|
87
|
+
};
|
|
88
|
+
ctx.store.addCallParticipant(data.callId, participant);
|
|
89
|
+
ctx.store.updateCallState(data.callId, 'connecting');
|
|
90
|
+
// Notify caller that callee accepted
|
|
91
|
+
emitToUser(ctx, call.callerId, {
|
|
92
|
+
event: 'call:participant-joined',
|
|
93
|
+
data: { callId: data.callId, participant },
|
|
94
|
+
});
|
|
95
|
+
emitToUser(ctx, call.callerId, {
|
|
96
|
+
event: 'call:state-changed',
|
|
97
|
+
data: { callId: data.callId, state: 'connecting' },
|
|
98
|
+
});
|
|
99
|
+
// Notify callee of connecting state
|
|
100
|
+
emitToUser(ctx, ctx.userId, {
|
|
101
|
+
event: 'call:state-changed',
|
|
102
|
+
data: { callId: data.callId, state: 'connecting' },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function handleCallHangup(ctx, data) {
|
|
106
|
+
const call = ctx.store.getCall(data.callId);
|
|
107
|
+
if (!call)
|
|
108
|
+
return;
|
|
109
|
+
// Remove the user who hung up
|
|
110
|
+
ctx.store.removeCallParticipant(data.callId, ctx.userId);
|
|
111
|
+
// Notify other participants
|
|
112
|
+
for (const p of call.participants) {
|
|
113
|
+
if (p.userId === ctx.userId)
|
|
114
|
+
continue;
|
|
115
|
+
emitToUser(ctx, p.userId, {
|
|
116
|
+
event: 'call:participant-left',
|
|
117
|
+
data: { callId: data.callId, userId: ctx.userId },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Also notify caller/callees who may not be in participants yet (e.g., still ringing)
|
|
121
|
+
const allUsers = new Set([call.callerId, ...call.calleeIds]);
|
|
122
|
+
allUsers.delete(ctx.userId);
|
|
123
|
+
// If no participants left (or only one), end the call
|
|
124
|
+
const remainingParticipants = call.participants.filter(p => p.userId !== ctx.userId);
|
|
125
|
+
if (remainingParticipants.length <= 1) {
|
|
126
|
+
ctx.store.updateCallState(data.callId, 'ended', data.reason);
|
|
127
|
+
for (const uid of allUsers) {
|
|
128
|
+
emitToUser(ctx, uid, {
|
|
129
|
+
event: 'call:state-changed',
|
|
130
|
+
data: { callId: data.callId, state: 'ended', endReason: data.reason },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
ctx.store.removeCall(data.callId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function handleCallMediaToggle(ctx, data) {
|
|
137
|
+
const call = ctx.store.getCall(data.callId);
|
|
138
|
+
if (!call)
|
|
139
|
+
return;
|
|
140
|
+
const participant = call.participants.find(p => p.userId === ctx.userId);
|
|
141
|
+
if (!participant)
|
|
142
|
+
return;
|
|
143
|
+
if (data.audio !== undefined)
|
|
144
|
+
participant.audioMuted = !data.audio;
|
|
145
|
+
if (data.video !== undefined)
|
|
146
|
+
participant.videoMuted = !data.video;
|
|
147
|
+
if (data.screenShare !== undefined)
|
|
148
|
+
participant.screenSharing = data.screenShare;
|
|
149
|
+
// Notify all other participants
|
|
150
|
+
for (const p of call.participants) {
|
|
151
|
+
if (p.userId === ctx.userId)
|
|
152
|
+
continue;
|
|
153
|
+
emitToUser(ctx, p.userId, {
|
|
154
|
+
event: 'call:media-changed',
|
|
155
|
+
data: {
|
|
156
|
+
callId: data.callId,
|
|
157
|
+
userId: ctx.userId,
|
|
158
|
+
audio: data.audio,
|
|
159
|
+
video: data.video,
|
|
160
|
+
screenShare: data.screenShare,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ── Mid-Call Participant Management ──────────────────────────────
|
|
166
|
+
export function handleCallAddParticipant(ctx, data) {
|
|
167
|
+
const call = ctx.store.getCall(data.callId);
|
|
168
|
+
if (!call)
|
|
169
|
+
return;
|
|
170
|
+
// Only existing participants can add someone
|
|
171
|
+
const isParticipant = call.participants.some(p => p.userId === ctx.userId);
|
|
172
|
+
if (!isParticipant)
|
|
173
|
+
return;
|
|
174
|
+
// Don't add someone already in the call
|
|
175
|
+
if (call.participants.some(p => p.userId === data.userId))
|
|
176
|
+
return;
|
|
177
|
+
// Check if the target user is already in another call
|
|
178
|
+
const targetActiveCall = ctx.store.getActiveCallForUser(data.userId);
|
|
179
|
+
if (targetActiveCall) {
|
|
180
|
+
emitToUser(ctx, ctx.userId, {
|
|
181
|
+
event: 'call:state-changed',
|
|
182
|
+
data: { callId: data.callId, state: call.state, error: 'user_busy' },
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Add to calleeIds so they are tracked
|
|
187
|
+
if (!call.calleeIds.includes(data.userId)) {
|
|
188
|
+
call.calleeIds.push(data.userId);
|
|
189
|
+
}
|
|
190
|
+
// Send incoming call notification to the new user
|
|
191
|
+
emitToUser(ctx, data.userId, { event: 'call:incoming', data: call });
|
|
192
|
+
}
|
|
193
|
+
export function handleCallRemoveParticipant(ctx, data) {
|
|
194
|
+
const call = ctx.store.getCall(data.callId);
|
|
195
|
+
if (!call)
|
|
196
|
+
return;
|
|
197
|
+
// Only the caller (owner) or admins can kick participants
|
|
198
|
+
if (ctx.userId !== call.callerId)
|
|
199
|
+
return;
|
|
200
|
+
// Can't remove yourself (use hangup instead)
|
|
201
|
+
if (data.userId === ctx.userId)
|
|
202
|
+
return;
|
|
203
|
+
// Check the target is actually in the call
|
|
204
|
+
const targetParticipant = call.participants.find(p => p.userId === data.userId);
|
|
205
|
+
if (!targetParticipant)
|
|
206
|
+
return;
|
|
207
|
+
ctx.store.removeCallParticipant(data.callId, data.userId);
|
|
208
|
+
// Notify all remaining participants
|
|
209
|
+
for (const p of call.participants) {
|
|
210
|
+
if (p.userId === data.userId)
|
|
211
|
+
continue;
|
|
212
|
+
emitToUser(ctx, p.userId, {
|
|
213
|
+
event: 'call:participant-left',
|
|
214
|
+
data: { callId: data.callId, userId: data.userId },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// Notify the removed user
|
|
218
|
+
emitToUser(ctx, data.userId, {
|
|
219
|
+
event: 'call:participant-left',
|
|
220
|
+
data: { callId: data.callId, userId: data.userId },
|
|
221
|
+
});
|
|
222
|
+
emitToUser(ctx, data.userId, {
|
|
223
|
+
event: 'call:state-changed',
|
|
224
|
+
data: { callId: data.callId, state: 'ended', endReason: 'completed' },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// ── WebRTC Signal Relay ─────────────────────────────────────────
|
|
228
|
+
/** Verify the sender is part of an active call with the target user. */
|
|
229
|
+
function verifyCallMembership(ctx, fromUserId, toUserId) {
|
|
230
|
+
// Ensure fromUserId matches the authenticated user
|
|
231
|
+
if (fromUserId !== ctx.userId)
|
|
232
|
+
return false;
|
|
233
|
+
// Check both users are participants in the same active call
|
|
234
|
+
const call = ctx.store.getActiveCallForUser(ctx.userId);
|
|
235
|
+
if (!call)
|
|
236
|
+
return false;
|
|
237
|
+
const isTarget = call.callerId === toUserId || call.calleeIds.includes(toUserId) ||
|
|
238
|
+
call.participants.some(p => p.userId === toUserId);
|
|
239
|
+
return isTarget;
|
|
240
|
+
}
|
|
241
|
+
export function handleSignalOffer(ctx, data) {
|
|
242
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
243
|
+
return;
|
|
244
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:offer', data });
|
|
245
|
+
}
|
|
246
|
+
export function handleSignalAnswer(ctx, data) {
|
|
247
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
248
|
+
return;
|
|
249
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:answer', data });
|
|
250
|
+
}
|
|
251
|
+
export function handleSignalIceCandidate(ctx, data) {
|
|
252
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
253
|
+
return;
|
|
254
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:ice-candidate', data });
|
|
255
|
+
}
|
|
256
|
+
export function handleSignalIceRestart(ctx, data) {
|
|
257
|
+
if (!verifyCallMembership(ctx, data.fromUserId, data.toUserId))
|
|
258
|
+
return;
|
|
259
|
+
emitToUser(ctx, data.toUserId, { event: 'signal:ice-restart', data });
|
|
260
|
+
}
|
|
261
|
+
export function handleCallQualityReport(ctx, data) {
|
|
262
|
+
const call = ctx.store.getCall(data.callId);
|
|
263
|
+
if (!call)
|
|
264
|
+
return;
|
|
265
|
+
// Broadcast quality report to all other participants in the call
|
|
266
|
+
for (const p of call.participants) {
|
|
267
|
+
if (p.userId === ctx.userId)
|
|
268
|
+
continue;
|
|
269
|
+
emitToUser(ctx, p.userId, { event: 'call:quality-changed', data });
|
|
270
|
+
}
|
|
271
|
+
}
|