mirror of
https://github.com/immich-app/immich.git
synced 2026-06-05 06:15:24 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a29dc703b6 | |||
| 92841f311f | |||
| 9d2e576630 | |||
| 936418a464 | |||
| 84c75d95c7 |
@@ -1 +0,0 @@
|
||||
custom: ['https://buy.immich.app', 'https://immich.store']
|
||||
@@ -1,134 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation
|
||||
in our community a harassment-free experience for everyone, regardless
|
||||
of age, body size, visible or invisible disability, ethnicity, sex
|
||||
characteristics, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance,
|
||||
race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open,
|
||||
welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for
|
||||
our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our
|
||||
mistakes, and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or
|
||||
political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in
|
||||
a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our
|
||||
standards of acceptable behavior and will take appropriate and fair
|
||||
corrective action in response to any behavior that they deem
|
||||
inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit,
|
||||
or reject comments, commits, code, wiki edits, issues, and other
|
||||
contributions that are not aligned to this Code of Conduct, and will
|
||||
communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also
|
||||
applies when an individual is officially representing the community in
|
||||
public spaces. Examples of representing our community include using an
|
||||
official e-mail address, posting via an official social media account,
|
||||
or acting as an appointed representative at an online or offline
|
||||
event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||
may be reported to the community leaders responsible for enforcement
|
||||
at our Discord channel. All complaints
|
||||
will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and
|
||||
security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in
|
||||
determining the consequences for any action they deem in violation of
|
||||
this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior
|
||||
deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders,
|
||||
providing clarity around the nature of the violation and an
|
||||
explanation of why the behavior was inappropriate. A public apology
|
||||
may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued
|
||||
behavior. No interaction with the people involved, including
|
||||
unsolicited interaction with those enforcing the Code of Conduct, for
|
||||
a specified period of time. This includes avoiding interactions in
|
||||
community spaces as well as external channels like social
|
||||
media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards,
|
||||
including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or
|
||||
public communication with the community for a specified period of
|
||||
time. No public or private interaction with the people involved,
|
||||
including unsolicited interaction with those enforcing the Code of
|
||||
Conduct, is allowed during this period. Violating these terms may lead
|
||||
to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of
|
||||
community standards, including sustained inappropriate behavior,
|
||||
harassment of an individual, or aggression toward or disparagement of
|
||||
classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction
|
||||
within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor
|
||||
Covenant][homepage], version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of
|
||||
conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the
|
||||
FAQ at https://www.contributor-covenant.org/faq. Translations are
|
||||
available at https://www.contributor-covenant.org/translations.
|
||||
@@ -1,5 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to `security@immich.app`
|
||||
@@ -55,22 +55,6 @@
|
||||
}
|
||||
],
|
||||
"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": [
|
||||
@@ -254,36 +238,6 @@
|
||||
"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",
|
||||
"title": "DEV: Nested properties",
|
||||
|
||||
Vendored
-3
@@ -22,7 +22,4 @@ declare module 'main' {
|
||||
export function assetTimeline(): I32;
|
||||
export function assetTrash(): I32;
|
||||
export function assetAddToAlbums(): I32;
|
||||
|
||||
// integrations
|
||||
export function webhook(): I32;
|
||||
}
|
||||
|
||||
@@ -102,44 +102,6 @@ export const assetTrash = () => {
|
||||
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 = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { albumIds: string[]; albumName?: string }>(({ config, data, functions }) => {
|
||||
const assetId = data.asset.id;
|
||||
|
||||
@@ -213,8 +213,6 @@ export class PluginRepository {
|
||||
{
|
||||
useWasi: true,
|
||||
runInWorker,
|
||||
// allow plugins (e.g. the webhook workflow step) to make outbound HTTP requests
|
||||
allowedHosts: ['*'],
|
||||
functions: {
|
||||
'extism:host/user': functions ?? {},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { WorkflowStepConfig, WorkflowTrigger } from '@immich/plugin-sdk';
|
||||
import { Kysely } from 'kysely';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:http';
|
||||
import { AddressInfo } from 'node:net';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
import { AssetVisibility, LogLevel } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
@@ -334,47 +332,4 @@ describe('core plugin', () => {
|
||||
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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
onError?: () => void;
|
||||
ref?: HTMLDivElement;
|
||||
imgRef?: HTMLImageElement;
|
||||
imgNaturalSize?: Size;
|
||||
imgScaledSize?: Size;
|
||||
backdrop?: Snippet;
|
||||
overlays?: Snippet;
|
||||
};
|
||||
@@ -82,6 +84,10 @@
|
||||
ref = $bindable(),
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
imgRef = $bindable(),
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
imgNaturalSize = $bindable(),
|
||||
// eslint-disable-next-line no-useless-assignment
|
||||
imgScaledSize = $bindable(),
|
||||
asset,
|
||||
sharedLink,
|
||||
objectFit = 'contain',
|
||||
@@ -149,10 +155,22 @@
|
||||
return { width: 1, height: 1 };
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
imgNaturalSize = imageDimensions;
|
||||
});
|
||||
|
||||
const scaledDimensions = $derived.by(() => {
|
||||
const scaleFn = objectFit === 'cover' ? scaleToCover : scaleToFit;
|
||||
return scaleFn(imageDimensions, container);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
imgScaledSize = scaledDimensions;
|
||||
});
|
||||
|
||||
const { insetInlineStart, top, displayWidth, displayHeight, rasterWidth, rasterHeight, rasterScale } = $derived.by(
|
||||
() => {
|
||||
const scaleFn = objectFit === 'cover' ? scaleToCover : scaleToFit;
|
||||
const { width, height } = scaleFn(imageDimensions, container);
|
||||
const { width, height } = scaledDimensions;
|
||||
if (maxRasterPixels === 0) {
|
||||
return {
|
||||
insetInlineStart: (container.width - width) / 2 + 'px',
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { SlideshowLook, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
|
||||
import { getNaturalSize, scaleToFit, type Size } from '$lib/utils/container-utils';
|
||||
import type { Size } from '$lib/utils/container-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getOcrBoundingBoxes } from '$lib/utils/ocr-utils';
|
||||
import { getBoundingBox, type BoundingBox } from '$lib/utils/people-utils';
|
||||
@@ -67,13 +67,9 @@
|
||||
height: containerHeight,
|
||||
});
|
||||
|
||||
const overlaySize = $derived.by((): Size => {
|
||||
if (!assetViewerManager.imgRef || !visibleImageReady) {
|
||||
return { width: 0, height: 0 };
|
||||
}
|
||||
let scaledDimensions = $state<Size>({ width: 0, height: 0 });
|
||||
|
||||
return scaleToFit(getNaturalSize(assetViewerManager.imgRef), { width: containerWidth, height: containerHeight });
|
||||
});
|
||||
const overlaySize = $derived(visibleImageReady ? scaledDimensions : { width: 0, height: 0 });
|
||||
|
||||
const highlightedBoxes = $derived(getBoundingBox(assetViewerManager.highlightedFaces, overlaySize));
|
||||
const isHighlighting = $derived(highlightedBoxes.length > 0);
|
||||
@@ -235,6 +231,7 @@
|
||||
onReady?.();
|
||||
}}
|
||||
bind:imgRef={assetViewerManager.imgRef}
|
||||
bind:imgScaledSize={scaledDimensions}
|
||||
bind:ref={adaptiveImage}
|
||||
>
|
||||
{#snippet backdrop()}
|
||||
|
||||
Reference in New Issue
Block a user