wip: call host function from plugin

This commit is contained in:
Alex Tran
2026-01-20 22:10:21 +00:00
parent 92fae68253
commit bbb2f01aa7
5 changed files with 74 additions and 28 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-core", "name": "immich-core",
"version": "2.4.1 ", "version": "2.4.1",
"title": "Immich Core", "title": "Immich Core",
"description": "Core workflow capabilities for Immich", "description": "Core workflow capabilities for Immich",
"author": "Immich Team", "author": "Immich Team",
+1
View File
@@ -9,5 +9,6 @@ declare module 'extism:host' {
interface user { interface user {
updateAsset(ptr: PTR): I32; updateAsset(ptr: PTR): I32;
addAssetToAlbum(ptr: PTR): I32; addAssetToAlbum(ptr: PTR): I32;
getFacesForAsset(ptr: PTR): PTR;
} }
} }
+37 -15
View File
@@ -1,4 +1,4 @@
const { updateAsset, addAssetToAlbum } = Host.getFunctions(); const { updateAsset, addAssetToAlbum, getFacesForAsset } = Host.getFunctions();
function parseInput() { function parseInput() {
return JSON.parse(Host.inputString()); return JSON.parse(Host.inputString());
@@ -9,29 +9,51 @@ function returnOutput(output: any) {
return 0; 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() { export function filterPerson() {
const input = parseInput(); const input = parseInput();
const { authToken, data, config } = input;
const { personIds, matchMode = 'any' } = config;
const { data, config } = input; if (!personIds || personIds.length === 0) {
const { personIds, matchMode } = config; return returnOutput({ passed: true });
const faces = data.faces || [];
if (faces.length === 0) {
return returnOutput({ passed: false });
} }
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 const assetPersonIds: string[] = faces
.filter((face: { personId: string | null }) => face.personId !== null) .filter((face: { personId: string | null }) => face.personId !== null)
.map((face: { personId: string }) => face.personId); .map((face: { personId: string }) => face.personId);
let passed = false; let passed = false;
if (!personIds || personIds.length === 0) { if (matchMode === 'all') {
passed = true;
} else if (matchMode === 'any') {
passed = personIds.some((id: string) => assetPersonIds.includes(id));
} else if (matchMode === 'all') {
passed = personIds.every((id: string) => assetPersonIds.includes(id)); passed = personIds.every((id: string) => assetPersonIds.includes(id));
} else if (matchMode === 'exact') { } else if (matchMode === 'exact') {
const uniquePersonIds = new Set(personIds); const uniquePersonIds = new Set(personIds);
@@ -80,7 +102,7 @@ export function actionAddToAlbum() {
authToken, authToken,
assetId: data.asset.id, assetId: data.asset.id,
albumId: albumId, albumId: albumId,
}) }),
); );
addAssetToAlbum(ptr.offset); addAssetToAlbum(ptr.offset);
@@ -97,7 +119,7 @@ export function actionArchive() {
authToken, authToken,
id: data.asset.id, id: data.asset.id,
visibility: 'archive', visibility: 'archive',
}) }),
); );
updateAsset(ptr.offset); updateAsset(ptr.offset);
@@ -7,6 +7,7 @@ import { AlbumRepository } from 'src/repositories/album.repository';
import { AssetRepository } from 'src/repositories/asset.repository'; import { AssetRepository } from 'src/repositories/asset.repository';
import { CryptoRepository } from 'src/repositories/crypto.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { PersonRepository } from 'src/repositories/person.repository';
import { AssetTable } from 'src/schema/tables/asset.table'; import { AssetTable } from 'src/schema/tables/asset.table';
import { requireAccess } from 'src/utils/access'; import { requireAccess } from 'src/utils/access';
@@ -20,6 +21,7 @@ export class PluginHostFunctions {
private albumRepository: AlbumRepository, private albumRepository: AlbumRepository,
private accessRepository: AccessRepository, private accessRepository: AccessRepository,
private cryptoRepository: CryptoRepository, private cryptoRepository: CryptoRepository,
private personRepository: PersonRepository,
private logger: LoggingRepository, private logger: LoggingRepository,
private pluginJwtSecret: string, private pluginJwtSecret: string,
) {} ) {}
@@ -33,6 +35,7 @@ export class PluginHostFunctions {
'extism:host/user': { 'extism:host/user': {
updateAsset: (cp: CurrentPlugin, offs: bigint) => this.handleUpdateAsset(cp, offs), updateAsset: (cp: CurrentPlugin, offs: bigint) => this.handleUpdateAsset(cp, offs),
addAssetToAlbum: (cp: CurrentPlugin, offs: bigint) => this.handleAddAssetToAlbum(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]); await this.albumRepository.addAssetIds(albumId, [assetId]);
return 0; 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);
}
} }
+8 -12
View File
@@ -17,7 +17,7 @@ import { IWorkflowJob, JobItem, JobOf, WorkflowData } from 'src/types';
interface WorkflowContext { interface WorkflowContext {
authToken: string; authToken: string;
asset: Asset; asset: Asset;
faces?: { faceId: string; personId: string | null }[]; personId?: string;
} }
interface PluginInput<T = unknown> { interface PluginInput<T = unknown> {
@@ -25,7 +25,7 @@ interface PluginInput<T = unknown> {
config: T; config: T;
data: { data: {
asset: Asset; asset: Asset;
faces?: { faceId: string; personId: string | null }[]; personId?: string;
}; };
} }
@@ -46,6 +46,7 @@ export class PluginService extends BaseService {
this.albumRepository, this.albumRepository,
this.accessRepository, this.accessRepository,
this.cryptoRepository, this.cryptoRepository,
this.personRepository,
this.logger, this.logger,
this.pluginJwtSecret, this.pluginJwtSecret,
); );
@@ -231,7 +232,7 @@ export class PluginService extends BaseService {
const authToken = this.cryptoRepository.signJwt({ userId: data.userId }, this.pluginJwtSecret); const authToken = this.cryptoRepository.signJwt({ userId: data.userId }, this.pluginJwtSecret);
const context = { const context: WorkflowContext = {
authToken, authToken,
asset, asset,
}; };
@@ -257,16 +258,10 @@ export class PluginService extends BaseService {
const authToken = this.cryptoRepository.signJwt({ userId: data.userId }, this.pluginJwtSecret); const authToken = this.cryptoRepository.signJwt({ userId: data.userId }, this.pluginJwtSecret);
const faces = await this.personRepository.getFaces(data.assetId); const context: WorkflowContext = {
const facePayload = faces.map((face) => ({
faceId: face.id,
personId: face.personId,
}));
const context = {
authToken, authToken,
asset, asset,
faces: facePayload, personId: data.personId,
}; };
const filtersPassed = await this.executeFilters(workflowFilters, context); const filtersPassed = await this.executeFilters(workflowFilters, context);
@@ -309,7 +304,7 @@ export class PluginService extends BaseService {
config: workflowFilter.filterConfig, config: workflowFilter.filterConfig,
data: { data: {
asset: context.asset, asset: context.asset,
faces: context.faces, personId: context.personId,
}, },
}; };
@@ -352,6 +347,7 @@ export class PluginService extends BaseService {
config: workflowAction.actionConfig, config: workflowAction.actionConfig,
data: { data: {
asset: context.asset, asset: context.asset,
personId: context.personId,
}, },
}; };