Compare commits

..

3 Commits

Author SHA1 Message Date
Jason Rasmussen
9a35fc8631 feat: plugins 2025-10-21 16:45:23 -04:00
shenlong
c3a533ab40 chore(dep): bump flutter to 3.35.6 (#23120)
* chore(dep): bump flutter to 3.35.6

* chore(dep): bump flutter to 3.35.6 (#23121)

chore(dep): remove unused pub deps

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-10-21 15:46:48 +00:00
Rui Gonçalves
dbd6dcb786 fix(server): use GPSLongitudeRef and GPSLatitudeRef EXIF fields (#21445)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-10-21 16:12:22 +02:00
45 changed files with 696 additions and 957 deletions

View File

@@ -34,7 +34,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^60.0.0",
"exiftool-vendored": "^28.3.1",
"exiftool-vendored": "^31.1.0",
"globals": "^16.0.0",
"jose": "^5.6.3",
"luxon": "^3.4.4",

View File

@@ -1604,7 +1604,6 @@
"read_changelog": "Read Changelog",
"readonly_mode_disabled": "Read-only mode disabled",
"readonly_mode_enabled": "Read-only mode enabled",
"plugins": "Plugins",
"ready_for_upload": "Ready for upload",
"reassign": "Reassign",
"reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",

View File

@@ -1,7 +1,7 @@
[tools]
node = "22.20.0"
flutter = "3.35.5"
pnpm = "10.18.3"
flutter = "3.35.6"
pnpm = "10.18.1"
terragrunt = "0.58.12"
opentofu = "1.7.1"

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.35.4"
"flutter": "3.35.6"
}

View File

@@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.35.4",
"dart.flutterSdkPath": ".fvm/versions/3.35.6",
"dart.lineLength": 120,
"[dart]": {
"editor.rulers": [120]

View File

@@ -481,14 +481,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.7.0"
easy_image_viewer:
dependency: "direct main"
description:
name: easy_image_viewer
sha256: fb6cb123c3605552cc91150dcdb50ca977001dcddfb71d20caa0c5edc9a80947
url: "https://pub.dev"
source: hosted
version: "1.5.1"
easy_localization:
dependency: "direct main"
description:
@@ -1413,14 +1405,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.6.4"
photo_manager_image_provider:
dependency: "direct main"
description:
name: photo_manager_image_provider
sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pigeon:
dependency: "direct dev"
description:
@@ -2180,4 +2164,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.35.4"
flutter: ">=3.35.6"

View File

@@ -6,12 +6,9 @@ version: 2.1.0+3022
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.4
flutter: 3.35.6
dependencies:
flutter:
sdk: flutter
async: ^2.11.0
auto_route: ^9.2.0
background_downloader: ^9.2.5
@@ -23,10 +20,15 @@ dependencies:
crop_image: ^1.0.16
crypto: ^3.0.6
device_info_plus: ^12.0.0
# DB
drift: ^2.23.1
drift_flutter: ^0.2.4
dynamic_color: ^1.7.0
easy_image_viewer: ^1.5.1
easy_localization: ^3.0.7+1
ffi: ^2.1.4
file_picker: ^8.0.0+1
flutter:
sdk: flutter
flutter_cache_manager: ^3.4.1
flutter_displaymode: ^0.6.0
flutter_hooks: ^0.21.2
@@ -37,26 +39,39 @@ dependencies:
flutter_web_auth_2: ^5.0.0-alpha.0
fluttertoast: ^8.2.12
geolocator: ^14.0.0
hooks_riverpod: ^2.6.1
home_widget: ^0.8.0
hooks_riverpod: ^2.6.1
http: ^1.3.0
image_picker: ^1.1.2
intl: ^0.20.0
isar:
git:
url: https://github.com/immich-app/isar
ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a'
path: packages/isar/
isar_community_flutter_libs: 3.3.0-dev.3
local_auth: ^2.3.0
logging: ^1.3.0
maplibre_gl: ^0.22.0
native_video_player:
git:
url: https://github.com/immich-app/native_video_player
ref: '893894b'
network_info_plus: ^6.1.3
octo_image: ^2.1.0
openapi:
path: openapi
package_info_plus: ^8.3.0
path: ^1.9.1
path_provider: ^2.1.5
path_provider_foundation: ^2.4.1
permission_handler: ^11.4.0
photo_manager: ^3.6.4
photo_manager_image_provider: ^2.2.0
pinput: ^5.0.1
punycode: ^1.0.0
riverpod_annotation: ^2.6.1
scroll_date_picker: ^3.8.0
scrollable_positioned_list: ^0.3.8
share_handler: ^0.0.22
share_plus: ^10.1.4
@@ -69,52 +84,34 @@ dependencies:
uuid: ^4.5.1
wakelock_plus: ^1.2.10
worker_manager: ^7.2.3
scroll_date_picker: ^3.8.0
ffi: ^2.1.4
native_video_player:
git:
url: https://github.com/immich-app/native_video_player
ref: '893894b'
openapi:
path: openapi
isar:
git:
url: https://github.com/immich-app/isar
ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a'
path: packages/isar/
isar_community_flutter_libs: 3.3.0-dev.3
# DB
drift: ^2.23.1
drift_flutter: ^0.2.4
dev_dependencies:
auto_route_generator: ^9.0.0
build_runner: ^2.4.8
custom_lint: ^0.7.5
# Drift generator
drift_dev: ^2.23.1
fake_async: ^1.3.1
file: ^7.0.1 # for MemoryFileSystem
flutter_launcher_icons: ^0.14.3
flutter_lints: ^5.0.0
flutter_native_splash: ^2.4.5
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
build_runner: ^2.4.8
auto_route_generator: ^9.0.0
flutter_launcher_icons: ^0.14.3
flutter_native_splash: ^2.4.5
immich_mobile_immich_lint:
path: './immich_lint'
integration_test:
sdk: flutter
isar_generator:
git:
url: https://github.com/immich-app/isar
ref: 'bb1dca40fe87a001122e5d43bc6254718cb49f3a'
path: packages/isar_generator/
integration_test:
sdk: flutter
custom_lint: ^0.7.5
riverpod_lint: ^2.6.1
riverpod_generator: ^2.6.1
mocktail: ^1.0.4
immich_mobile_immich_lint:
path: './immich_lint'
fake_async: ^1.3.1
file: ^7.0.1 # for MemoryFileSystem
# Drift generator
drift_dev: ^2.23.1
# Type safe platform code
pigeon: ^26.0.0
riverpod_generator: ^2.6.1
riverpod_lint: ^2.6.1
flutter:
uses-material-design: true

View File

@@ -5717,154 +5717,6 @@
"description": "This endpoint requires the `person.read` permission."
}
},
"/plugins": {
"get": {
"operationId": "searchPlugins",
"parameters": [
{
"name": "isEnabled",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isInstalled",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isTrusted",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "name",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/PluginResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Plugin"
],
"x-immich-admin-only": true,
"x-immich-permission": "plugin.read",
"description": "This endpoint is an admin-only route, and requires the `plugin.read` permission."
}
},
"/plugins/{id}": {
"delete": {
"operationId": "deletePlugin",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"tags": [
"Plugin"
]
},
"put": {
"operationId": "updatePlugin",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PluginUpdateDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PluginResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Plugin"
],
"x-immich-admin-only": true,
"x-immich-permission": "plugin.update",
"description": "This endpoint is an admin-only route, and requires the `plugin.update` permission."
}
},
"/search/cities": {
"get": {
"operationId": "getAssetsByCity",
@@ -13359,9 +13211,6 @@
"person.statistics",
"person.merge",
"person.reassign",
"plugin.read",
"plugin.update",
"plugin.delete",
"pinCode.create",
"pinCode.update",
"pinCode.delete",
@@ -13649,66 +13498,6 @@
],
"type": "object"
},
"PluginResponseDto": {
"properties": {
"createdAt": {
"format": "date-time",
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"isEnabled": {
"type": "boolean"
},
"isInstalled": {
"type": "boolean"
},
"isTrusted": {
"type": "boolean"
},
"name": {
"type": "string"
},
"packageId": {
"type": "string"
},
"updatedAt": {
"format": "date-time",
"type": "string"
},
"version": {
"type": "integer"
}
},
"required": [
"createdAt",
"description",
"id",
"isEnabled",
"isInstalled",
"isTrusted",
"name",
"packageId",
"updatedAt",
"version"
],
"type": "object"
},
"PluginUpdateDto": {
"properties": {
"isEnabled": {
"type": "boolean"
}
},
"required": [
"isEnabled"
],
"type": "object"
},
"PurchaseResponse": {
"properties": {
"hideBuyButtonUntil": {

2
plugins/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

26
plugins/LICENSE Normal file
View File

@@ -0,0 +1,26 @@
Copyright 2024, The Extism Authors.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

12
plugins/esbuild.js Normal file
View File

@@ -0,0 +1,12 @@
const esbuild = require('esbuild');
esbuild
.build({
entryPoints: ['src/index.ts'],
outdir: 'dist',
bundle: true,
sourcemap: true,
minify: false, // might want to use true for production build
format: 'cjs', // needs to be CJS for now
target: ['es2020'] // don't go over es2020 because quickjs doesn't support it
})

443
plugins/package-lock.json generated Normal file
View File

@@ -0,0 +1,443 @@
{
"name": "js-pdk-template",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "js-pdk-template",
"version": "1.0.0",
"license": "BSD-3-Clause",
"devDependencies": {
"@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6",
"typescript": "^5.3.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
"integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
"integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
"integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
"integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
"integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
"integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
"integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
"integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
"integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
"integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
"integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
"integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
"integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
"integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
"integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
"integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
"integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
"integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
"integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
"integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
"integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
"integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
"integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@extism/js-pdk": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@extism/js-pdk/-/js-pdk-1.0.1.tgz",
"integrity": "sha512-YJWfHGeOuJnQw4V8NPNHvbSr6S8iDd2Ga6VEukwlRP7tu62ozTxIgokYw8i+rajD/16zz/gK0KYARBpm2qPAmQ==",
"dev": true
},
"node_modules/esbuild": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
"integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.19.12",
"@esbuild/android-arm": "0.19.12",
"@esbuild/android-arm64": "0.19.12",
"@esbuild/android-x64": "0.19.12",
"@esbuild/darwin-arm64": "0.19.12",
"@esbuild/darwin-x64": "0.19.12",
"@esbuild/freebsd-arm64": "0.19.12",
"@esbuild/freebsd-x64": "0.19.12",
"@esbuild/linux-arm": "0.19.12",
"@esbuild/linux-arm64": "0.19.12",
"@esbuild/linux-ia32": "0.19.12",
"@esbuild/linux-loong64": "0.19.12",
"@esbuild/linux-mips64el": "0.19.12",
"@esbuild/linux-ppc64": "0.19.12",
"@esbuild/linux-riscv64": "0.19.12",
"@esbuild/linux-s390x": "0.19.12",
"@esbuild/linux-x64": "0.19.12",
"@esbuild/netbsd-x64": "0.19.12",
"@esbuild/openbsd-x64": "0.19.12",
"@esbuild/sunos-x64": "0.19.12",
"@esbuild/win32-arm64": "0.19.12",
"@esbuild/win32-ia32": "0.19.12",
"@esbuild/win32-x64": "0.19.12"
}
},
"node_modules/typescript": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

19
plugins/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "plugins",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"build": "pnpm build:tsc && pnpm build:wasm",
"build:tsc": "tsc --noEmit && node esbuild.js",
"build:wasm": "extism-js dist/index.js -i src/index.d.ts -o dist/plugin.wasm"
},
"keywords": [],
"author": "",
"license": "BSD-3-Clause",
"devDependencies": {
"@extism/js-pdk": "^1.0.1",
"esbuild": "^0.19.6",
"typescript": "^5.3.2"
}
}

9
plugins/src/index.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
declare module 'main' {
export function archiveAssetAction(): I32;
}
declare module 'extism:host' {
interface user {
updateAsset(ptr: PTR): I32;
}
}

16
plugins/src/index.ts Normal file
View File

@@ -0,0 +1,16 @@
const { updateAsset } = Host.getFunctions();
export function archiveAssetAction() {
const event = JSON.parse(Host.inputString());
const ptr = Memory.fromString(
JSON.stringify({
id: event.asset.id,
visibility: 'archive',
})
);
updateAsset(ptr.offset);
ptr.free();
return 0;
}

24
plugins/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es2020", // Specify ECMAScript target version
"module": "commonjs", // Specify module code generation
"lib": [
"es2020"
], // Specify a list of library files to be included in the compilation
"types": [
"@extism/js-pdk",
"./src/index.d.ts"
], // Specify a list of type definition files to be included in the compilation
"strict": true, // Enable all strict type-checking options
"esModuleInterop": true, // Enables compatibility with Babel-style module imports
"skipLibCheck": true, // Skip type checking of declaration files
"allowJs": true, // Allow JavaScript files to be compiled
"noEmit": true // Do not emit outputs (no .js or .d.ts files)
},
"include": [
"src/**/*.ts" // Include all TypeScript files in src directory
],
"exclude": [
"node_modules" // Exclude the node_modules directory
]
}

