libre-webui 0.25.0 → 0.26.0
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 +32 -22
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +26 -4
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/middleware/auth.d.ts +7 -0
- package/backend/dist/middleware/auth.d.ts.map +1 -1
- package/backend/dist/middleware/auth.js +26 -0
- package/backend/dist/middleware/auth.js.map +1 -1
- package/backend/dist/middleware/index.d.ts +20 -0
- package/backend/dist/middleware/index.d.ts.map +1 -1
- package/backend/dist/middleware/index.js +105 -2
- package/backend/dist/middleware/index.js.map +1 -1
- package/backend/dist/models/personaModel.d.ts.map +1 -1
- package/backend/dist/models/personaModel.js +2 -0
- package/backend/dist/models/personaModel.js.map +1 -1
- package/backend/dist/observability/otel.d.ts +40 -0
- package/backend/dist/observability/otel.d.ts.map +1 -0
- package/backend/dist/observability/otel.js +337 -0
- package/backend/dist/observability/otel.js.map +1 -0
- package/backend/dist/observability/requestContext.d.ts +14 -0
- package/backend/dist/observability/requestContext.d.ts.map +1 -0
- package/backend/dist/observability/requestContext.js +21 -0
- package/backend/dist/observability/requestContext.js.map +1 -0
- package/backend/dist/persistence/chatGenerationTypes.d.ts +9 -0
- package/backend/dist/persistence/chatGenerationTypes.d.ts.map +1 -1
- package/backend/dist/persistence/extensionTypes.d.ts +13 -0
- package/backend/dist/persistence/extensionTypes.d.ts.map +1 -1
- package/backend/dist/persistence/extensionTypes.js.map +1 -1
- package/backend/dist/persistence/index.d.ts +1 -1
- package/backend/dist/persistence/index.d.ts.map +1 -1
- package/backend/dist/persistence/index.js.map +1 -1
- package/backend/dist/persistence/postgresAgentFoundationMigration.d.ts +9 -0
- package/backend/dist/persistence/postgresAgentFoundationMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresAgentFoundationMigration.js +164 -0
- package/backend/dist/persistence/postgresAgentFoundationMigration.js.map +1 -0
- package/backend/dist/persistence/postgresExtensionRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresExtensionRepositories.js +46 -2
- package/backend/dist/persistence/postgresExtensionRepositories.js.map +1 -1
- package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.d.ts +10 -0
- package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.js +148 -0
- package/backend/dist/persistence/postgresMediaEnterpriseOpsMigration.js.map +1 -0
- package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
- package/backend/dist/persistence/postgresMigrationRegistry.js +10 -0
- package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
- package/backend/dist/persistence/postgresNotesV2Migration.d.ts +4 -0
- package/backend/dist/persistence/postgresNotesV2Migration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresNotesV2Migration.js +57 -0
- package/backend/dist/persistence/postgresNotesV2Migration.js.map +1 -0
- package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresResourceRepositories.js +1646 -11
- package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
- package/backend/dist/persistence/postgresSkillFilesMigration.d.ts +9 -0
- package/backend/dist/persistence/postgresSkillFilesMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresSkillFilesMigration.js +50 -0
- package/backend/dist/persistence/postgresSkillFilesMigration.js.map +1 -0
- package/backend/dist/persistence/postgresTeamCollaborationMigration.d.ts +10 -0
- package/backend/dist/persistence/postgresTeamCollaborationMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresTeamCollaborationMigration.js +161 -0
- package/backend/dist/persistence/postgresTeamCollaborationMigration.js.map +1 -0
- package/backend/dist/persistence/resourceTypes.d.ts +541 -3
- package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
- package/backend/dist/persistence/resourceTypes.js.map +1 -1
- package/backend/dist/persistence/sqliteExtensionRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteExtensionRepositories.js +26 -4
- package/backend/dist/persistence/sqliteExtensionRepositories.js.map +1 -1
- package/backend/dist/persistence/sqliteMigrations.d.ts +9 -0
- package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteMigrations.js +1196 -29
- package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.js +1523 -134
- package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.js +257 -3
- package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
- package/backend/dist/platform/jobs/domainJobContracts.d.ts +8 -0
- package/backend/dist/platform/jobs/domainJobContracts.d.ts.map +1 -1
- package/backend/dist/platform/jobs/domainJobContracts.js +8 -0
- package/backend/dist/platform/jobs/domainJobContracts.js.map +1 -1
- package/backend/dist/platform/jobs/domainJobHandlers.d.ts.map +1 -1
- package/backend/dist/platform/jobs/domainJobHandlers.js +310 -3
- package/backend/dist/platform/jobs/domainJobHandlers.js.map +1 -1
- package/backend/dist/platform/jobs/embeddedDurableJobWorker.d.ts.map +1 -1
- package/backend/dist/platform/jobs/embeddedDurableJobWorker.js +4 -2
- package/backend/dist/platform/jobs/embeddedDurableJobWorker.js.map +1 -1
- package/backend/dist/platform/jobs/postgresDurableJobWorker.d.ts.map +1 -1
- package/backend/dist/platform/jobs/postgresDurableJobWorker.js +4 -2
- package/backend/dist/platform/jobs/postgresDurableJobWorker.js.map +1 -1
- package/backend/dist/platform/storage/pgVectorStore.d.ts +2 -1
- package/backend/dist/platform/storage/pgVectorStore.d.ts.map +1 -1
- package/backend/dist/platform/storage/pgVectorStore.js +51 -0
- package/backend/dist/platform/storage/pgVectorStore.js.map +1 -1
- package/backend/dist/platform/storage/platformDomainRepositories.d.ts +8 -2
- package/backend/dist/platform/storage/platformDomainRepositories.d.ts.map +1 -1
- package/backend/dist/platform/storage/postgresPlatformDomainRepositories.d.ts.map +1 -1
- package/backend/dist/platform/storage/postgresPlatformDomainRepositories.js +25 -4
- package/backend/dist/platform/storage/postgresPlatformDomainRepositories.js.map +1 -1
- package/backend/dist/platform/storage/sqliteEncryptedVectorStore.d.ts +2 -1
- package/backend/dist/platform/storage/sqliteEncryptedVectorStore.d.ts.map +1 -1
- package/backend/dist/platform/storage/sqliteEncryptedVectorStore.js +35 -0
- package/backend/dist/platform/storage/sqliteEncryptedVectorStore.js.map +1 -1
- package/backend/dist/platform/storage/sqlitePlatformDomainRepositories.d.ts.map +1 -1
- package/backend/dist/platform/storage/sqlitePlatformDomainRepositories.js +26 -3
- package/backend/dist/platform/storage/sqlitePlatformDomainRepositories.js.map +1 -1
- package/backend/dist/platform/storage/vectorStore.d.ts +14 -0
- package/backend/dist/platform/storage/vectorStore.d.ts.map +1 -1
- package/backend/dist/platform/storage/vectorStore.js.map +1 -1
- package/backend/dist/routes/access.d.ts.map +1 -1
- package/backend/dist/routes/access.js +84 -3
- package/backend/dist/routes/access.js.map +1 -1
- package/backend/dist/routes/calendar.d.ts.map +1 -1
- package/backend/dist/routes/calendar.js +125 -22
- package/backend/dist/routes/calendar.js.map +1 -1
- package/backend/dist/routes/channels.d.ts +3 -0
- package/backend/dist/routes/channels.d.ts.map +1 -0
- package/backend/dist/routes/channels.js +447 -0
- package/backend/dist/routes/channels.js.map +1 -0
- package/backend/dist/routes/chat.d.ts.map +1 -1
- package/backend/dist/routes/chat.js +195 -7
- package/backend/dist/routes/chat.js.map +1 -1
- package/backend/dist/routes/costs.d.ts +3 -0
- package/backend/dist/routes/costs.d.ts.map +1 -0
- package/backend/dist/routes/costs.js +191 -0
- package/backend/dist/routes/costs.js.map +1 -0
- package/backend/dist/routes/documents.d.ts.map +1 -1
- package/backend/dist/routes/documents.js +64 -21
- package/backend/dist/routes/documents.js.map +1 -1
- package/backend/dist/routes/evaluations.d.ts +3 -0
- package/backend/dist/routes/evaluations.d.ts.map +1 -0
- package/backend/dist/routes/evaluations.js +352 -0
- package/backend/dist/routes/evaluations.js.map +1 -0
- package/backend/dist/routes/imageGen.d.ts.map +1 -1
- package/backend/dist/routes/imageGen.js +2 -1
- package/backend/dist/routes/imageGen.js.map +1 -1
- package/backend/dist/routes/media.d.ts.map +1 -1
- package/backend/dist/routes/media.js +228 -7
- package/backend/dist/routes/media.js.map +1 -1
- package/backend/dist/routes/notes.d.ts.map +1 -1
- package/backend/dist/routes/notes.js +149 -36
- package/backend/dist/routes/notes.js.map +1 -1
- package/backend/dist/routes/notifications.d.ts +3 -0
- package/backend/dist/routes/notifications.d.ts.map +1 -0
- package/backend/dist/routes/notifications.js +213 -0
- package/backend/dist/routes/notifications.js.map +1 -0
- package/backend/dist/routes/openaiCompat.d.ts +3 -0
- package/backend/dist/routes/openaiCompat.d.ts.map +1 -0
- package/backend/dist/routes/openaiCompat.js +314 -0
- package/backend/dist/routes/openaiCompat.js.map +1 -0
- package/backend/dist/routes/personas.js +1 -1
- package/backend/dist/routes/personas.js.map +1 -1
- package/backend/dist/routes/prompts.d.ts +3 -0
- package/backend/dist/routes/prompts.d.ts.map +1 -0
- package/backend/dist/routes/prompts.js +173 -0
- package/backend/dist/routes/prompts.js.map +1 -0
- package/backend/dist/routes/search.d.ts.map +1 -1
- package/backend/dist/routes/search.js +29 -1
- package/backend/dist/routes/search.js.map +1 -1
- package/backend/dist/routes/skills.d.ts +3 -0
- package/backend/dist/routes/skills.d.ts.map +1 -0
- package/backend/dist/routes/skills.js +246 -0
- package/backend/dist/routes/skills.js.map +1 -0
- package/backend/dist/routes/stt.d.ts.map +1 -1
- package/backend/dist/routes/stt.js +2 -1
- package/backend/dist/routes/stt.js.map +1 -1
- package/backend/dist/routes/tools.d.ts +3 -0
- package/backend/dist/routes/tools.d.ts.map +1 -0
- package/backend/dist/routes/tools.js +359 -0
- package/backend/dist/routes/tools.js.map +1 -0
- package/backend/dist/routes/tts.d.ts.map +1 -1
- package/backend/dist/routes/tts.js +63 -8
- package/backend/dist/routes/tts.js.map +1 -1
- package/backend/dist/routes/voiceAccess.d.ts +3 -0
- package/backend/dist/routes/voiceAccess.d.ts.map +1 -0
- package/backend/dist/routes/voiceAccess.js +102 -0
- package/backend/dist/routes/voiceAccess.js.map +1 -0
- package/backend/dist/services/apiTokenService.d.ts +2 -2
- package/backend/dist/services/apiTokenService.d.ts.map +1 -1
- package/backend/dist/services/apiTokenService.js +7 -0
- package/backend/dist/services/apiTokenService.js.map +1 -1
- package/backend/dist/services/authorizationService.d.ts +8 -4
- package/backend/dist/services/authorizationService.d.ts.map +1 -1
- package/backend/dist/services/authorizationService.js +31 -1
- package/backend/dist/services/authorizationService.js.map +1 -1
- package/backend/dist/services/automationSchedulerService.d.ts.map +1 -1
- package/backend/dist/services/automationSchedulerService.js +33 -3
- package/backend/dist/services/automationSchedulerService.js.map +1 -1
- package/backend/dist/services/automationService.d.ts +4 -1
- package/backend/dist/services/automationService.d.ts.map +1 -1
- package/backend/dist/services/automationService.js +25 -2
- package/backend/dist/services/automationService.js.map +1 -1
- package/backend/dist/services/builtinToolsService.d.ts +20 -0
- package/backend/dist/services/builtinToolsService.d.ts.map +1 -0
- package/backend/dist/services/builtinToolsService.js +661 -0
- package/backend/dist/services/builtinToolsService.js.map +1 -0
- package/backend/dist/services/calendarService.d.ts +74 -0
- package/backend/dist/services/calendarService.d.ts.map +1 -0
- package/backend/dist/services/calendarService.js +339 -0
- package/backend/dist/services/calendarService.js.map +1 -0
- package/backend/dist/services/channelService.d.ts +195 -0
- package/backend/dist/services/channelService.d.ts.map +1 -0
- package/backend/dist/services/channelService.js +1057 -0
- package/backend/dist/services/channelService.js.map +1 -0
- package/backend/dist/services/chatRequestService.d.ts.map +1 -1
- package/backend/dist/services/chatRequestService.js +29 -1
- package/backend/dist/services/chatRequestService.js.map +1 -1
- package/backend/dist/services/chatService.d.ts +61 -1
- package/backend/dist/services/chatService.d.ts.map +1 -1
- package/backend/dist/services/chatService.js +218 -1
- package/backend/dist/services/chatService.js.map +1 -1
- package/backend/dist/services/chatToolRuntimeService.d.ts +55 -0
- package/backend/dist/services/chatToolRuntimeService.d.ts.map +1 -0
- package/backend/dist/services/chatToolRuntimeService.js +389 -0
- package/backend/dist/services/chatToolRuntimeService.js.map +1 -0
- package/backend/dist/services/costGovernanceService.d.ts +102 -0
- package/backend/dist/services/costGovernanceService.d.ts.map +1 -0
- package/backend/dist/services/costGovernanceService.js +463 -0
- package/backend/dist/services/costGovernanceService.js.map +1 -0
- package/backend/dist/services/dataArchiveService.d.ts +6 -0
- package/backend/dist/services/dataArchiveService.d.ts.map +1 -1
- package/backend/dist/services/dataArchiveService.js +23 -4
- package/backend/dist/services/dataArchiveService.js.map +1 -1
- package/backend/dist/services/documentService.d.ts +58 -1
- package/backend/dist/services/documentService.d.ts.map +1 -1
- package/backend/dist/services/documentService.js +289 -84
- package/backend/dist/services/documentService.js.map +1 -1
- package/backend/dist/services/durableChatGenerationService.d.ts +9 -0
- package/backend/dist/services/durableChatGenerationService.d.ts.map +1 -1
- package/backend/dist/services/durableChatGenerationService.js +152 -11
- package/backend/dist/services/durableChatGenerationService.js.map +1 -1
- package/backend/dist/services/evaluationService.d.ts +111 -0
- package/backend/dist/services/evaluationService.d.ts.map +1 -0
- package/backend/dist/services/evaluationService.js +403 -0
- package/backend/dist/services/evaluationService.js.map +1 -0
- package/backend/dist/services/galleryService.d.ts +6 -0
- package/backend/dist/services/galleryService.d.ts.map +1 -1
- package/backend/dist/services/galleryService.js +26 -0
- package/backend/dist/services/galleryService.js.map +1 -1
- package/backend/dist/services/mcpClientService.d.ts +29 -0
- package/backend/dist/services/mcpClientService.d.ts.map +1 -0
- package/backend/dist/services/mcpClientService.js +189 -0
- package/backend/dist/services/mcpClientService.js.map +1 -0
- package/backend/dist/services/noteService.d.ts +71 -0
- package/backend/dist/services/noteService.d.ts.map +1 -0
- package/backend/dist/services/noteService.js +397 -0
- package/backend/dist/services/noteService.js.map +1 -0
- package/backend/dist/services/notificationService.d.ts +55 -0
- package/backend/dist/services/notificationService.d.ts.map +1 -0
- package/backend/dist/services/notificationService.js +308 -0
- package/backend/dist/services/notificationService.js.map +1 -0
- package/backend/dist/services/openApiToolService.d.ts +38 -0
- package/backend/dist/services/openApiToolService.d.ts.map +1 -0
- package/backend/dist/services/openApiToolService.js +254 -0
- package/backend/dist/services/openApiToolService.js.map +1 -0
- package/backend/dist/services/personaService.d.ts +18 -2
- package/backend/dist/services/personaService.d.ts.map +1 -1
- package/backend/dist/services/personaService.js +113 -4
- package/backend/dist/services/personaService.js.map +1 -1
- package/backend/dist/services/pluginImageGenerationService.d.ts +19 -0
- package/backend/dist/services/pluginImageGenerationService.d.ts.map +1 -1
- package/backend/dist/services/pluginImageGenerationService.js +121 -0
- package/backend/dist/services/pluginImageGenerationService.js.map +1 -1
- package/backend/dist/services/pluginService.d.ts +7 -0
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +8 -2
- package/backend/dist/services/pluginService.js.map +1 -1
- package/backend/dist/services/promptService.d.ts +74 -0
- package/backend/dist/services/promptService.d.ts.map +1 -0
- package/backend/dist/services/promptService.js +513 -0
- package/backend/dist/services/promptService.js.map +1 -0
- package/backend/dist/services/resourceGrantService.d.ts.map +1 -1
- package/backend/dist/services/resourceGrantService.js +62 -17
- package/backend/dist/services/resourceGrantService.js.map +1 -1
- package/backend/dist/services/searchService.d.ts +29 -0
- package/backend/dist/services/searchService.d.ts.map +1 -0
- package/backend/dist/services/searchService.js +0 -0
- package/backend/dist/services/searchService.js.map +1 -0
- package/backend/dist/services/securityAuditService.d.ts.map +1 -1
- package/backend/dist/services/securityAuditService.js +2 -1
- package/backend/dist/services/securityAuditService.js.map +1 -1
- package/backend/dist/services/sharedResourceAccess.d.ts +25 -0
- package/backend/dist/services/sharedResourceAccess.d.ts.map +1 -0
- package/backend/dist/services/sharedResourceAccess.js +59 -0
- package/backend/dist/services/sharedResourceAccess.js.map +1 -0
- package/backend/dist/services/skillService.d.ts +137 -0
- package/backend/dist/services/skillService.d.ts.map +1 -0
- package/backend/dist/services/skillService.js +852 -0
- package/backend/dist/services/skillService.js.map +1 -0
- package/backend/dist/services/toolAccessService.d.ts +9 -0
- package/backend/dist/services/toolAccessService.d.ts.map +1 -0
- package/backend/dist/services/toolAccessService.js +60 -0
- package/backend/dist/services/toolAccessService.js.map +1 -0
- package/backend/dist/services/toolApprovalService.d.ts +34 -0
- package/backend/dist/services/toolApprovalService.d.ts.map +1 -0
- package/backend/dist/services/toolApprovalService.js +188 -0
- package/backend/dist/services/toolApprovalService.js.map +1 -0
- package/backend/dist/services/toolGatewayService.d.ts +53 -0
- package/backend/dist/services/toolGatewayService.d.ts.map +1 -0
- package/backend/dist/services/toolGatewayService.js +249 -0
- package/backend/dist/services/toolGatewayService.js.map +1 -0
- package/backend/dist/services/toolServerService.d.ts +57 -0
- package/backend/dist/services/toolServerService.d.ts.map +1 -0
- package/backend/dist/services/toolServerService.js +521 -0
- package/backend/dist/services/toolServerService.js.map +1 -0
- package/backend/dist/services/voiceAccessService.d.ts +10 -0
- package/backend/dist/services/voiceAccessService.d.ts.map +1 -0
- package/backend/dist/services/voiceAccessService.js +74 -0
- package/backend/dist/services/voiceAccessService.js.map +1 -0
- package/backend/dist/services/voiceProfileService.d.ts +20 -0
- package/backend/dist/services/voiceProfileService.d.ts.map +1 -1
- package/backend/dist/services/voiceProfileService.js +60 -0
- package/backend/dist/services/voiceProfileService.js.map +1 -1
- package/backend/dist/storage.d.ts +28 -1
- package/backend/dist/storage.d.ts.map +1 -1
- package/backend/dist/storage.js +100 -1
- package/backend/dist/storage.js.map +1 -1
- package/backend/dist/storageMappers.d.ts +2 -2
- package/backend/dist/storageMappers.d.ts.map +1 -1
- package/backend/dist/storageMappers.js.map +1 -1
- package/backend/dist/types/index.d.ts +197 -2
- package/backend/dist/types/index.d.ts.map +1 -1
- package/backend/dist/types/index.js.map +1 -1
- package/backend/dist/types/tools.d.ts +137 -0
- package/backend/dist/types/tools.d.ts.map +1 -0
- package/backend/dist/types/tools.js +25 -0
- package/backend/dist/types/tools.js.map +1 -0
- package/backend/dist/utils/chatDocumentContext.d.ts +26 -1
- package/backend/dist/utils/chatDocumentContext.d.ts.map +1 -1
- package/backend/dist/utils/chatDocumentContext.js +87 -18
- package/backend/dist/utils/chatDocumentContext.js.map +1 -1
- package/backend/dist/utils/documentExtraction.d.ts +69 -0
- package/backend/dist/utils/documentExtraction.d.ts.map +1 -0
- package/backend/dist/utils/documentExtraction.js +545 -0
- package/backend/dist/utils/documentExtraction.js.map +1 -0
- package/backend/dist/utils/hybridRetrieval.d.ts +34 -0
- package/backend/dist/utils/hybridRetrieval.d.ts.map +1 -0
- package/backend/dist/utils/hybridRetrieval.js +119 -0
- package/backend/dist/utils/hybridRetrieval.js.map +1 -0
- package/backend/dist/utils/icsCalendar.d.ts +27 -0
- package/backend/dist/utils/icsCalendar.d.ts.map +1 -0
- package/backend/dist/utils/icsCalendar.js +313 -0
- package/backend/dist/utils/icsCalendar.js.map +1 -0
- package/backend/dist/utils/imageEditUpload.d.ts +29 -0
- package/backend/dist/utils/imageEditUpload.d.ts.map +1 -0
- package/backend/dist/utils/imageEditUpload.js +82 -0
- package/backend/dist/utils/imageEditUpload.js.map +1 -0
- package/backend/dist/utils/logRedaction.d.ts +2 -0
- package/backend/dist/utils/logRedaction.d.ts.map +1 -0
- package/backend/dist/utils/logRedaction.js +66 -0
- package/backend/dist/utils/logRedaction.js.map +1 -0
- package/backend/dist/utils/logger.d.ts +3 -0
- package/backend/dist/utils/logger.d.ts.map +1 -1
- package/backend/dist/utils/logger.js +63 -20
- package/backend/dist/utils/logger.js.map +1 -1
- package/backend/dist/utils/pluginChatAdapter.d.ts +5 -1
- package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginChatAdapter.js +83 -0
- package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
- package/backend/dist/utils/pluginDefinitionTrust.js +1 -1
- package/backend/dist/utils/resourceLimits.d.ts +61 -2
- package/backend/dist/utils/resourceLimits.d.ts.map +1 -1
- package/backend/dist/utils/resourceLimits.js +59 -0
- package/backend/dist/utils/resourceLimits.js.map +1 -1
- package/backend/dist/utils/toolEgress.d.ts +22 -0
- package/backend/dist/utils/toolEgress.d.ts.map +1 -0
- package/backend/dist/utils/toolEgress.js +157 -0
- package/backend/dist/utils/toolEgress.js.map +1 -0
- package/backend/dist/utils/websocketMessages.d.ts +6 -0
- package/backend/dist/utils/websocketMessages.d.ts.map +1 -1
- package/backend/dist/utils/websocketMessages.js +8 -0
- package/backend/dist/utils/websocketMessages.js.map +1 -1
- package/backend/dist/utils/zipArchive.d.ts +32 -0
- package/backend/dist/utils/zipArchive.d.ts.map +1 -0
- package/backend/dist/utils/zipArchive.js +171 -0
- package/backend/dist/utils/zipArchive.js.map +1 -0
- package/backend/dist/websocketServer.d.ts.map +1 -1
- package/backend/dist/websocketServer.js +142 -35
- package/backend/dist/websocketServer.js.map +1 -1
- package/backend/package.json +1 -1
- package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
- package/frontend/dist/assets/index-BPsH9FII.js +80 -0
- package/frontend/dist/assets/index-BtXF0Oxa.css +1 -0
- package/frontend/dist/index.html +18 -18
- package/frontend/dist/js/{ArtifactContainer-CpUi6ekF.js → ArtifactContainer-CIoFoMv8.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-BeNs3gHp.js → ArtifactDemoPage-D0JEIAbZ.js} +1 -1
- package/frontend/dist/js/{ArtifactSandboxFrame-qsj_Y3LV.js → ArtifactSandboxFrame-REazyIrx.js} +2 -2
- package/frontend/dist/js/ArtifactSlideOutPanel-A-i94ghF.js +1 -0
- package/frontend/dist/js/AutomationsPage-Dow_lbIf.js +1 -0
- package/frontend/dist/js/{Button-BPf_pOxH.js → Button-C0r3J9G_.js} +1 -1
- package/frontend/dist/js/CalendarPage-DLjasZOd.js +1 -0
- package/frontend/dist/js/ChannelsPage-C8hvTD7J.js +1 -0
- package/frontend/dist/js/ChatPage-BAgBHUES.js +16 -0
- package/frontend/dist/js/EvaluationsPage-Bm8FRoeX.js +2 -0
- package/frontend/dist/js/{FirstTimeSetup-Df0YJni3.js → FirstTimeSetup-BxJapNul.js} +1 -1
- package/frontend/dist/js/GalleryPage-DHz9DkK7.js +1 -0
- package/frontend/dist/js/HomePage-BqNy-FWX.js +1 -0
- package/frontend/dist/js/LibreClawPage-BDzDILjY.js +1 -0
- package/frontend/dist/js/{ModelManagerModals-DcLGYN3l.js → ModelManagerModals-DX9eT5r2.js} +1 -1
- package/frontend/dist/js/ModelSelector-ol8mK2ja.js +1 -0
- package/frontend/dist/js/NotesPage-DzHEx2O4.js +3 -0
- package/frontend/dist/js/{OptimizedSyntaxHighlighter-Dz7D1LPB.js → OptimizedSyntaxHighlighter-CZNPwN0i.js} +1 -1
- package/frontend/dist/js/PersonasPage-M_WKH2a9.js +13 -0
- package/frontend/dist/js/PluginUsagePage-Ctoka6JZ.js +1 -0
- package/frontend/dist/js/RichMessageContent-BqgafH4p.js +3 -0
- package/frontend/dist/js/SettingsModal-R9EDadAV.js +19 -0
- package/frontend/dist/js/{SettingsToggle-woG_WebF.js → SettingsToggle-BIVyfGjl.js} +1 -1
- package/frontend/dist/js/SystemPage-aOkrSuY7.js +1 -0
- package/frontend/dist/js/UserManagementPage-BOuaX3ay.js +1 -0
- package/frontend/dist/js/WorkPage-DxZISerQ.js +6 -0
- package/frontend/dist/js/api-6bSMtSNh.js +14 -0
- package/frontend/dist/js/{appStore-0JnCUlEq.js → appStore-CdObhQBb.js} +2 -2
- package/frontend/dist/js/ar-DT_VLBO3.js +2 -0
- package/frontend/dist/js/{authStore-DbMdvNgJ.js → authStore-DbqqrfZv.js} +3 -3
- package/frontend/dist/js/{babel-BsYoxD-7.js → babel-DSqqk5kt.js} +1 -1
- package/frontend/dist/js/bn-BrvEqhGA.js +2 -0
- package/frontend/dist/js/chatStore-DbZPJNsu.js +3 -0
- package/frontend/dist/js/{config-DSOYQ9a3.js → config-8KdgjtEx.js} +1 -1
- package/frontend/dist/js/cs-BthnM_YI.js +2 -0
- package/frontend/dist/js/da-DCth-6_j.js +2 -0
- package/frontend/dist/js/de-BBWt0gJ_.js +2 -0
- package/frontend/dist/js/es-DuslOPqM.js +2 -0
- package/frontend/dist/js/{estree-BcnBcOAx.js → estree-BU5-LV_1.js} +3 -3
- package/frontend/dist/js/evaluationsApi-CwZJsgeG.js +1 -0
- package/frontend/dist/js/fr-DZAeg46S.js +2 -0
- package/frontend/dist/js/hi-ijDSiY77.js +2 -0
- package/frontend/dist/js/{html-DkY55Jys.js → html-DeoAy2bo.js} +2 -2
- package/frontend/dist/js/id-BS6qS0oq.js +2 -0
- package/frontend/dist/js/is-CO2PMDte.js +2 -0
- package/frontend/dist/js/it-ChhxT4hs.js +2 -0
- package/frontend/dist/js/ja-cyeqUeJN.js +2 -0
- package/frontend/dist/js/ko-CfqHTLXj.js +2 -0
- package/frontend/dist/js/{markdown-IZSI9DXw.js → markdown-DOLsYkBY.js} +3 -3
- package/frontend/dist/js/markdown-core-BK7WVOb7.js +32 -0
- package/frontend/dist/js/{markdown-math-DK846gXU.js → markdown-math-e1ByS3KY.js} +5 -5
- package/frontend/dist/js/{messageCodeStyles-BFNR9Wv6.js → messageCodeStyles-DhVgizF-.js} +2 -2
- package/frontend/dist/js/ms-DZ5pACe1.js +2 -0
- package/frontend/dist/js/nl-BsRsnOVp.js +2 -0
- package/frontend/dist/js/pl-DXSTAW4M.js +2 -0
- package/frontend/dist/js/{postcss-BJ0okz7n.js → postcss-Bg15EcFE.js} +1 -1
- package/frontend/dist/js/pt-DmDx0JxB.js +2 -0
- package/frontend/dist/js/{react-vendor-C5yShNlM.js → react-vendor-BG_HPV6X.js} +1 -1
- package/frontend/dist/js/router-vendor-CbexJIsv.js +1 -0
- package/frontend/dist/js/ru-d2g8EWfY.js +2 -0
- package/frontend/dist/js/{standalone-Csw3jfJ_.js → standalone-BO5pzI8_.js} +3 -3
- package/frontend/dist/js/sv-CIhYUTVQ.js +2 -0
- package/frontend/dist/js/{syntax-highlight-3EEX6CGL.js → syntax-highlight-DcR9Vhb7.js} +3 -3
- package/frontend/dist/js/th-B3wSxanB.js +2 -0
- package/frontend/dist/js/tr-R4UGgPOY.js +2 -0
- package/frontend/dist/js/{typescript-CnF4UVx7.js → typescript-6GJ8APmn.js} +1 -1
- package/frontend/dist/js/ui-CV0LYmkM.js +2 -0
- package/frontend/dist/js/ui-vendor-CWjIBW-z.js +177 -0
- package/frontend/dist/js/uk-BzUF4Vkt.js +2 -0
- package/frontend/dist/js/{useTranslation-Bho4HCyg.js → useTranslation-5b5UhAc9.js} +1 -1
- package/frontend/dist/js/{utils-D9W1Lnkn.js → utils-BGrdX9ie.js} +1 -1
- package/frontend/dist/js/{utils-vendor-DWh39_3x.js → utils-vendor-9bQ62WeD.js} +1 -1
- package/frontend/dist/js/vi-CKc6Fg4h.js +2 -0
- package/frontend/dist/js/{workApi-C3uy9EDT.js → workApi-BruWTlHM.js} +3 -3
- package/frontend/dist/js/{yaml-DzT5hBSH.js → yaml-DUeWcOOH.js} +2 -2
- package/frontend/dist/js/zh-CXL-kDe8.js +2 -0
- package/package.json +2 -2
- package/plugins/openai.json +52 -9
- package/frontend/dist/assets/index-6o-Hc2wS.js +0 -41
- package/frontend/dist/assets/index-D-pM957b.css +0 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-CSFRluWA.js +0 -1
- package/frontend/dist/js/AutomationsPage-KUeWoZNv.js +0 -1
- package/frontend/dist/js/CalendarPage-D9I972w8.js +0 -1
- package/frontend/dist/js/ChatPage-CmBx4oAL.js +0 -16
- package/frontend/dist/js/GalleryPage-bLCrdJN_.js +0 -1
- package/frontend/dist/js/HomePage-Uc6PEg-U.js +0 -1
- package/frontend/dist/js/LibreClawPage-CG63DHg1.js +0 -1
- package/frontend/dist/js/ModelSelector-yxJC64rM.js +0 -1
- package/frontend/dist/js/ModelsPage-1R_TxgmJ.js +0 -2
- package/frontend/dist/js/NotesPage-L65j5HoW.js +0 -1
- package/frontend/dist/js/PersonasPage-CKeBpHIH.js +0 -13
- package/frontend/dist/js/PluginUsagePage-BX1SKMbW.js +0 -1
- package/frontend/dist/js/RichMessageContent-DiySVYGB.js +0 -3
- package/frontend/dist/js/SettingsModal-68SG1uZ2.js +0 -1
- package/frontend/dist/js/SystemPage-CytpZiQV.js +0 -1
- package/frontend/dist/js/UserManagementPage-BzJBvZiS.js +0 -1
- package/frontend/dist/js/WorkPage-C8gr0QJe.js +0 -6
- package/frontend/dist/js/api-BDtqBtqw.js +0 -14
- package/frontend/dist/js/ar-CSpi37Gr.js +0 -2
- package/frontend/dist/js/bn-CyH1BeDD.js +0 -2
- package/frontend/dist/js/chatStore-CHQ2uINS.js +0 -3
- package/frontend/dist/js/cs-Cv2NfRK5.js +0 -2
- package/frontend/dist/js/da-D6D6IvPK.js +0 -2
- package/frontend/dist/js/de-BNoJJTrT.js +0 -2
- package/frontend/dist/js/es-kzo9C7q_.js +0 -2
- package/frontend/dist/js/fr-CjHYvJRp.js +0 -2
- package/frontend/dist/js/hi-BU98AdCQ.js +0 -2
- package/frontend/dist/js/id-CpdedejX.js +0 -2
- package/frontend/dist/js/is-Cp5dDWBH.js +0 -2
- package/frontend/dist/js/it-CuH_cK4U.js +0 -2
- package/frontend/dist/js/ja-BWXXXiHn.js +0 -2
- package/frontend/dist/js/ko-DQgv2M1t.js +0 -2
- package/frontend/dist/js/markdown-core-BmPPZiuD.js +0 -31
- package/frontend/dist/js/ms-DCcRoIcX.js +0 -2
- package/frontend/dist/js/nl-CbKR1_EU.js +0 -2
- package/frontend/dist/js/pl-CRyJZccn.js +0 -2
- package/frontend/dist/js/pt-BXYmHFGH.js +0 -2
- package/frontend/dist/js/router-vendor-DGFGM3EP.js +0 -1
- package/frontend/dist/js/ru-M9SyZG4b.js +0 -2
- package/frontend/dist/js/sv-DJ486LTU.js +0 -2
- package/frontend/dist/js/th-Ch7gKtZv.js +0 -2
- package/frontend/dist/js/tr-ClehWhSC.js +0 -2
- package/frontend/dist/js/ui-CC-K19wO.js +0 -2
- package/frontend/dist/js/ui-vendor-B1UgO3vd.js +0 -177
- package/frontend/dist/js/uk-BqAd5zHF.js +0 -2
- package/frontend/dist/js/vi-D9gjlDVB.js +0 -2
- package/frontend/dist/js/zh-DKTyHkiR.js +0 -2
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Channels (CHANNEL-01/02): public, private, and direct-message
|
|
19
|
+
* conversations with a persistent, idempotent, ordered timeline plus
|
|
20
|
+
* threads, reactions, pins, and per-member unread cursors.
|
|
21
|
+
*
|
|
22
|
+
* SQL is authoritative for every row; the durable event ledger only fans
|
|
23
|
+
* the same facts out to live subscribers (stream `channel:<id>`), so a
|
|
24
|
+
* missed event is recovered by reading the timeline, never replayed into
|
|
25
|
+
* divergence. Message and channel text is encrypted at rest with the same
|
|
26
|
+
* boundary as chat messages. Deletion is a tombstone: content is cleared
|
|
27
|
+
* but the timeline entry survives so threads never dangle.
|
|
28
|
+
*/
|
|
29
|
+
import { randomUUID } from 'node:crypto';
|
|
30
|
+
import { Readable } from 'node:stream';
|
|
31
|
+
import { getPersistence } from '../persistence/index.js';
|
|
32
|
+
import { PersistenceResourceLimitError, } from '../persistence/resourceTypes.js';
|
|
33
|
+
import { encryptionService } from './encryptionService.js';
|
|
34
|
+
import { userModel } from '../models/userModel.js';
|
|
35
|
+
import { getDurableEventGateway } from '../platform/events/index.js';
|
|
36
|
+
import { channelEventStreamId, CHANNEL_MENTION_IDEMPOTENCY_SCOPE, CHANNEL_MENTION_JOB_TYPE, } from '../platform/jobs/domainJobContracts.js';
|
|
37
|
+
import { getDurableJobRuntime } from '../platform/jobs/durableJobRuntime.js';
|
|
38
|
+
import { getCoordinator } from '../platform/coordination/service.js';
|
|
39
|
+
import { getPlatformStorageRuntime } from '../platform/storage/index.js';
|
|
40
|
+
import { createLogger } from '../utils/logger.js';
|
|
41
|
+
import { MAX_CHANNEL_ATTACHMENT_BYTES, MAX_CHANNEL_ATTACHMENTS_PER_MESSAGE, MAX_CHANNEL_MENTION_CONTEXT_MESSAGES, MAX_CHANNEL_DESCRIPTION_LENGTH, MAX_CHANNEL_EMOJI_LENGTH, MAX_CHANNEL_MEMBERS, MAX_CHANNEL_MESSAGE_LENGTH, MAX_CHANNEL_MESSAGES, MAX_CHANNEL_NAME_LENGTH, MAX_CHANNEL_PAGE_SIZE, MAX_CHANNEL_PINNED_LISTED, MAX_CHANNEL_REACTIONS_PER_MESSAGE, MAX_CHANNEL_THREAD_PAGE_SIZE, MAX_CHANNELS_LISTED, MAX_CHANNELS_PER_USER, } from '../utils/resourceLimits.js';
|
|
42
|
+
const logger = createLogger('channels');
|
|
43
|
+
export class ChannelError extends Error {
|
|
44
|
+
constructor(message, statusCode) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.statusCode = statusCode;
|
|
47
|
+
this.name = 'ChannelError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const repositories = () => getPersistence(encryptionService).repositories.resources;
|
|
51
|
+
const channels = () => repositories().channels;
|
|
52
|
+
const messages = () => repositories().channelMessages;
|
|
53
|
+
const notFound = () => new ChannelError('Channel not found', 404);
|
|
54
|
+
export const CHANNEL_ATTACHMENT_BLOB_PURPOSE = 'channel-attachment';
|
|
55
|
+
const PENDING_ATTACHMENT_TTL_MS = 15 * 60 * 1000;
|
|
56
|
+
const pendingAttachmentKey = (userId, attachmentId) => `channel-upload:${userId}:${attachmentId}`;
|
|
57
|
+
const isChannelType = (value) => value === 'public' || value === 'private' || value === 'dm';
|
|
58
|
+
/** Deterministic identity for a DM pair, order-independent. */
|
|
59
|
+
export const dmKeyFor = (left, right) => ['dm', ...[left, right].sort()].join(':');
|
|
60
|
+
const decryptOptional = (value) => {
|
|
61
|
+
if (!value)
|
|
62
|
+
return undefined;
|
|
63
|
+
try {
|
|
64
|
+
return encryptionService.decrypt(value);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const mapChannel = (row) => ({
|
|
71
|
+
id: row.id,
|
|
72
|
+
type: row.type,
|
|
73
|
+
name: row.name ? (decryptOptional(row.name) ?? '') : '',
|
|
74
|
+
...(row.description ? { description: decryptOptional(row.description) } : {}),
|
|
75
|
+
createdBy: row.created_by,
|
|
76
|
+
createdAt: row.created_at,
|
|
77
|
+
updatedAt: row.updated_at,
|
|
78
|
+
...(row.archived_at ? { archivedAt: row.archived_at } : {}),
|
|
79
|
+
});
|
|
80
|
+
const readMetadata = (row) => {
|
|
81
|
+
if (!row.metadata)
|
|
82
|
+
return {};
|
|
83
|
+
try {
|
|
84
|
+
const decoded = JSON.parse(encryptionService.decrypt(row.metadata));
|
|
85
|
+
return {
|
|
86
|
+
...(decoded.pending === true ? { pending: true } : {}),
|
|
87
|
+
...(typeof decoded.error === 'string' ? { error: decoded.error } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const mapMessage = (row, context) => {
|
|
95
|
+
const deleted = row.deleted_at !== null;
|
|
96
|
+
const metadata = deleted ? {} : readMetadata(row);
|
|
97
|
+
return {
|
|
98
|
+
id: row.id,
|
|
99
|
+
channelId: row.channel_id,
|
|
100
|
+
...(row.parent_id ? { parentId: row.parent_id } : {}),
|
|
101
|
+
authorKind: row.author_kind === 'model' ? 'model' : 'user',
|
|
102
|
+
...(row.model ? { model: row.model } : {}),
|
|
103
|
+
author: row.user_id
|
|
104
|
+
? {
|
|
105
|
+
userId: row.user_id,
|
|
106
|
+
username: context.usernames.get(row.user_id) ?? row.user_id,
|
|
107
|
+
}
|
|
108
|
+
: null,
|
|
109
|
+
content: deleted ? '' : (decryptOptional(row.content) ?? ''),
|
|
110
|
+
createdAt: row.created_at,
|
|
111
|
+
updatedAt: row.updated_at,
|
|
112
|
+
...(row.edited_at ? { editedAt: row.edited_at } : {}),
|
|
113
|
+
...(deleted ? { deleted: true } : {}),
|
|
114
|
+
...(row.pinned_at ? { pinnedAt: row.pinned_at } : {}),
|
|
115
|
+
...(context.replyCounts[row.id]
|
|
116
|
+
? { replyCount: context.replyCounts[row.id] }
|
|
117
|
+
: {}),
|
|
118
|
+
...(context.reactions.has(row.id)
|
|
119
|
+
? { reactions: context.reactions.get(row.id) }
|
|
120
|
+
: {}),
|
|
121
|
+
...(context.attachments.has(row.id)
|
|
122
|
+
? { attachments: context.attachments.get(row.id) }
|
|
123
|
+
: {}),
|
|
124
|
+
...metadata,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const resolveUsernames = async (userIds) => {
|
|
128
|
+
const names = new Map();
|
|
129
|
+
for (const userId of new Set(userIds)) {
|
|
130
|
+
try {
|
|
131
|
+
const user = await userModel.getUserById(userId);
|
|
132
|
+
if (user)
|
|
133
|
+
names.set(userId, user.username);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// A deleted account renders as its identifier.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return names;
|
|
140
|
+
};
|
|
141
|
+
/** Live fan-out is best-effort; SQL rows are the recovery path. */
|
|
142
|
+
const appendChannelEvent = async (channelId, eventType, subjectId, actorUserId, payload) => {
|
|
143
|
+
try {
|
|
144
|
+
await getDurableEventGateway().append({
|
|
145
|
+
eventId: randomUUID(),
|
|
146
|
+
streamId: channelEventStreamId(channelId),
|
|
147
|
+
eventType,
|
|
148
|
+
subjectId,
|
|
149
|
+
actorUserId,
|
|
150
|
+
payload: { mode: 'encrypted', value: payload },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
logger.warn('Channel event fan-out failed; timeline reads recover', {
|
|
155
|
+
channelId,
|
|
156
|
+
eventType,
|
|
157
|
+
error,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const buildViewContext = async (actorUserId, rows) => {
|
|
162
|
+
const messageIds = rows.map(row => row.id);
|
|
163
|
+
const rootIds = rows.filter(row => !row.parent_id).map(row => row.id);
|
|
164
|
+
const [replyCounts, reactionRows, attachmentRows] = await Promise.all([
|
|
165
|
+
rootIds.length > 0
|
|
166
|
+
? messages().countThreadReplies(rootIds)
|
|
167
|
+
: Promise.resolve({}),
|
|
168
|
+
messages().listReactionsForMessages(messageIds),
|
|
169
|
+
messages().listAttachmentsForMessages(messageIds),
|
|
170
|
+
]);
|
|
171
|
+
const reactions = new Map();
|
|
172
|
+
for (const reaction of reactionRows) {
|
|
173
|
+
const list = reactions.get(reaction.message_id) ?? [];
|
|
174
|
+
const existing = list.find(entry => entry.emoji === reaction.emoji);
|
|
175
|
+
if (existing) {
|
|
176
|
+
existing.count += 1;
|
|
177
|
+
if (reaction.user_id === actorUserId)
|
|
178
|
+
existing.mine = true;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
list.push({
|
|
182
|
+
emoji: reaction.emoji,
|
|
183
|
+
count: 1,
|
|
184
|
+
mine: reaction.user_id === actorUserId,
|
|
185
|
+
});
|
|
186
|
+
reactions.set(reaction.message_id, list);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const attachments = new Map();
|
|
190
|
+
for (const attachment of attachmentRows) {
|
|
191
|
+
const list = attachments.get(attachment.message_id) ?? [];
|
|
192
|
+
list.push({
|
|
193
|
+
id: attachment.id,
|
|
194
|
+
filename: attachment.filename,
|
|
195
|
+
contentType: attachment.content_type,
|
|
196
|
+
size: attachment.size,
|
|
197
|
+
});
|
|
198
|
+
attachments.set(attachment.message_id, list);
|
|
199
|
+
}
|
|
200
|
+
const usernames = await resolveUsernames(rows.flatMap(row => (row.user_id ? [row.user_id] : [])));
|
|
201
|
+
return { actorUserId, usernames, replyCounts, reactions, attachments };
|
|
202
|
+
};
|
|
203
|
+
const mapRows = async (actorUserId, rows) => {
|
|
204
|
+
const context = await buildViewContext(actorUserId, rows);
|
|
205
|
+
return rows.map(row => mapMessage(row, context));
|
|
206
|
+
};
|
|
207
|
+
export class ChannelService {
|
|
208
|
+
/** Membership gate used by every channel read and write. */
|
|
209
|
+
async requireMember(channelId, userId) {
|
|
210
|
+
const channel = await channels().findById(channelId);
|
|
211
|
+
if (!channel)
|
|
212
|
+
throw notFound();
|
|
213
|
+
const member = await channels().findMember(channelId, userId);
|
|
214
|
+
if (!member)
|
|
215
|
+
throw notFound();
|
|
216
|
+
return { channel, member };
|
|
217
|
+
}
|
|
218
|
+
async createChannel(actor, input) {
|
|
219
|
+
if (!isChannelType(input.type) || input.type === 'dm') {
|
|
220
|
+
throw new ChannelError('Channel type must be public or private', 400);
|
|
221
|
+
}
|
|
222
|
+
const name = (input.name ?? '').trim();
|
|
223
|
+
if (!name || name.length > MAX_CHANNEL_NAME_LENGTH) {
|
|
224
|
+
throw new ChannelError('Channel name is required', 400);
|
|
225
|
+
}
|
|
226
|
+
if ((input.description?.length ?? 0) > MAX_CHANNEL_DESCRIPTION_LENGTH) {
|
|
227
|
+
throw new ChannelError('Channel description is too long', 400);
|
|
228
|
+
}
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const channel = {
|
|
231
|
+
id: randomUUID(),
|
|
232
|
+
type: input.type,
|
|
233
|
+
name: encryptionService.encrypt(name),
|
|
234
|
+
description: input.description
|
|
235
|
+
? encryptionService.encrypt(input.description)
|
|
236
|
+
: null,
|
|
237
|
+
dm_key: null,
|
|
238
|
+
created_by: actor.userId,
|
|
239
|
+
created_at: now,
|
|
240
|
+
updated_at: now,
|
|
241
|
+
archived_at: null,
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
await channels().insertWithOwner(channel, {
|
|
245
|
+
channel_id: channel.id,
|
|
246
|
+
user_id: actor.userId,
|
|
247
|
+
role: 'owner',
|
|
248
|
+
joined_at: now,
|
|
249
|
+
last_read_at: now,
|
|
250
|
+
}, MAX_CHANNELS_PER_USER);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof PersistenceResourceLimitError) {
|
|
254
|
+
throw new ChannelError(`A user may create at most ${MAX_CHANNELS_PER_USER} channels`, 409);
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
for (const memberId of new Set(input.memberIds ?? [])) {
|
|
259
|
+
if (memberId === actor.userId)
|
|
260
|
+
continue;
|
|
261
|
+
await this.addMember({ userId: actor.userId }, channel.id, memberId);
|
|
262
|
+
}
|
|
263
|
+
return mapChannel(channel);
|
|
264
|
+
}
|
|
265
|
+
/** Opens (or returns) the deterministic DM channel between two users. */
|
|
266
|
+
async openDm(actor, peerUserId) {
|
|
267
|
+
if (!peerUserId || peerUserId === actor.userId) {
|
|
268
|
+
throw new ChannelError('A direct message needs another user', 400);
|
|
269
|
+
}
|
|
270
|
+
const peer = await userModel.getUserById(peerUserId);
|
|
271
|
+
if (!peer)
|
|
272
|
+
throw new ChannelError('User not found', 404);
|
|
273
|
+
const dmKey = dmKeyFor(actor.userId, peerUserId);
|
|
274
|
+
const existing = await channels().findByDmKey(dmKey);
|
|
275
|
+
if (existing) {
|
|
276
|
+
// Re-attach either participant who previously left.
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
for (const userId of [actor.userId, peerUserId]) {
|
|
279
|
+
if (!(await channels().findMember(existing.id, userId))) {
|
|
280
|
+
await channels().upsertMember({
|
|
281
|
+
channel_id: existing.id,
|
|
282
|
+
user_id: userId,
|
|
283
|
+
role: 'member',
|
|
284
|
+
joined_at: now,
|
|
285
|
+
last_read_at: userId === actor.userId ? now : 0,
|
|
286
|
+
}, MAX_CHANNEL_MEMBERS);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return mapChannel(existing);
|
|
290
|
+
}
|
|
291
|
+
const now = Date.now();
|
|
292
|
+
const channel = {
|
|
293
|
+
id: randomUUID(),
|
|
294
|
+
type: 'dm',
|
|
295
|
+
name: encryptionService.encrypt(''),
|
|
296
|
+
description: null,
|
|
297
|
+
dm_key: dmKey,
|
|
298
|
+
created_by: actor.userId,
|
|
299
|
+
created_at: now,
|
|
300
|
+
updated_at: now,
|
|
301
|
+
archived_at: null,
|
|
302
|
+
};
|
|
303
|
+
await channels().insertWithOwner(channel, {
|
|
304
|
+
channel_id: channel.id,
|
|
305
|
+
user_id: actor.userId,
|
|
306
|
+
role: 'member',
|
|
307
|
+
joined_at: now,
|
|
308
|
+
last_read_at: now,
|
|
309
|
+
}, MAX_CHANNELS_PER_USER);
|
|
310
|
+
await channels().upsertMember({
|
|
311
|
+
channel_id: channel.id,
|
|
312
|
+
user_id: peerUserId,
|
|
313
|
+
role: 'member',
|
|
314
|
+
joined_at: now,
|
|
315
|
+
last_read_at: 0,
|
|
316
|
+
}, MAX_CHANNEL_MEMBERS);
|
|
317
|
+
return mapChannel(channel);
|
|
318
|
+
}
|
|
319
|
+
/** The actor's channels with unread counts, newest activity first. */
|
|
320
|
+
async listMine(actor) {
|
|
321
|
+
const memberships = await channels().listForUser(actor.userId, MAX_CHANNELS_LISTED);
|
|
322
|
+
const unread = await channels().unreadSummaryForUser(actor.userId);
|
|
323
|
+
const unreadByChannel = new Map(unread.map(row => [row.channel_id, row]));
|
|
324
|
+
const summaries = [];
|
|
325
|
+
for (const { channel, member } of memberships) {
|
|
326
|
+
const summary = {
|
|
327
|
+
...mapChannel(channel),
|
|
328
|
+
role: member.role,
|
|
329
|
+
isMember: true,
|
|
330
|
+
lastReadAt: member.last_read_at,
|
|
331
|
+
unreadCount: unreadByChannel.get(channel.id)?.unread_count ?? 0,
|
|
332
|
+
latestMessageAt: unreadByChannel.get(channel.id)?.latest_message_at ?? null,
|
|
333
|
+
};
|
|
334
|
+
if (channel.type === 'dm') {
|
|
335
|
+
const members = await channels().listMembers(channel.id, 4);
|
|
336
|
+
const peer = members.find(entry => entry.user_id !== actor.userId);
|
|
337
|
+
if (peer) {
|
|
338
|
+
const names = await resolveUsernames([peer.user_id]);
|
|
339
|
+
summary.dmPeer = {
|
|
340
|
+
userId: peer.user_id,
|
|
341
|
+
username: names.get(peer.user_id) ?? peer.user_id,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
summaries.push(summary);
|
|
346
|
+
}
|
|
347
|
+
return summaries;
|
|
348
|
+
}
|
|
349
|
+
/** Browseable public channels with membership state. */
|
|
350
|
+
async listPublic(actor) {
|
|
351
|
+
const rows = await channels().listPublic(MAX_CHANNELS_LISTED);
|
|
352
|
+
const summaries = [];
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
const member = await channels().findMember(row.id, actor.userId);
|
|
355
|
+
summaries.push({
|
|
356
|
+
...mapChannel(row),
|
|
357
|
+
isMember: Boolean(member),
|
|
358
|
+
...(member ? { role: member.role } : {}),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return summaries;
|
|
362
|
+
}
|
|
363
|
+
async getChannel(actor, channelId) {
|
|
364
|
+
const channel = await channels().findById(channelId);
|
|
365
|
+
if (!channel)
|
|
366
|
+
throw notFound();
|
|
367
|
+
const member = await channels().findMember(channelId, actor.userId);
|
|
368
|
+
if (!member && channel.type !== 'public')
|
|
369
|
+
throw notFound();
|
|
370
|
+
const summary = {
|
|
371
|
+
...mapChannel(channel),
|
|
372
|
+
isMember: Boolean(member),
|
|
373
|
+
...(member ? { role: member.role } : {}),
|
|
374
|
+
...(member ? { lastReadAt: member.last_read_at } : {}),
|
|
375
|
+
};
|
|
376
|
+
if (channel.type === 'dm' && member) {
|
|
377
|
+
const members = await channels().listMembers(channel.id, 4);
|
|
378
|
+
const peer = members.find(entry => entry.user_id !== actor.userId);
|
|
379
|
+
if (peer) {
|
|
380
|
+
const names = await resolveUsernames([peer.user_id]);
|
|
381
|
+
summary.dmPeer = {
|
|
382
|
+
userId: peer.user_id,
|
|
383
|
+
username: names.get(peer.user_id) ?? peer.user_id,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return summary;
|
|
388
|
+
}
|
|
389
|
+
async updateChannel(actor, channelId, updates) {
|
|
390
|
+
const { channel, member } = await this.requireMember(channelId, actor.userId);
|
|
391
|
+
if (member.role !== 'owner')
|
|
392
|
+
throw notFound();
|
|
393
|
+
if (channel.type === 'dm') {
|
|
394
|
+
throw new ChannelError('Direct messages cannot be renamed', 400);
|
|
395
|
+
}
|
|
396
|
+
const next = { ...channel, updated_at: Date.now() };
|
|
397
|
+
if (updates.name !== undefined) {
|
|
398
|
+
const name = updates.name.trim();
|
|
399
|
+
if (!name || name.length > MAX_CHANNEL_NAME_LENGTH) {
|
|
400
|
+
throw new ChannelError('Channel name is required', 400);
|
|
401
|
+
}
|
|
402
|
+
next.name = encryptionService.encrypt(name);
|
|
403
|
+
}
|
|
404
|
+
if (updates.description !== undefined) {
|
|
405
|
+
if (updates.description.length > MAX_CHANNEL_DESCRIPTION_LENGTH) {
|
|
406
|
+
throw new ChannelError('Channel description is too long', 400);
|
|
407
|
+
}
|
|
408
|
+
next.description = updates.description
|
|
409
|
+
? encryptionService.encrypt(updates.description)
|
|
410
|
+
: null;
|
|
411
|
+
}
|
|
412
|
+
if (updates.archived !== undefined) {
|
|
413
|
+
next.archived_at = updates.archived ? Date.now() : null;
|
|
414
|
+
}
|
|
415
|
+
await channels().update(next);
|
|
416
|
+
await appendChannelEvent(channelId, 'channel.updated.v1', channelId, actor.userId, {
|
|
417
|
+
type: 'channel_updated',
|
|
418
|
+
channelId,
|
|
419
|
+
});
|
|
420
|
+
return mapChannel(next);
|
|
421
|
+
}
|
|
422
|
+
async deleteChannel(actor, channelId) {
|
|
423
|
+
const { channel, member } = await this.requireMember(channelId, actor.userId);
|
|
424
|
+
if (channel.type !== 'dm' && member.role !== 'owner')
|
|
425
|
+
throw notFound();
|
|
426
|
+
const blobs = await messages().listAttachmentBlobIds(channelId);
|
|
427
|
+
await channels().delete(channelId);
|
|
428
|
+
if (blobs.length > 0) {
|
|
429
|
+
try {
|
|
430
|
+
const { getPlatformStorageRuntime } = await import('../platform/storage/index.js');
|
|
431
|
+
const blobStore = getPlatformStorageRuntime().blobStore;
|
|
432
|
+
for (const blob of blobs) {
|
|
433
|
+
if (!blob.created_by)
|
|
434
|
+
continue;
|
|
435
|
+
await blobStore
|
|
436
|
+
.delete({ id: blob.blob_id, ownerUserId: blob.created_by })
|
|
437
|
+
.catch(() => undefined);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
logger.warn('Channel attachment blob cleanup failed', { error });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async addMember(actor, channelId, userId) {
|
|
446
|
+
const { channel, member } = await this.requireMember(channelId, actor.userId);
|
|
447
|
+
if (channel.type === 'dm') {
|
|
448
|
+
throw new ChannelError('Direct messages are limited to two people', 400);
|
|
449
|
+
}
|
|
450
|
+
if (channel.type === 'private' && member.role !== 'owner') {
|
|
451
|
+
throw notFound();
|
|
452
|
+
}
|
|
453
|
+
const user = await userModel.getUserById(userId);
|
|
454
|
+
if (!user)
|
|
455
|
+
throw new ChannelError('User not found', 404);
|
|
456
|
+
try {
|
|
457
|
+
await channels().upsertMember({
|
|
458
|
+
channel_id: channelId,
|
|
459
|
+
user_id: userId,
|
|
460
|
+
role: 'member',
|
|
461
|
+
joined_at: Date.now(),
|
|
462
|
+
last_read_at: 0,
|
|
463
|
+
}, MAX_CHANNEL_MEMBERS);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (error instanceof PersistenceResourceLimitError) {
|
|
467
|
+
throw new ChannelError(`A channel may have at most ${MAX_CHANNEL_MEMBERS} members`, 409);
|
|
468
|
+
}
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
await appendChannelEvent(channelId, 'channel.member.v1', userId, actor.userId, {
|
|
472
|
+
type: 'member_added',
|
|
473
|
+
channelId,
|
|
474
|
+
userId,
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
userId,
|
|
478
|
+
username: user.username,
|
|
479
|
+
role: 'member',
|
|
480
|
+
joinedAt: Date.now(),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/** Join a public channel. */
|
|
484
|
+
async join(actor, channelId) {
|
|
485
|
+
const channel = await channels().findById(channelId);
|
|
486
|
+
if (!channel || channel.type !== 'public' || channel.archived_at) {
|
|
487
|
+
throw notFound();
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
await channels().upsertMember({
|
|
491
|
+
channel_id: channelId,
|
|
492
|
+
user_id: actor.userId,
|
|
493
|
+
role: 'member',
|
|
494
|
+
joined_at: Date.now(),
|
|
495
|
+
last_read_at: Date.now(),
|
|
496
|
+
}, MAX_CHANNEL_MEMBERS);
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
if (error instanceof PersistenceResourceLimitError) {
|
|
500
|
+
throw new ChannelError(`A channel may have at most ${MAX_CHANNEL_MEMBERS} members`, 409);
|
|
501
|
+
}
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
await appendChannelEvent(channelId, 'channel.member.v1', actor.userId, actor.userId, { type: 'member_added', channelId, userId: actor.userId });
|
|
505
|
+
}
|
|
506
|
+
async removeMember(actor, channelId, userId) {
|
|
507
|
+
const { channel, member } = await this.requireMember(channelId, actor.userId);
|
|
508
|
+
const leaving = userId === actor.userId;
|
|
509
|
+
if (!leaving && member.role !== 'owner')
|
|
510
|
+
throw notFound();
|
|
511
|
+
if (channel.type === 'dm' && !leaving) {
|
|
512
|
+
throw new ChannelError('Direct-message members cannot be removed', 400);
|
|
513
|
+
}
|
|
514
|
+
if (leaving && member.role === 'owner') {
|
|
515
|
+
throw new ChannelError('The channel owner must delete the channel instead of leaving it', 400);
|
|
516
|
+
}
|
|
517
|
+
const removed = await channels().removeMember(channelId, userId);
|
|
518
|
+
if (!removed)
|
|
519
|
+
throw notFound();
|
|
520
|
+
await appendChannelEvent(channelId, 'channel.member.v1', userId, actor.userId, {
|
|
521
|
+
type: 'member_removed',
|
|
522
|
+
channelId,
|
|
523
|
+
userId,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
async listMembers(actor, channelId) {
|
|
527
|
+
await this.requireMember(channelId, actor.userId);
|
|
528
|
+
const rows = await channels().listMembers(channelId, MAX_CHANNEL_MEMBERS);
|
|
529
|
+
const usernames = await resolveUsernames(rows.map(row => row.user_id));
|
|
530
|
+
return rows.map(row => ({
|
|
531
|
+
userId: row.user_id,
|
|
532
|
+
username: usernames.get(row.user_id) ?? row.user_id,
|
|
533
|
+
role: row.role,
|
|
534
|
+
joinedAt: row.joined_at,
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Idempotent post: the client supplies the message id, so a retried
|
|
539
|
+
* request lands on the same timeline entry and fans out exactly once.
|
|
540
|
+
*/
|
|
541
|
+
async postMessage(actor, channelId, input) {
|
|
542
|
+
const { channel } = await this.requireMember(channelId, actor.userId);
|
|
543
|
+
if (channel.archived_at) {
|
|
544
|
+
throw new ChannelError('This channel is archived', 409);
|
|
545
|
+
}
|
|
546
|
+
const content = input.content ?? '';
|
|
547
|
+
// Pending model replies legitimately start empty; user posts need
|
|
548
|
+
// either text or an attachment.
|
|
549
|
+
if (!content.trim() &&
|
|
550
|
+
(input.attachments?.length ?? 0) === 0 &&
|
|
551
|
+
input.authorKind !== 'model') {
|
|
552
|
+
throw new ChannelError('Message content is required', 400);
|
|
553
|
+
}
|
|
554
|
+
if (content.length > MAX_CHANNEL_MESSAGE_LENGTH) {
|
|
555
|
+
throw new ChannelError('Message content is too long', 400);
|
|
556
|
+
}
|
|
557
|
+
if (input.parentId) {
|
|
558
|
+
const parent = await messages().findById(input.parentId);
|
|
559
|
+
if (!parent || parent.channel_id !== channelId)
|
|
560
|
+
throw notFound();
|
|
561
|
+
if (parent.parent_id) {
|
|
562
|
+
throw new ChannelError('Threads are one level deep', 400);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const now = Date.now();
|
|
566
|
+
const record = {
|
|
567
|
+
id: input.id ?? randomUUID(),
|
|
568
|
+
channel_id: channelId,
|
|
569
|
+
user_id: actor.userId,
|
|
570
|
+
parent_id: input.parentId ?? null,
|
|
571
|
+
author_kind: input.authorKind === 'model' ? 'model' : 'user',
|
|
572
|
+
model: input.model ?? null,
|
|
573
|
+
content: encryptionService.encrypt(content),
|
|
574
|
+
metadata: input.metadata
|
|
575
|
+
? encryptionService.encrypt(JSON.stringify(input.metadata))
|
|
576
|
+
: null,
|
|
577
|
+
created_at: now,
|
|
578
|
+
updated_at: now,
|
|
579
|
+
edited_at: null,
|
|
580
|
+
deleted_at: null,
|
|
581
|
+
pinned_at: null,
|
|
582
|
+
pinned_by: null,
|
|
583
|
+
};
|
|
584
|
+
let stored;
|
|
585
|
+
let inserted;
|
|
586
|
+
try {
|
|
587
|
+
({ stored, inserted } = await messages().insertIfAbsent(record, input.attachments ?? [], MAX_CHANNEL_MESSAGES));
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
if (error instanceof PersistenceResourceLimitError) {
|
|
591
|
+
throw new ChannelError('This channel is full', 409);
|
|
592
|
+
}
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
if (inserted && stored.user_id === actor.userId) {
|
|
596
|
+
// The author has read their own message.
|
|
597
|
+
await channels().advanceLastRead(channelId, actor.userId, now);
|
|
598
|
+
}
|
|
599
|
+
const [view] = await mapRows(actor.userId, [stored]);
|
|
600
|
+
if (inserted) {
|
|
601
|
+
await appendChannelEvent(channelId, 'channel.message.v1', stored.id, actor.userId, { type: 'message_created', message: view });
|
|
602
|
+
}
|
|
603
|
+
return { message: view, created: inserted };
|
|
604
|
+
}
|
|
605
|
+
async editMessage(actor, messageId, content) {
|
|
606
|
+
const row = await messages().findById(messageId);
|
|
607
|
+
if (!row || row.deleted_at)
|
|
608
|
+
throw notFound();
|
|
609
|
+
await this.requireMember(row.channel_id, actor.userId);
|
|
610
|
+
if (row.user_id !== actor.userId || row.author_kind !== 'user') {
|
|
611
|
+
throw notFound();
|
|
612
|
+
}
|
|
613
|
+
if (!content.trim() || content.length > MAX_CHANNEL_MESSAGE_LENGTH) {
|
|
614
|
+
throw new ChannelError('Message content is required', 400);
|
|
615
|
+
}
|
|
616
|
+
const now = Date.now();
|
|
617
|
+
const next = {
|
|
618
|
+
...row,
|
|
619
|
+
content: encryptionService.encrypt(content),
|
|
620
|
+
updated_at: now,
|
|
621
|
+
edited_at: now,
|
|
622
|
+
};
|
|
623
|
+
await messages().update(next);
|
|
624
|
+
const [view] = await mapRows(actor.userId, [next]);
|
|
625
|
+
await appendChannelEvent(row.channel_id, 'channel.message.v1', messageId, actor.userId, { type: 'message_edited', message: view });
|
|
626
|
+
return view;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Tombstone deletion: the row survives with cleared content so replies
|
|
630
|
+
* and the ordered timeline stay intact. Attachments are removed.
|
|
631
|
+
*/
|
|
632
|
+
async deleteMessage(actor, messageId) {
|
|
633
|
+
const row = await messages().findById(messageId);
|
|
634
|
+
if (!row || row.deleted_at)
|
|
635
|
+
throw notFound();
|
|
636
|
+
const { member } = await this.requireMember(row.channel_id, actor.userId);
|
|
637
|
+
if (row.user_id !== actor.userId && member.role !== 'owner') {
|
|
638
|
+
throw notFound();
|
|
639
|
+
}
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
const next = {
|
|
642
|
+
...row,
|
|
643
|
+
content: encryptionService.encrypt(''),
|
|
644
|
+
metadata: null,
|
|
645
|
+
updated_at: now,
|
|
646
|
+
deleted_at: now,
|
|
647
|
+
pinned_at: null,
|
|
648
|
+
pinned_by: null,
|
|
649
|
+
};
|
|
650
|
+
await messages().update(next);
|
|
651
|
+
await appendChannelEvent(row.channel_id, 'channel.message.v1', messageId, actor.userId, { type: 'message_deleted', messageId, channelId: row.channel_id });
|
|
652
|
+
}
|
|
653
|
+
async setPinned(actor, messageId, pinned) {
|
|
654
|
+
const row = await messages().findById(messageId);
|
|
655
|
+
if (!row || row.deleted_at)
|
|
656
|
+
throw notFound();
|
|
657
|
+
await this.requireMember(row.channel_id, actor.userId);
|
|
658
|
+
const now = Date.now();
|
|
659
|
+
const next = {
|
|
660
|
+
...row,
|
|
661
|
+
pinned_at: pinned ? now : null,
|
|
662
|
+
pinned_by: pinned ? actor.userId : null,
|
|
663
|
+
updated_at: now,
|
|
664
|
+
};
|
|
665
|
+
await messages().update(next);
|
|
666
|
+
const [view] = await mapRows(actor.userId, [next]);
|
|
667
|
+
await appendChannelEvent(row.channel_id, 'channel.pin.v1', messageId, actor.userId, { type: pinned ? 'message_pinned' : 'message_unpinned', message: view });
|
|
668
|
+
return view;
|
|
669
|
+
}
|
|
670
|
+
async listPinned(actor, channelId) {
|
|
671
|
+
await this.requireMember(channelId, actor.userId);
|
|
672
|
+
const rows = await messages().listPinned(channelId, MAX_CHANNEL_PINNED_LISTED);
|
|
673
|
+
return mapRows(actor.userId, rows);
|
|
674
|
+
}
|
|
675
|
+
async react(actor, messageId, emoji, add) {
|
|
676
|
+
const normalized = (emoji ?? '').trim();
|
|
677
|
+
if (!normalized ||
|
|
678
|
+
normalized.length > MAX_CHANNEL_EMOJI_LENGTH ||
|
|
679
|
+
[...normalized].some(character => {
|
|
680
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
681
|
+
return codePoint <= 31 || codePoint === 127;
|
|
682
|
+
})) {
|
|
683
|
+
throw new ChannelError('Invalid reaction', 400);
|
|
684
|
+
}
|
|
685
|
+
const row = await messages().findById(messageId);
|
|
686
|
+
if (!row || row.deleted_at)
|
|
687
|
+
throw notFound();
|
|
688
|
+
await this.requireMember(row.channel_id, actor.userId);
|
|
689
|
+
let changed;
|
|
690
|
+
if (add) {
|
|
691
|
+
try {
|
|
692
|
+
changed = await messages().addReaction({
|
|
693
|
+
id: randomUUID(),
|
|
694
|
+
message_id: messageId,
|
|
695
|
+
user_id: actor.userId,
|
|
696
|
+
emoji: normalized,
|
|
697
|
+
created_at: Date.now(),
|
|
698
|
+
}, MAX_CHANNEL_REACTIONS_PER_MESSAGE);
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
if (error instanceof PersistenceResourceLimitError) {
|
|
702
|
+
throw new ChannelError('This message has too many reactions', 409);
|
|
703
|
+
}
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
changed = await messages().removeReaction(messageId, actor.userId, normalized);
|
|
709
|
+
}
|
|
710
|
+
if (changed) {
|
|
711
|
+
await appendChannelEvent(row.channel_id, 'channel.reaction.v1', messageId, actor.userId, {
|
|
712
|
+
type: add ? 'reaction_added' : 'reaction_removed',
|
|
713
|
+
messageId,
|
|
714
|
+
channelId: row.channel_id,
|
|
715
|
+
emoji: normalized,
|
|
716
|
+
userId: actor.userId,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
/** Root timeline page (newest-first before / oldest-first after). */
|
|
721
|
+
async listTimeline(actor, channelId, options) {
|
|
722
|
+
await this.requireMember(channelId, actor.userId);
|
|
723
|
+
const limit = Math.min(Math.max(options.limit ?? 50, 1), MAX_CHANNEL_PAGE_SIZE);
|
|
724
|
+
const rows = await messages().listPage(channelId, {
|
|
725
|
+
parentId: null,
|
|
726
|
+
...(options.before ? { before: options.before } : {}),
|
|
727
|
+
...(options.after ? { after: options.after } : {}),
|
|
728
|
+
limit,
|
|
729
|
+
});
|
|
730
|
+
// Deliver oldest-first regardless of the paging direction.
|
|
731
|
+
const ordered = options.after ? rows : [...rows].reverse();
|
|
732
|
+
return mapRows(actor.userId, ordered);
|
|
733
|
+
}
|
|
734
|
+
async listThread(actor, parentId) {
|
|
735
|
+
const parent = await messages().findById(parentId);
|
|
736
|
+
if (!parent)
|
|
737
|
+
throw notFound();
|
|
738
|
+
await this.requireMember(parent.channel_id, actor.userId);
|
|
739
|
+
const rows = await messages().listThread(parentId, MAX_CHANNEL_THREAD_PAGE_SIZE);
|
|
740
|
+
return mapRows(actor.userId, [parent, ...rows]);
|
|
741
|
+
}
|
|
742
|
+
/** Advance the read cursor; it never moves backwards. */
|
|
743
|
+
async markRead(actor, channelId, lastReadAt) {
|
|
744
|
+
await this.requireMember(channelId, actor.userId);
|
|
745
|
+
const at = lastReadAt !== undefined && Number.isSafeInteger(lastReadAt)
|
|
746
|
+
? lastReadAt
|
|
747
|
+
: Date.now();
|
|
748
|
+
await channels().advanceLastRead(channelId, actor.userId, at);
|
|
749
|
+
}
|
|
750
|
+
async findMessage(messageId) {
|
|
751
|
+
return messages().findById(messageId);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Stores an uploaded file as a blob owned by the uploader and parks its
|
|
755
|
+
* descriptor in the coordination cache until a message claims it. The
|
|
756
|
+
* cache is shared across replicas, so upload and post may hit different
|
|
757
|
+
* nodes.
|
|
758
|
+
*/
|
|
759
|
+
async uploadAttachment(actor, channelId, file) {
|
|
760
|
+
await this.requireMember(channelId, actor.userId);
|
|
761
|
+
if (file.buffer.length === 0) {
|
|
762
|
+
throw new ChannelError('The attachment is empty', 400);
|
|
763
|
+
}
|
|
764
|
+
if (file.buffer.length > MAX_CHANNEL_ATTACHMENT_BYTES) {
|
|
765
|
+
throw new ChannelError('The attachment exceeds the maximum size', 400);
|
|
766
|
+
}
|
|
767
|
+
const attachmentId = randomUUID();
|
|
768
|
+
const platform = getPlatformStorageRuntime();
|
|
769
|
+
const blob = await platform.blobStore.put({
|
|
770
|
+
ownerUserId: actor.userId,
|
|
771
|
+
purpose: CHANNEL_ATTACHMENT_BLOB_PURPOSE,
|
|
772
|
+
contentType: file.contentType,
|
|
773
|
+
originalFilename: file.filename,
|
|
774
|
+
expectedSize: file.buffer.length,
|
|
775
|
+
metadata: {
|
|
776
|
+
resourceType: 'channel-attachment',
|
|
777
|
+
resourceId: attachmentId,
|
|
778
|
+
},
|
|
779
|
+
source: Readable.from(file.buffer),
|
|
780
|
+
});
|
|
781
|
+
const coordinator = getCoordinator();
|
|
782
|
+
await coordinator.setCache(pendingAttachmentKey(actor.userId, attachmentId), {
|
|
783
|
+
channelId,
|
|
784
|
+
blobId: blob.id,
|
|
785
|
+
filename: file.filename,
|
|
786
|
+
contentType: file.contentType,
|
|
787
|
+
size: file.buffer.length,
|
|
788
|
+
}, PENDING_ATTACHMENT_TTL_MS);
|
|
789
|
+
return {
|
|
790
|
+
id: attachmentId,
|
|
791
|
+
filename: file.filename,
|
|
792
|
+
contentType: file.contentType,
|
|
793
|
+
size: file.buffer.length,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
async claimAttachments(actor, channelId, messageId, attachmentIds) {
|
|
797
|
+
if (attachmentIds.length === 0)
|
|
798
|
+
return [];
|
|
799
|
+
if (attachmentIds.length > MAX_CHANNEL_ATTACHMENTS_PER_MESSAGE) {
|
|
800
|
+
throw new ChannelError(`A message may carry at most ${MAX_CHANNEL_ATTACHMENTS_PER_MESSAGE} attachments`, 400);
|
|
801
|
+
}
|
|
802
|
+
const coordinator = getCoordinator();
|
|
803
|
+
const records = [];
|
|
804
|
+
for (const attachmentId of new Set(attachmentIds)) {
|
|
805
|
+
const pending = await coordinator.consumeCache(pendingAttachmentKey(actor.userId, attachmentId));
|
|
806
|
+
if (!pending || pending.channelId !== channelId) {
|
|
807
|
+
throw new ChannelError('Attachment upload expired', 400);
|
|
808
|
+
}
|
|
809
|
+
records.push({
|
|
810
|
+
id: attachmentId,
|
|
811
|
+
message_id: messageId,
|
|
812
|
+
channel_id: channelId,
|
|
813
|
+
blob_id: pending.blobId,
|
|
814
|
+
filename: pending.filename,
|
|
815
|
+
content_type: pending.contentType,
|
|
816
|
+
size: pending.size,
|
|
817
|
+
created_by: actor.userId,
|
|
818
|
+
created_at: Date.now(),
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
return records;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* The user-facing post: claims uploaded attachments and, when the
|
|
825
|
+
* composer named a model, appends a pending model reply and enqueues the
|
|
826
|
+
* durable mention job under the invoking user's identity.
|
|
827
|
+
*/
|
|
828
|
+
async postUserMessage(actor, channelId, input) {
|
|
829
|
+
const messageId = input.id ?? randomUUID();
|
|
830
|
+
const attachments = await this.claimAttachments(actor, channelId, messageId, input.attachmentIds ?? []);
|
|
831
|
+
const posted = await this.postMessage(actor, channelId, {
|
|
832
|
+
id: messageId,
|
|
833
|
+
content: input.content,
|
|
834
|
+
...(input.parentId ? { parentId: input.parentId } : {}),
|
|
835
|
+
attachments,
|
|
836
|
+
});
|
|
837
|
+
if (posted.created) {
|
|
838
|
+
await this.notifyForMessage(actor, channelId, messageId, input.content, input.parentId).catch(error => {
|
|
839
|
+
logger.warn('Channel notification fan-out failed', { error });
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if (!posted.created || !input.mentionModel)
|
|
843
|
+
return posted;
|
|
844
|
+
// The pending reply and its durable job share deterministic identities,
|
|
845
|
+
// so a retried request never produces a second model answer.
|
|
846
|
+
const replyId = `${messageId}-model`;
|
|
847
|
+
await this.postMessage(actor, channelId, {
|
|
848
|
+
id: replyId,
|
|
849
|
+
content: '',
|
|
850
|
+
...(input.parentId ? { parentId: input.parentId } : {}),
|
|
851
|
+
authorKind: 'model',
|
|
852
|
+
model: input.mentionModel,
|
|
853
|
+
metadata: { pending: true },
|
|
854
|
+
});
|
|
855
|
+
try {
|
|
856
|
+
await getDurableJobRuntime().service.enqueue({
|
|
857
|
+
jobType: CHANNEL_MENTION_JOB_TYPE,
|
|
858
|
+
actorUserId: actor.userId,
|
|
859
|
+
payload: {
|
|
860
|
+
mode: 'encrypted',
|
|
861
|
+
value: {
|
|
862
|
+
channelId,
|
|
863
|
+
promptMessageId: messageId,
|
|
864
|
+
replyMessageId: replyId,
|
|
865
|
+
model: input.mentionModel,
|
|
866
|
+
...(input.mentionProviderType
|
|
867
|
+
? { providerType: input.mentionProviderType }
|
|
868
|
+
: {}),
|
|
869
|
+
...(input.mentionProviderId
|
|
870
|
+
? { providerId: input.mentionProviderId }
|
|
871
|
+
: {}),
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
idempotencyScope: CHANNEL_MENTION_IDEMPOTENCY_SCOPE,
|
|
875
|
+
idempotencyKey: replyId,
|
|
876
|
+
maxAttempts: 2,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
logger.error('Failed to enqueue a channel model mention', { error });
|
|
881
|
+
await this.failModelReply(replyId, 'The model request could not start');
|
|
882
|
+
}
|
|
883
|
+
return posted;
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* In-app notifications for a new message: the DM peer, `@username`
|
|
887
|
+
* mentions of channel members, and the thread-root author.
|
|
888
|
+
*/
|
|
889
|
+
async notifyForMessage(actor, channelId, messageId, content, parentId) {
|
|
890
|
+
const channel = await channels().findById(channelId);
|
|
891
|
+
if (!channel)
|
|
892
|
+
return;
|
|
893
|
+
const { notificationService } = await import('./notificationService.js');
|
|
894
|
+
const authorNames = await resolveUsernames([actor.userId]);
|
|
895
|
+
const authorName = authorNames.get(actor.userId) ?? actor.userId;
|
|
896
|
+
const channelLabel = channel.type === 'dm'
|
|
897
|
+
? 'a direct message'
|
|
898
|
+
: `#${decryptOptional(channel.name) ?? 'channel'}`;
|
|
899
|
+
const preview = content.slice(0, 140);
|
|
900
|
+
const recipients = new Map();
|
|
901
|
+
if (channel.type === 'dm') {
|
|
902
|
+
const members = await channels().listMembers(channelId, 4);
|
|
903
|
+
for (const member of members) {
|
|
904
|
+
if (member.user_id === actor.userId)
|
|
905
|
+
continue;
|
|
906
|
+
recipients.set(member.user_id, {
|
|
907
|
+
type: 'channel-dm',
|
|
908
|
+
title: `${authorName} sent you a direct message`,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
else {
|
|
913
|
+
const mentioned = new Set([...content.matchAll(/@([A-Za-z0-9._-]+)/g)].map(match => match[1].toLowerCase()));
|
|
914
|
+
if (mentioned.size > 0) {
|
|
915
|
+
const members = await channels().listMembers(channelId, MAX_CHANNEL_MEMBERS);
|
|
916
|
+
const names = await resolveUsernames(members.map(member => member.user_id));
|
|
917
|
+
for (const member of members) {
|
|
918
|
+
if (member.user_id === actor.userId)
|
|
919
|
+
continue;
|
|
920
|
+
const username = names.get(member.user_id);
|
|
921
|
+
if (username && mentioned.has(username.toLowerCase())) {
|
|
922
|
+
recipients.set(member.user_id, {
|
|
923
|
+
type: 'channel-mention',
|
|
924
|
+
title: `${authorName} mentioned you in ${channelLabel}`,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (parentId) {
|
|
930
|
+
const parent = await messages().findById(parentId);
|
|
931
|
+
if (parent?.user_id &&
|
|
932
|
+
parent.user_id !== actor.userId &&
|
|
933
|
+
!recipients.has(parent.user_id) &&
|
|
934
|
+
parent.author_kind === 'user') {
|
|
935
|
+
recipients.set(parent.user_id, {
|
|
936
|
+
type: 'channel-mention',
|
|
937
|
+
title: `${authorName} replied to your message in ${channelLabel}`,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
for (const [userId, entry] of recipients) {
|
|
943
|
+
// Only members receive notifications, even if a mention names an
|
|
944
|
+
// outsider's username.
|
|
945
|
+
const member = await channels().findMember(channelId, userId);
|
|
946
|
+
if (!member)
|
|
947
|
+
continue;
|
|
948
|
+
await notificationService.publish({
|
|
949
|
+
userId,
|
|
950
|
+
type: entry.type,
|
|
951
|
+
title: entry.title,
|
|
952
|
+
body: preview,
|
|
953
|
+
href: `/channels?channel=${channelId}`,
|
|
954
|
+
sourceKey: `channel-message:${messageId}:${userId}`,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/** Reply completion used by the durable mention job. */
|
|
959
|
+
async completeModelReply(replyMessageId, content) {
|
|
960
|
+
const row = await messages().findById(replyMessageId);
|
|
961
|
+
if (!row || row.deleted_at)
|
|
962
|
+
return;
|
|
963
|
+
const now = Date.now();
|
|
964
|
+
const next = {
|
|
965
|
+
...row,
|
|
966
|
+
content: encryptionService.encrypt(content),
|
|
967
|
+
metadata: null,
|
|
968
|
+
updated_at: now,
|
|
969
|
+
};
|
|
970
|
+
await messages().update(next);
|
|
971
|
+
const [view] = await mapRows(row.user_id ?? 'default', [next]);
|
|
972
|
+
await appendChannelEvent(row.channel_id, 'channel.message.v1', replyMessageId, row.user_id ?? 'default', { type: 'message_edited', message: view });
|
|
973
|
+
}
|
|
974
|
+
async failModelReply(replyMessageId, errorSummary) {
|
|
975
|
+
const row = await messages().findById(replyMessageId);
|
|
976
|
+
if (!row || row.deleted_at)
|
|
977
|
+
return;
|
|
978
|
+
const now = Date.now();
|
|
979
|
+
const next = {
|
|
980
|
+
...row,
|
|
981
|
+
metadata: encryptionService.encrypt(JSON.stringify({ error: errorSummary })),
|
|
982
|
+
updated_at: now,
|
|
983
|
+
};
|
|
984
|
+
await messages().update(next);
|
|
985
|
+
const [view] = await mapRows(row.user_id ?? 'default', [next]);
|
|
986
|
+
await appendChannelEvent(row.channel_id, 'channel.message.v1', replyMessageId, row.user_id ?? 'default', { type: 'message_edited', message: view });
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Recent decrypted conversation context for a model mention, oldest
|
|
990
|
+
* first, ending at the prompting message.
|
|
991
|
+
*/
|
|
992
|
+
async mentionContext(channelId, promptMessageId) {
|
|
993
|
+
const prompt = await messages().findById(promptMessageId);
|
|
994
|
+
if (!prompt)
|
|
995
|
+
return [];
|
|
996
|
+
const rows = await messages().listPage(channelId, {
|
|
997
|
+
parentId: prompt.parent_id ? prompt.parent_id : null,
|
|
998
|
+
before: { created_at: prompt.created_at, id: prompt.id },
|
|
999
|
+
limit: MAX_CHANNEL_MENTION_CONTEXT_MESSAGES,
|
|
1000
|
+
});
|
|
1001
|
+
const ordered = [...rows].reverse();
|
|
1002
|
+
ordered.push(prompt);
|
|
1003
|
+
const usernames = await resolveUsernames(ordered.flatMap(row => (row.user_id ? [row.user_id] : [])));
|
|
1004
|
+
return ordered.flatMap(row => {
|
|
1005
|
+
if (row.deleted_at)
|
|
1006
|
+
return [];
|
|
1007
|
+
const content = decryptOptional(row.content);
|
|
1008
|
+
if (!content)
|
|
1009
|
+
return [];
|
|
1010
|
+
const author = row.author_kind === 'model'
|
|
1011
|
+
? `${row.model ?? 'assistant'} (model)`
|
|
1012
|
+
: (usernames.get(row.user_id ?? '') ?? 'someone');
|
|
1013
|
+
return [{ author, content }];
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
async openAttachment(actor, attachmentId) {
|
|
1017
|
+
const attachment = await messages().findAttachment(attachmentId);
|
|
1018
|
+
if (!attachment)
|
|
1019
|
+
throw notFound();
|
|
1020
|
+
await this.requireMember(attachment.channel_id, actor.userId);
|
|
1021
|
+
if (!attachment.created_by)
|
|
1022
|
+
throw notFound();
|
|
1023
|
+
const body = await getPlatformStorageRuntime().blobStore.open({
|
|
1024
|
+
id: attachment.blob_id,
|
|
1025
|
+
ownerUserId: attachment.created_by,
|
|
1026
|
+
});
|
|
1027
|
+
return {
|
|
1028
|
+
attachment: {
|
|
1029
|
+
id: attachment.id,
|
|
1030
|
+
filename: attachment.filename,
|
|
1031
|
+
contentType: attachment.content_type,
|
|
1032
|
+
size: attachment.size,
|
|
1033
|
+
},
|
|
1034
|
+
body,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
/** Recent decrypted candidates from the actor's channels, for search. */
|
|
1038
|
+
async searchCandidates(actor, maximum) {
|
|
1039
|
+
const memberships = await channels().listForUser(actor.userId, MAX_CHANNELS_LISTED);
|
|
1040
|
+
const rows = await messages().listRecentForChannels(memberships.map(entry => entry.channel.id), maximum);
|
|
1041
|
+
return rows.flatMap(row => {
|
|
1042
|
+
const content = decryptOptional(row.content);
|
|
1043
|
+
return content
|
|
1044
|
+
? [
|
|
1045
|
+
{
|
|
1046
|
+
channelId: row.channel_id,
|
|
1047
|
+
messageId: row.id,
|
|
1048
|
+
content,
|
|
1049
|
+
createdAt: row.created_at,
|
|
1050
|
+
},
|
|
1051
|
+
]
|
|
1052
|
+
: [];
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
export const channelService = new ChannelService();
|
|
1057
|
+
//# sourceMappingURL=channelService.js.map
|