mirror of
https://github.com/immich-app/immich.git
synced 2026-06-04 05:45:24 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfb2ec0f59 |
@@ -55,6 +55,22 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"uiHints": ["SmartAlbum"]
|
"uiHints": ["SmartAlbum"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "asset-webhook",
|
||||||
|
"title": "Send a webhook",
|
||||||
|
"description": "Send the information of newly uploaded assets to an external endpoint",
|
||||||
|
"trigger": "AssetCreate",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"method": "immich-plugin-core#webhook",
|
||||||
|
"config": {
|
||||||
|
"url": "",
|
||||||
|
"method": "POST"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uiHints": ["Webhook"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"methods": [
|
"methods": [
|
||||||
@@ -238,6 +254,36 @@
|
|||||||
"required": ["albumIds"]
|
"required": ["albumIds"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "webhook",
|
||||||
|
"title": "Send a webhook",
|
||||||
|
"description": "Send the asset information to an external endpoint",
|
||||||
|
"types": ["AssetV1"],
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Webhook URL",
|
||||||
|
"description": "The endpoint that will receive the asset information"
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "HTTP method",
|
||||||
|
"enum": ["POST", "PUT", "PATCH"],
|
||||||
|
"default": "POST",
|
||||||
|
"description": "HTTP method used to send the request"
|
||||||
|
},
|
||||||
|
"secret": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Secret",
|
||||||
|
"description": "Optional value sent as the X-Immich-Webhook-Secret header so the receiver can verify the request"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["url"]
|
||||||
|
},
|
||||||
|
"uiHints": ["Webhook"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "noop1",
|
"name": "noop1",
|
||||||
"title": "DEV: Nested properties",
|
"title": "DEV: Nested properties",
|
||||||
|
|||||||
Vendored
+3
@@ -22,4 +22,7 @@ declare module 'main' {
|
|||||||
export function assetTimeline(): I32;
|
export function assetTimeline(): I32;
|
||||||
export function assetTrash(): I32;
|
export function assetTrash(): I32;
|
||||||
export function assetAddToAlbums(): I32;
|
export function assetAddToAlbums(): I32;
|
||||||
|
|
||||||
|
// integrations
|
||||||
|
export function webhook(): I32;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,44 @@ export const assetTrash = () => {
|
|||||||
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
|
return wrapper<WorkflowType.AssetV1, { inverse?: boolean }>(() => ({}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WebhookConfig = {
|
||||||
|
url: string;
|
||||||
|
method?: 'PATCH' | 'POST' | 'PUT';
|
||||||
|
secret?: string;
|
||||||
|
};
|
||||||
|
export const webhook = () => {
|
||||||
|
return wrapper<WorkflowType.AssetV1, WebhookConfig>(({ config, data, trigger, workflow }) => {
|
||||||
|
const { url, method = 'POST', secret } = config;
|
||||||
|
if (!url) {
|
||||||
|
console.warn('Webhook step skipped: no URL configured');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'Immich',
|
||||||
|
};
|
||||||
|
if (secret) {
|
||||||
|
headers['X-Immich-Webhook-Secret'] = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = JSON.stringify({
|
||||||
|
trigger,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
asset: data.asset,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = Http.request({ url, method, headers }, body);
|
||||||
|
if (response.status >= 400) {
|
||||||
|
console.error(`Webhook request to ${url} failed with status ${response.status}`);
|
||||||
|
} else {
|
||||||
|
console.debug(`Webhook request to ${url} returned status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const assetAddToAlbums = () => {
|
export const assetAddToAlbums = () => {
|
||||||
return wrapper<WorkflowType.AssetV1, { albumIds: string[]; albumName?: string }>(({ config, data, functions }) => {
|
return wrapper<WorkflowType.AssetV1, { albumIds: string[]; albumName?: string }>(({ config, data, functions }) => {
|
||||||
const assetId = data.asset.id;
|
const assetId = data.asset.id;
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ export class PluginRepository {
|
|||||||
{
|
{
|
||||||
useWasi: true,
|
useWasi: true,
|
||||||
runInWorker,
|
runInWorker,
|
||||||
|
// allow plugins (e.g. the webhook workflow step) to make outbound HTTP requests
|
||||||
|
allowedHosts: ['*'],
|
||||||
functions: {
|
functions: {
|
||||||
'extism:host/user': functions ?? {},
|
'extism:host/user': functions ?? {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { AddressInfo } from 'node:net';
|
||||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||||
import { AssetVisibility, LogLevel } from 'src/enum';
|
import { AssetVisibility, LogLevel } from 'src/enum';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
@@ -332,4 +334,47 @@ describe('core plugin', () => {
|
|||||||
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id);
|
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('webhook', () => {
|
||||||
|
it('should send the asset information to the configured endpoint', async () => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
const received = new Promise<{ headers: NodeJS.Dict<string | string[]>; body: any }>((resolve, reject) => {
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
req.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
req.on('end', () => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
server.close();
|
||||||
|
resolve({ headers: req.headers, body: JSON.parse(Buffer.concat(chunks).toString()) });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
server.on('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const { port } = server.address() as AddressInfo;
|
||||||
|
createWorkflow({
|
||||||
|
ownerId: user.id,
|
||||||
|
trigger: WorkflowTrigger.AssetCreate,
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
method: 'immich-plugin-core#webhook',
|
||||||
|
config: { url: `http://127.0.0.1:${port}/hook`, secret: 'super-secret' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((workflow) => ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id }))
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { headers, body } = await received;
|
||||||
|
expect(headers['x-immich-webhook-secret']).toBe('super-secret');
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
trigger: WorkflowTrigger.AssetCreate,
|
||||||
|
asset: { id: asset.id, ownerId: user.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user