82
pnpm-lock.yaml generated
View File

@@ -238,8 +238,8 @@ importers:
specifier: ^60.0.0
version: 60.0.0(eslint@9.37.0(jiti@2.6.1))
exiftool-vendored:
specifier: ^28.3.1
version: 28.8.0
specifier: ^31.1.0
version: 31.1.0
globals:
specifier: ^16.0.0
version: 16.4.0
@@ -299,8 +299,23 @@ importers:
specifier: ^5.3.3
version: 5.9.3
plugins:
devDependencies:
'@extism/js-pdk':
specifier: ^1.0.1
version: 1.1.1
esbuild:
specifier: ^0.19.6
version: 0.19.12
typescript:
specifier: ^5.3.2
version: 5.9.3
server:
dependencies:
'@extism/extism':
specifier: 2.0.0-rc13
version: 2.0.0-rc13
'@nestjs/bullmq':
specifier: ^11.0.1
version: 11.0.4(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(bullmq@5.61.0)
@@ -404,8 +419,8 @@ importers:
specifier: 4.3.3
version: 4.3.3
exiftool-vendored:
specifier: ^28.8.0
version: 28.8.0
specifier: ^31.1.0
version: 31.1.0
express:
specifier: ^5.1.0
version: 5.1.0
@@ -2525,6 +2540,12 @@ packages:
resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@extism/extism@2.0.0-rc13':
resolution: {integrity: sha512-iQ3mrPKOC0WMZ94fuJrKbJmMyz4LQ9Abf8gd4F5ShxKWa+cRKcVzk0EqRQsp5xXsQ2dO3zJTiA6eTc4Ihf7k+A==}
'@extism/js-pdk@1.1.1':
resolution: {integrity: sha512-VZLn/dX0ttA1uKk2PZeR/FL3N+nA1S5Vc7E5gdjkR60LuUIwCZT9cYON245V4HowHlBA7YOegh0TLjkx+wNbrA==}
'@faker-js/faker@10.1.0':
resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==}
engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'}
@@ -3495,8 +3516,8 @@ packages:
peerDependencies:
'@photo-sphere-viewer/core': 5.14.0
'@photostructure/tz-lookup@11.2.0':
resolution: {integrity: sha512-DwrvodcXHNSdGdeSF7SBL5o8aBlsaeuCuG7633F04nYsL3hn5Hxe3z/5kCqxv61J1q7ggKZ27GPylR3x0cPNXQ==}
'@photostructure/tz-lookup@11.2.1':
resolution: {integrity: sha512-ugPtvpdLwGQ8IWezSGFgUCYOpO/XXetfKLNv+UN2jjTYyfIDq9dA21GydGyzXuoQ06nN3VGBd3JxmEu+ZtXScg==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
@@ -5210,9 +5231,9 @@ packages:
resolution: {integrity: sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==}
hasBin: true
batch-cluster@13.0.0:
resolution: {integrity: sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==}
engines: {node: '>=14'}
batch-cluster@15.0.1:
resolution: {integrity: sha512-eUmh0ld1AUPKTEmdzwGF9QTSexXAyt9rA1F5zDfW1wUi3okA3Tal4NLdCeFI6aiKpBenQhR6NmK9bW9tBHTGPQ==}
engines: {node: '>=20'}
batch@0.6.1:
resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
@@ -6547,16 +6568,18 @@ packages:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
exiftool-vendored.exe@13.0.0:
resolution: {integrity: sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==}
exiftool-vendored.exe@13.38.0:
resolution: {integrity: sha512-oZx5enTAvSiIAXL+OEk7nNWrfUhEdKUpaGwDjCmz4VKwOa4HbisqyM808xPGPYj8X7XikcME/fq5hvevPeE3cw==}
os: [win32]
exiftool-vendored.pl@13.0.1:
resolution: {integrity: sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A==}
exiftool-vendored.pl@13.38.0:
resolution: {integrity: sha512-Q3xl1nnwswrsR5344z4NyqvI74fKwla+VJHY1N+32gcDgt8cs9KBsDUwcNzKHSOSa/MjEfniuCJVrQiqR05iag==}
os: ['!win32']
hasBin: true
exiftool-vendored@28.8.0:
resolution: {integrity: sha512-R7tirJLr9fWuH9JS/KFFLB+O7jNGKuPXGxREc6YybYangEudGb+X8ERsYXk9AifMiAWh/2agNfbgkbcQcF+MxA==}
exiftool-vendored@31.1.0:
resolution: {integrity: sha512-q8StxLawHLDvhqv/uoBYCfVbDskn49Cr5ouNCZhh4lgryGu1aymHwK9AvO6RcW2SbPm5MSnQDJOfGp2MW5Nnrw==}
engines: {node: '>=20.0.0'}
expect-type@1.2.1:
resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==}
@@ -10946,6 +10969,9 @@ packages:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'}
urlpattern-polyfill@8.0.2:
resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==}
utf8-byte-length@1.0.5:
resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==}
@@ -14006,6 +14032,12 @@ snapshots:
'@eslint/core': 0.16.0
levn: 0.4.1
'@extism/extism@2.0.0-rc13': {}
'@extism/js-pdk@1.1.1':
dependencies:
urlpattern-polyfill: 8.0.2
'@faker-js/faker@10.1.0': {}
'@fig/complete-commander@3.2.0(commander@11.1.0)':
@@ -15128,7 +15160,7 @@ snapshots:
'@photo-sphere-viewer/core': 5.14.0
three: 0.180.0
'@photostructure/tz-lookup@11.2.0': {}
'@photostructure/tz-lookup@11.2.1': {}
'@pkgjs/parseargs@0.11.0':
optional: true
@@ -17069,7 +17101,7 @@ snapshots:
baseline-browser-mapping@2.8.15: {}
batch-cluster@13.0.0: {}
batch-cluster@15.0.1: {}
batch@0.6.1: {}
@@ -18608,21 +18640,21 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
exiftool-vendored.exe@13.0.0:
exiftool-vendored.exe@13.38.0:
optional: true
exiftool-vendored.pl@13.0.1: {}
exiftool-vendored.pl@13.38.0: {}
exiftool-vendored@28.8.0:
exiftool-vendored@31.1.0:
dependencies:
'@photostructure/tz-lookup': 11.2.0
'@photostructure/tz-lookup': 11.2.1
'@types/luxon': 3.7.1
batch-cluster: 13.0.0
exiftool-vendored.pl: 13.0.1
batch-cluster: 15.0.1
exiftool-vendored.pl: 13.38.0
he: 1.2.0
luxon: 3.7.2
optionalDependencies:
exiftool-vendored.exe: 13.0.0
exiftool-vendored.exe: 13.38.0
expect-type@1.2.1: {}
@@ -23923,6 +23955,8 @@ snapshots:
punycode: 1.4.1
qs: 6.14.0
urlpattern-polyfill@8.0.2: {}
utf8-byte-length@1.0.5: {}
util-deprecate@1.0.2: {}

