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

View File

@@ -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",

View File

@@ -9,5 +9,6 @@ declare module 'extism:host' {
interface user {
updateAsset(ptr: PTR): I32;
addAssetToAlbum(ptr: PTR): I32;
getFacesForAsset(ptr: PTR): PTR;
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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<T = unknown> {
@@ -25,7 +25,7 @@ interface PluginInput<T = unknown> {
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,
},
};