diff --git a/plugins/manifest.json b/plugins/manifest.json index a9c07c6141..d47b0b5c5a 100644 --- a/plugins/manifest.json +++ b/plugins/manifest.json @@ -1,6 +1,6 @@ { "name": "immich-core", - "version": "2.4.1 ", + "version": "2.4.1", "title": "Immich Core", "description": "Core workflow capabilities for Immich", "author": "Immich Team", diff --git a/plugins/src/index.d.ts b/plugins/src/index.d.ts index 9d90b26f62..0d2cdb0f95 100644 --- a/plugins/src/index.d.ts +++ b/plugins/src/index.d.ts @@ -9,5 +9,6 @@ declare module 'extism:host' { interface user { updateAsset(ptr: PTR): I32; addAssetToAlbum(ptr: PTR): I32; + getFacesForAsset(ptr: PTR): PTR; } } diff --git a/plugins/src/index.ts b/plugins/src/index.ts index 137cee7087..8635c0583c 100644 --- a/plugins/src/index.ts +++ b/plugins/src/index.ts @@ -1,4 +1,4 @@ -const { updateAsset, addAssetToAlbum } = Host.getFunctions(); +const { updateAsset, addAssetToAlbum, getFacesForAsset } = Host.getFunctions(); function parseInput() { return JSON.parse(Host.inputString()); @@ -9,29 +9,51 @@ function returnOutput(output: any) { return 0; } +/** + * Filter by person - checks if the recognized person matches the configured person IDs. + * + * For PersonRecognized trigger: + * - data.personId contains the ID of the person that was just recognized + * - Checks if personId is in the configured list + * + * matchMode options: + * - 'any': passes if the triggering person is in the list + * - 'all': passes if all configured persons are present in the asset + * - 'exact': passes if the asset contains exactly the configured persons + */ export function filterPerson() { const input = parseInput(); + const { authToken, data, config } = input; + const { personIds, matchMode = 'any' } = config; - const { data, config } = input; - const { personIds, matchMode } = config; - - const faces = data.faces || []; - - if (faces.length === 0) { - return returnOutput({ passed: false }); + if (!personIds || personIds.length === 0) { + return returnOutput({ passed: true }); } + const triggerPersonId = data.personId; + + if (matchMode === 'any') { + const passed = triggerPersonId && personIds.includes(triggerPersonId); + return returnOutput({ passed }); + } + + const payload = Memory.fromJsonObject({ + authToken, + assetId: data.asset.id, + }); + + const resultPtr = getFacesForAsset(payload.offset); + payload.free(); + + const faces = JSON.parse(Memory.find(resultPtr).readJsonObject()); + const assetPersonIds: string[] = faces .filter((face: { personId: string | null }) => face.personId !== null) .map((face: { personId: string }) => face.personId); let passed = false; - if (!personIds || personIds.length === 0) { - passed = true; - } else if (matchMode === 'any') { - passed = personIds.some((id: string) => assetPersonIds.includes(id)); - } else if (matchMode === 'all') { + if (matchMode === 'all') { passed = personIds.every((id: string) => assetPersonIds.includes(id)); } else if (matchMode === 'exact') { const uniquePersonIds = new Set(personIds); @@ -80,7 +102,7 @@ export function actionAddToAlbum() { authToken, assetId: data.asset.id, albumId: albumId, - }) + }), ); addAssetToAlbum(ptr.offset); @@ -97,7 +119,7 @@ export function actionArchive() { authToken, id: data.asset.id, visibility: 'archive', - }) + }), ); updateAsset(ptr.offset); diff --git a/server/src/services/plugin-host.functions.ts b/server/src/services/plugin-host.functions.ts index 50b1052b54..197b913587 100644 --- a/server/src/services/plugin-host.functions.ts +++ b/server/src/services/plugin-host.functions.ts @@ -7,6 +7,7 @@ import { AlbumRepository } from 'src/repositories/album.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PersonRepository } from 'src/repositories/person.repository'; import { AssetTable } from 'src/schema/tables/asset.table'; import { requireAccess } from 'src/utils/access'; @@ -20,6 +21,7 @@ export class PluginHostFunctions { private albumRepository: AlbumRepository, private accessRepository: AccessRepository, private cryptoRepository: CryptoRepository, + private personRepository: PersonRepository, private logger: LoggingRepository, private pluginJwtSecret: string, ) {} @@ -33,6 +35,7 @@ export class PluginHostFunctions { 'extism:host/user': { updateAsset: (cp: CurrentPlugin, offs: bigint) => this.handleUpdateAsset(cp, offs), addAssetToAlbum: (cp: CurrentPlugin, offs: bigint) => this.handleAddAssetToAlbum(cp, offs), + getFacesForAsset: (cp: CurrentPlugin, offs: bigint) => this.handleGetFacesForAsset(cp, offs), }, }; } @@ -117,4 +120,28 @@ export class PluginHostFunctions { await this.albumRepository.addAssetIds(albumId, [assetId]); return 0; } + + /** + * Host function wrapper for getFacesForAsset. + * Reads the input from the plugin, parses it, and returns faces data. + */ + private async handleGetFacesForAsset(cp: CurrentPlugin, offs: bigint) { + const input = JSON.parse(cp.read(offs)!.text()); + const result = await this.getFacesForAsset(input); + return cp.store(JSON.stringify(result)); + } + + async getFacesForAsset(input: { authToken: string; assetId: string }) { + const { authToken, assetId } = input; + + const auth = this.validateToken(authToken); + + await requireAccess(this.accessRepository, { + auth: { user: { id: auth.userId } } as any, + permission: Permission.AssetRead, + ids: [assetId], + }); + + return this.personRepository.getFaces(assetId); + } } diff --git a/server/src/services/plugin.service.ts b/server/src/services/plugin.service.ts index 297a830fa7..b168dbe171 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -17,7 +17,7 @@ import { IWorkflowJob, JobItem, JobOf, WorkflowData } from 'src/types'; interface WorkflowContext { authToken: string; asset: Asset; - faces?: { faceId: string; personId: string | null }[]; + personId?: string; } interface PluginInput { @@ -25,7 +25,7 @@ interface PluginInput { config: T; data: { asset: Asset; - faces?: { faceId: string; personId: string | null }[]; + personId?: string; }; } @@ -46,6 +46,7 @@ export class PluginService extends BaseService { this.albumRepository, this.accessRepository, this.cryptoRepository, + this.personRepository, this.logger, this.pluginJwtSecret, ); @@ -231,7 +232,7 @@ export class PluginService extends BaseService { const authToken = this.cryptoRepository.signJwt({ userId: data.userId }, this.pluginJwtSecret); - const context = { + const context: WorkflowContext = { authToken, asset, }; @@ -257,16 +258,10 @@ export class PluginService extends BaseService { const authToken = this.cryptoRepository.signJwt({ userId: data.userId }, this.pluginJwtSecret); - const faces = await this.personRepository.getFaces(data.assetId); - const facePayload = faces.map((face) => ({ - faceId: face.id, - personId: face.personId, - })); - - const context = { + const context: WorkflowContext = { authToken, asset, - faces: facePayload, + personId: data.personId, }; const filtersPassed = await this.executeFilters(workflowFilters, context); @@ -309,7 +304,7 @@ export class PluginService extends BaseService { config: workflowFilter.filterConfig, data: { asset: context.asset, - faces: context.faces, + personId: context.personId, }, }; @@ -352,6 +347,7 @@ export class PluginService extends BaseService { config: workflowAction.actionConfig, data: { asset: context.asset, + personId: context.personId, }, };