View File

@@ -4,6 +4,7 @@ packages:
- e2e
- open-api/typescript-sdk
- server
- plugins
- web
- .github
ignoredBuiltDependencies:

View File

@@ -61,7 +61,7 @@ RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
# Flutter SDK
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
ENV FLUTTER_CHANNEL="stable"
ENV FLUTTER_VERSION="3.35.4"
ENV FLUTTER_VERSION="3.35.6"
ENV FLUTTER_HOME=/flutter
ENV PATH=${PATH}:${FLUTTER_HOME}/bin

View File

@@ -34,6 +34,7 @@
"email:dev": "email dev -p 3050 --dir src/emails"
},
"dependencies": {
"@extism/extism": "2.0.0-rc13",
"@nestjs/bullmq": "^11.0.1",
"@nestjs/common": "^11.0.4",
"@nestjs/core": "^11.0.4",
@@ -68,7 +69,7 @@
"cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"cron": "4.3.3",
"exiftool-vendored": "^28.8.0",
"exiftool-vendored": "^31.1.0",
"express": "^5.1.0",
"fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2",

View File

@@ -18,7 +18,6 @@ import { NotificationController } from 'src/controllers/notification.controller'
import { OAuthController } from 'src/controllers/oauth.controller';
import { PartnerController } from 'src/controllers/partner.controller';
import { PersonController } from 'src/controllers/person.controller';
import { PluginController } from 'src/controllers/plugin.controller';
import { SearchController } from 'src/controllers/search.controller';
import { ServerController } from 'src/controllers/server.controller';
import { SessionController } from 'src/controllers/session.controller';
@@ -55,7 +54,6 @@ export const controllers = [
OAuthController,
PartnerController,
PersonController,
PluginController,
SearchController,
ServerController,
SessionController,

View File

@@ -1,36 +0,0 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import { PluginResponseDto, PluginSearchDto, PluginUpdateDto } from 'src/dtos/plugin.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { PluginService } from 'src/services/plugin.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Plugin')
@Controller('plugins')
export class PluginController {
constructor(private service: PluginService) {}
@Get()
@Authenticated({ admin: true, permission: Permission.PluginRead })
searchPlugins(@Auth() auth: AuthDto, @Query() dto: PluginSearchDto): Promise<PluginResponseDto[]> {
return this.service.search(auth, dto);
}
@Put(':id')
@Authenticated({ admin: true, permission: Permission.PluginUpdate })
updatePlugin(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: PluginUpdateDto,
): Promise<PluginResponseDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
deletePlugin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
}

View File

@@ -144,19 +144,6 @@ export type UserAdmin = User & {
metadata: UserMetadataItem[];
};
export type Plugin = {
id: string;
createdAt: Date;
updatedAt: Date;
packageId: string;
version: number;
name: string;
description: string;
isEnabled: boolean;
isInstalled: boolean;
isTrusted: boolean;
};
export type StorageAsset = {
id: string;
ownerId: string;

View File

@@ -1,58 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsString } from 'class-validator';
import { Plugin } from 'src/database';
import { Optional, ValidateBoolean } from 'src/validation';
export class PluginSearchDto {
@ValidateBoolean({ optional: true })
isEnabled?: boolean;
@ValidateBoolean({ optional: true })
isTrusted?: boolean;
@ValidateBoolean({ optional: true })
isInstalled?: boolean;
@IsString()
@Optional()
name?: string;
}
export class PluginImportDto {
url!: string;
install!: boolean;
isEnabled!: boolean;
isTrusted!: boolean;
}
export class PluginUpdateDto {
@IsBoolean()
isEnabled!: boolean;
}
export class PluginResponseDto {
id!: string;
createdAt!: Date;
updatedAt!: Date;
packageId!: string;
@ApiProperty({ type: 'integer' })
version!: number;
name!: string;
description!: string;
isEnabled!: boolean;
isInstalled!: boolean;
isTrusted!: boolean;
}
export const mapPlugin = (plugin: Plugin): PluginResponseDto => ({
id: plugin.id,
createdAt: plugin.createdAt,
updatedAt: plugin.updatedAt,
packageId: plugin.packageId,
version: plugin.version,
name: plugin.name,
description: plugin.description,
isEnabled: plugin.isEnabled,
isInstalled: plugin.isInstalled,
isTrusted: plugin.isTrusted,
});

View File

@@ -164,10 +164,6 @@ export enum Permission {
PersonMerge = 'person.merge',
PersonReassign = 'person.reassign',
PluginRead = 'plugin.read',
PluginUpdate = 'plugin.update',
PluginDelete = 'plugin.delete',
PinCodeCreate = 'pinCode.create',
PinCodeUpdate = 'pinCode.update',
PinCodeDelete = 'pinCode.delete',

View File

@@ -1,91 +0,0 @@
export type PluginFactory = {
register: () => MaybePromise<Plugin>;
};
export type PluginLike = Plugin | PluginFactory | { default: Plugin } | { plugin: Plugin };
export interface Plugin<T extends PluginConfig | undefined = undefined> {
version: 1;
id: string;
name: string;
description: string;
actions: PluginAction<T>[];
}
export type PluginAction<T extends PluginConfig | undefined = undefined> = {
id: string;
name: string;
description: string;
events?: EventType[];
config?: T;
} & (
| { type: ActionType.ASSET; onAction: OnAction<T, AssetDto> }
| { type: ActionType.ALBUM; onAction: OnAction<T, AlbumDto> }
| { type: ActionType.ALBUM_ASSET; onAction: OnAction<T, { asset: AssetDto; album: AlbumDto }> }
);
export type OnAction<T extends PluginConfig | undefined, D = PluginActionData> = T extends undefined
? (ctx: PluginContext, data: D) => MaybePromise<void>
: (ctx: PluginContext, data: D, config: InferConfig<T>) => MaybePromise<void>;
export interface PluginContext {
updateAsset: (asset: { id: string; isArchived: boolean }) => Promise<void>;
}
export type PluginActionData = { data: { asset?: AssetDto; album?: AlbumDto } } & (
| { type: EventType.ASSET_UPLOAD; data: { asset: AssetDto } }
| { type: EventType.ASSET_UPDATE; data: { asset: AssetDto } }
| { type: EventType.ASSET_TRASH; data: { asset: AssetDto } }
| { type: EventType.ASSET_DELETE; data: { asset: AssetDto } }
| { type: EventType.ALBUM_CREATE; data: { album: AlbumDto } }
| { type: EventType.ALBUM_UPDATE; data: { album: AlbumDto } }
);
export type PluginConfig = Record<string, ConfigItem>;
export type ConfigItem = {
name: string;
description: string;
required?: boolean;
} & { [K in Types]: { type: K; default?: InferType<K> } }[Types];
export type InferType<T extends Types> = T extends 'string'
? string
: T extends 'date'
? Date
: T extends 'number'
? number
: T extends 'boolean'
? boolean
: never;
type Types = 'string' | 'boolean' | 'number' | 'date';
type MaybePromise<T = void> = Promise<T> | T;
type IfRequired<T extends ConfigItem, Type> = T['required'] extends true ? Type : Type | undefined;
type InferConfig<T> = T extends PluginConfig
? {
[K in keyof T]: IfRequired<T[K], InferType<T[K]['type']>>;
}
: never;
export enum ActionType {
ASSET = 'asset',
ALBUM = 'album',
ALBUM_ASSET = 'album-asset',
}
export enum EventType {
ASSET_UPLOAD = 'asset.upload',
ASSET_UPDATE = 'asset.update',
ASSET_TRASH = 'asset.trash',
ASSET_DELETE = 'asset.delete',
ASSET_ARCHIVE = 'asset.archive',
ASSET_UNARCHIVE = 'asset.unarchive',
ALBUM_CREATE = 'album.create',
ALBUM_UPDATE = 'album.update',
ALBUM_DELETE = 'album.delete',
}
export type AssetDto = { id: string; type: 'asset' };
export type AlbumDto = { id: string; type: 'album' };

View File

@@ -1,92 +0,0 @@
import sdk from '../../open-api/typescript-sdk';
export type Plugin = {
id: string;
name: string;
description: string;
filters: Filter[];
// actions: Action[];
};
export enum EntityType {
Asset = 'asset',
Album = 'album',
}
type PluginItem = {
id: string;
name: string;
description?: string;
type: EntityType;
configuration?: Config[];
};
type FilterContext<C = Record<string, any>, D = any> = {
api: {
getAssetAlbums: (assetId: string) => Promise<any[]>;
};
sdk: typeof sdk;
config: C;
};
type AssetFilter = {
type: EntityType.Asset;
filter: (ctx: FilterContext, input: { asset: { id: string } }) => Promise<boolean>;
};
type AlbumFilter = {
type: EntityType.Album;
filter: (ctx: FilterContext, input: { album: { id: string; name: string } }) => Promise<boolean>;
};
export type Filter = PluginItem & (AssetFilter | AlbumFilter);
export type Config = {
key: string;
type: PluginConfigType;
required?: boolean;
};
export type PluginConfigType = 'string' | 'number' | 'boolean' | 'date' | 'albumId' | 'assetId';
const authenticate = (ctx: FilterContext) => {
const
sdk.init()
}
export const corePlugin: Plugin = {
id: 'immich',
name: 'Immich Core Plugin',
description: 'Core actions and filters for workflows',
filters: [
{
id: 'core.notInAnyAlbum',
name: 'Not in any album',
description: 'Filters assets that are not in any album',
type: EntityType.Asset,
async filter(ctx, { asset }) {
const albums = await ctx.sdk.getAllAlbums({ assetId: asset.id });
return albums.length === 0;
},
},
{
id: 'core.notInAlbum',
name: 'Not in an album',
description: 'Run on assets not in the specified album',
type: EntityType.Asset,
configuration: [
{
key: 'albumId',
type: 'string',
required: true,
},
],
async filter(ctx, { asset }) {
// missing api to check if an asset is in an album
const albums = await ctx.sdk.getAllAlbums({ assetId: asset.id });
return !!albums.find((album) => album.id === ctx.config.albumId);
},
},
],
};

View File

@@ -1,55 +0,0 @@
import { ActionType, AssetDto, Plugin, PluginContext } from 'src/interfaces/plugin.interface';
const onAsset = async (ctx: PluginContext, asset: AssetDto) => {
await ctx.updateAsset({ id: asset.id, isArchived: true });
};
export const plugin: Plugin = {
version: 1,
id: 'immich-plugins',
name: 'Asset Plugin',
description: 'Immich asset plugin',
actions: [
{
id: 'asset.favorite',
name: '',
type: ActionType.ASSET,
description: '',
onAction: async (ctx, asset) => {
await ctx.updateAsset({ id: asset.id, isArchived: false });
},
},
{
id: 'asset.unfavorite',
name: '',
type: ActionType.ASSET,
description: '',
onAction: () => {
console.log('Unfavorite');
},
},
{
id: 'asset.action',
name: '',
type: ActionType.ASSET,
description: '',
onAction: (ctx, asset) => onAsset(ctx, asset),
},
{
id: 'album-asset.action',
name: '',
type: ActionType.ALBUM_ASSET,
description: '',
onAction: (ctx, { asset }) => onAsset(ctx, asset),
},
{
id: 'asset.unarchive',
name: '',
type: ActionType.ASSET,
description: '',
onAction: () => {
console.log('Unarchive');
},
},
],
};

View File

@@ -33,6 +33,11 @@ type Item<T extends EmitEvent> = {
label: string;
};
type AssetCreateV1 = {
id: string;
ownerId: string;
};
type EventMap = {
// app events
AppBootstrap: [];
@@ -53,6 +58,7 @@ type EventMap = {
AlbumInvite: [{ id: string; userId: string }];
// asset events
AssetCreate: [{ asset: AssetCreateV1 }];
AssetTag: [{ assetId: string }];
AssetUntag: [{ assetId: string }];
AssetHide: [{ assetId: string; userId: string }];

View File

@@ -27,7 +27,6 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { PartnerRepository } from 'src/repositories/partner.repository';
import { PersonRepository } from 'src/repositories/person.repository';
import { PluginRepository } from 'src/repositories/plugin.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { SearchRepository } from 'src/repositories/search.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
@@ -45,6 +44,7 @@ import { TrashRepository } from 'src/repositories/trash.repository';
import { UserRepository } from 'src/repositories/user.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
import { ViewRepository } from 'src/repositories/view-repository';
export const repositories = [
AccessRepository,
ActivityRepository,
@@ -76,7 +76,6 @@ export const repositories = [
PartnerRepository,
PersonRepository,
ProcessRepository,
PluginRepository,
SearchRepository,
SessionRepository,
ServerInfoRepository,

View File

@@ -84,6 +84,7 @@ export class MetadataRepository {
numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength', 'FileSize'],
/* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
geoTz: (lat, lon) => geotz.find(lat, lon)[0],
geolocation: true,
// Enable exiftool LFS to parse metadata for files larger than 2GB.
readArgs: ['-api', 'largefilesupport=1'],
writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],

View File

@@ -1,83 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Insertable, Kysely, Updateable } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { writeFile } from 'node:fs/promises';
import { PluginLike } from 'src/interfaces/plugin.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { DB } from 'src/schema';
import { PluginTable } from 'src/schema/tables/plugin.table';
type PluginSearchOptions = {
id?: string;
namespace?: string;
version?: number;
name?: string;
isEnabled?: boolean;
isInstalled?: boolean;
isTrusted?: boolean;
};
@Injectable()
export class PluginRepository {
constructor(
@InjectKysely() private db: Kysely<DB>,
private logger: LoggingRepository,
) {
this.logger.setContext(PluginRepository.name);
}
search(options: PluginSearchOptions) {
return this.db
.selectFrom('plugin')
.select([
'id',
'packageId',
'version',
'name',
'description',
'isEnabled',
'isInstalled',
'isTrusted',
'requirePath',
'createdAt',
'updatedAt',
'deletedAt',
])
.$if(!!options.id, (qb) => qb.where('id', '=', options.id!))
.$if(!!options.version, (qb) => qb.where('version', '=', options.version!))
.$if(!!options.name, (qb) => qb.where('name', '=', options.name!))
.$if(!!options.isEnabled, (qb) => qb.where('isEnabled', '=', options.isEnabled!))
.$if(!!options.isInstalled, (qb) => qb.where('isInstalled', '=', options.isInstalled!))
.$if(!!options.isTrusted, (qb) => qb.where('isTrusted', '=', options.isTrusted!))
.execute();
}
create(dto: Insertable<PluginTable>) {
return this.db.insertInto('plugin').values(dto).returningAll().executeTakeFirstOrThrow();
}
get(id: string) {
return this.db.selectFrom('plugin').where('id', '=', id).executeTakeFirst();
}
update(dto: Updateable<PluginTable>) {
return this.db.updateTable('plugin').set(dto).returningAll().executeTakeFirstOrThrow();
}
async delete(id: string): Promise<void> {
await this.db.deleteFrom('plugin').where('id', '=', id).execute();
}
async download(url: string, downloadPath: string): Promise<void> {
try {
const { json } = await fetch(url);
await writeFile(downloadPath, await json());
} catch (error) {
this.logger.error(`Error downloading the plugin from ${url}. ${error}`);
}
}
load(pluginPath: string): Promise<PluginLike> {
return import(pluginPath);
}
}

View File

@@ -51,7 +51,6 @@ import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
import { PartnerTable } from 'src/schema/tables/partner.table';
import { PersonAuditTable } from 'src/schema/tables/person-audit.table';
import { PersonTable } from 'src/schema/tables/person.table';
import { PluginTable } from 'src/schema/tables/plugin.table';
import { SessionTable } from 'src/schema/tables/session.table';
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
@@ -106,7 +105,6 @@ export class ImmichDatabase {
PartnerTable,
PersonTable,
PersonAuditTable,
PluginTable,
SessionTable,
SharedLinkAssetTable,
SharedLinkTable,
@@ -204,8 +202,6 @@ export interface DB {
person: PersonTable;
person_audit: PersonAuditTable;
plugin: PluginTable;
session: SessionTable;
session_sync_checkpoint: SessionSyncCheckpointTable;

View File

@@ -1,61 +0,0 @@
import { Insertable } from 'kysely';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Generated,
PrimaryGeneratedColumn,
Table,
Timestamp,
UpdateDateColumn,
} from 'src/sql-tools';
const plugin: Insertable<PluginTable> = {
version: 1,
id: '123',
name: 'Immich Core Plugin',
description: 'Core plugins for Immich',
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
packageId: 'immich-plugin-',
};
@Table('plugins')
export class PluginTable {
@PrimaryGeneratedColumn('uuid')
id!: Generated<string>;
@CreateDateColumn()
createdAt!: Generated<Timestamp>;
@UpdateDateColumn()
updatedAt!: Generated<Timestamp>;
@DeleteDateColumn()
deletedAt!: Date | null;
@Column({ unique: true })
packageId!: string;
@Column()
version!: number;
@Column()
name!: string;
@Column()
description!: string;
@Column({ type: 'boolean', default: true })
isEnabled!: Generated<boolean>;
@Column({ type: 'boolean', default: false })
isInstalled!: Generated<boolean>;
@Column({ type: 'boolean', default: false })
isTrusted!: Generated<boolean>;
@Column({ nullable: true })
requirePath!: string | null;
}

View File

@@ -426,6 +426,9 @@ export class AssetMediaService extends BaseService {
}
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size });
await this.eventRepository.emit('AssetCreate', { asset });
await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: asset.id, source: 'upload' } });
return asset;

View File

@@ -34,7 +34,6 @@ import { NotificationRepository } from 'src/repositories/notification.repository
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { PartnerRepository } from 'src/repositories/partner.repository';
import { PersonRepository } from 'src/repositories/person.repository';
import { PluginRepository } from 'src/repositories/plugin.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { SearchRepository } from 'src/repositories/search.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
@@ -139,7 +138,6 @@ export class BaseService {
protected oauthRepository: OAuthRepository,
protected partnerRepository: PartnerRepository,
protected personRepository: PersonRepository,
protected pluginRepository: PluginRepository,
protected processRepository: ProcessRepository,
protected searchRepository: SearchRepository,
protected serverInfoRepository: ServerInfoRepository,

View File

@@ -41,7 +41,6 @@ import { UserAdminService } from 'src/services/user-admin.service';
import { UserService } from 'src/services/user.service';
import { VersionService } from 'src/services/version.service';
import { ViewService } from 'src/services/view.service';
import { WorkflowService } from 'src/services/workflow.service';
export const services = [
ApiKeyService,
@@ -87,5 +86,4 @@ export const services = [
UserService,
VersionService,
ViewService,
WorkflowService,
];

View File

@@ -447,7 +447,10 @@ export class MetadataService extends BaseService {
* For RAW images in the CR2 or RAF format, the "ImageSize" value seems to be correct,
* but ImageWidth and ImageHeight are not correct (they contain the dimensions of the preview image).
*/
let [width, height] = exifTags.ImageSize?.split('x').map((dim) => Number.parseInt(dim) || undefined) || [];
let [width, height] =
exifTags.ImageSize?.toString()
?.split('x')
?.map((dim) => Number.parseInt(dim) || undefined) ?? [];
if (!width || !height) {
[width, height] = [exifTags.ImageWidth, exifTags.ImageHeight];
}

View File

@@ -1,42 +1,31 @@
import { Plugin } from 'src/database';
import { AuthDto } from 'src/dtos/auth.dto';
import { mapPlugin, PluginSearchDto, PluginUpdateDto } from 'src/dtos/plugin.dto';
import { Permission } from 'src/enum';
import { CurrentPlugin, newPlugin } from '@extism/extism';
import { Updateable } from 'kysely';
import { resolve } from 'node:path';
import { OnEvent } from 'src/decorators';
import { ArgOf } from 'src/repositories/event.repository';
import { AssetTable } from 'src/schema/tables/asset.table';
import { BaseService } from 'src/services/base.service';
const plugins: Plugin[] = [
{
id: '123',
name: 'Immich Core Plugin',
description: 'Core plugins for Immich',
version: 1,
isEnabled: true,
isInstalled: true,
isTrusted: true,
createdAt: new Date(),
updatedAt: new Date(),
packageId: 'immich-plugin-',
},
];
export class PluginService extends BaseService {
async search(auth: AuthDto, dto: PluginSearchDto) {
await this.requireAccess({ auth, permission: Permission.PluginRead, ids: [] });
// return this.pluginRepository.search(dto);
return plugins.map(mapPlugin);
}
async update(auth: AuthDto, id: string, dto: PluginUpdateDto) {
await this.requireAccess({ auth, permission: Permission.PluginUpdate, ids: [id] });
return this.pluginRepository.update({
id,
isEnabled: dto.isEnabled,
@OnEvent({ name: 'AssetCreate' })
async handleAssetCreate({ asset }: ArgOf<'AssetCreate'>) {
console.log(`PluginService.handleAssetCreate: ${asset.id}`);
const corePath = resolve('../plugins/dist/plugin.wasm');
const plugin = await newPlugin(corePath, {
useWasi: true,
functions: {
'extism:host/user': {
updateAsset: (cp: CurrentPlugin, offs: bigint) => this.updateAsset(JSON.parse(cp.read(offs)!.text())),
},
},
});
const event = { asset };
await plugin.call('archiveAssetAction', JSON.stringify(event));
}
async delete(auth: AuthDto, id: string) {
await this.requireAccess({ auth, permission: Permission.PluginUpdate, ids: [id] });
await this.pluginRepository.delete(id);
async updateAsset(asset: Updateable<AssetTable> & { id: string }) {
console.log(`Updating asset ${asset.id} -- ${JSON.stringify({ ...asset, id: undefined })}`);
await this.assetRepository.update(asset);
}
}

View File

@@ -1,31 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PluginLike } from 'src/interfaces/plugin.interface';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class WorkflowService extends BaseService {
private plugins?: PluginLike[];
async init(): Promise<void> {
const activePlugins = await this.pluginRepository.search({ isEnabled: true });
const installPaths = activePlugins.map((p) => p.requirePath).filter(Boolean) as string[];
this.plugins = await Promise.all(installPaths.map((path) => this.pluginRepository.load(path!)));
}
// async register() {
// const plugins = ['/src/abc'];
// for (const pluginModule of plugins) {
// // eslint-disable-next-line @typescript-eslint/no-var-requires
// try {
// const plugin: Plugin = ;
// const actions = await plugin.register();
// for (const action of actions) {
// this.actions[action.id] = action;
// }
// } catch (error) {
// console.error(`Unable to load module: ${pluginModule}`, error);
// continue;
// }
// }
// }
}

View File

@@ -1,12 +0,0 @@
import { AssetDto, EventType, OnAction, PluginConfig } from 'src/interfaces/plugin.interface';
export const createPluginAction = <T extends PluginConfig | undefined = undefined>(options: {
id: string;
name: string;
description: string;
events?: EventType[];
config?: T;
}) => ({
addHandler: (onAction: OnAction<T>) => ({ ...options, onAction }),
onAsset: (onAction: OnAction<T, AssetDto>) => ({ ...options, onAction }),
});

View File

@@ -25,7 +25,6 @@ export enum AppRoute {
ADMIN_STATS = '/admin/server-status',
ADMIN_JOBS = '/admin/jobs-status',
ADMIN_REPAIR = '/admin/repair',
ADMIN_PLUGINS = '/admin/plugins',
ALBUMS = '/albums',
LIBRARIES = '/libraries',

View File

@@ -2,7 +2,7 @@
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { AppRoute } from '$lib/constants';
import { NavbarItem } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiConnection, mdiServer, mdiSync } from '@mdi/js';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js';
import { t } from 'svelte-i18n';
</script>
@@ -10,7 +10,6 @@
<div class="flex flex-col pt-8 pe-4 gap-1">
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
<NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} />
<NavbarItem title={$t('plugins')} href={AppRoute.ADMIN_PLUGINS} icon={mdiConnection} />
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import { Button, Icon, Switch } from '@immich/ui';
import { mdiCheckDecagram, mdiWrench } from '@mdi/js';
import { range } from 'lodash-es';
import type { PageData } from './$types';
type Props = {
data: PageData;
};
const { data }: Props = $props();
const plugins = range(0, 8).map((index) => ({
name: `Plugin-${index}`,
description: `Plugin ${index} is awesome because it can do x and even y!`,
isEnabled: Math.random() < 0.5,
isInstalled: Math.random() < 0.5,
isOfficial: Math.random() < 0.5,
version: 1,
}));
</script>
<AdminPageLayout title={data.meta.title}>
{#snippet buttons()}
<div class="flex gap-2 items-center justify-center">
<Button leadingIcon={mdiWrench} onclick={() => console.log('clicked')}>Test</Button>
</div>
{/snippet}
<section id="setting-content" class="flex place-content-center sm:mx-4">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
{#each plugins as plugin, i (i)}
<section
class="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4"
>
<div class="flex flex-col gap-2">
<h1 class="m-0 items-start flex gap-2">
{plugin.name}
{#if plugin.isOfficial}
<Icon icon={mdiCheckDecagram} size="18" class="text-success" />
{/if}
<div class="place-self-end justify-self-end justify-end self-end">Version {plugin.version}</div>
</h1>
<p class="m-0 text-sm text-gray-600 dark:text-gray-300">{plugin.description}</p>
</div>
<div class="flex">Is {plugin.isInstalled ? '' : 'not '}installed</div>
<Switch checked={plugin.isEnabled} id={plugin.name} title="Enabled" />
</section>
{/each}
</div>
</section>
</AdminPageLayout>

View File

@@ -1,16 +0,0 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
const plugins = [];
const $t = await getFormatter();
return {
plugins,
meta: {
title: $t('plugins'),
},
};
}) satisfies PageLoad;