diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index f7be93e520..1c82ec45c1 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -461,8 +461,52 @@ select "asset_face".* from "asset_face" + inner join "person" on "asset_face"."faceClusterId" = "person"."faceClusterId" + and "person"."id" = $1 where - "asset_face"."faceClusterId" = $1 + "asset_face"."assetId" in ( + select + "asset"."id" + from + "asset" + where + ( + "asset"."ownerId" = "person"."ownerId" + or exists ( + select + from + "partner" + where + "partner"."sharedById" = "asset"."ownerId" + and "partner"."sharedWithId" = "person"."ownerId" + and ( + $2 = any ("partner"."permissions") + or "partner"."permissions" @> $3 + ) + ) + or exists ( + select + from + "album_asset" + inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId" + and "album_user"."userId" = "person"."ownerId" + where + "album_asset"."assetId" = "asset"."id" + and "album_user"."albumId" in ( + select + "album_user"."albumId" + from + "album_user" + where + "album_user"."userId" = "asset"."ownerId" + and ( + $4 = any ("album_user"."permissions") + or "album_user"."permissions" @> $5 + ) + ) + ) + ) + ) and "asset_face"."deletedAt" is null and "asset_face"."isVisible" is true diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 99947d81c0..52c8a8432e 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -8,6 +8,7 @@ import { SelectQueryBuilder, ShallowDehydrateObject, sql, + StringReference, Updateable, UpdateResult, } from 'kysely'; @@ -215,6 +216,52 @@ export const hasAssetPermissions = ), ]); +export const hasAssetPermissionsRef = ( + eb: ExpressionBuilder, + userIdRef: StringReference, + permissions: SharingPermission[], + ignoreTimelineVisibility: boolean = false, +) => + eb.or([ + eb('asset.ownerId', '=', eb.ref(userIdRef as never)), + eb.exists( + eb + .selectFrom('partner') + .whereRef('partner.sharedById', '=', 'asset.ownerId') + .whereRef('partner.sharedWithId', '=', userIdRef as never) + .where((eb) => + eb.or([ + eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')), + eb('partner.permissions', '@>', eb.val(permissions)), + ]), + ) + .$if(!ignoreTimelineVisibility, (qb) => qb.where('partner.inTimeline', '=', true)), + ), + eb.exists( + eb + .selectFrom('album_asset') + .whereRef('album_asset.assetId', '=', 'asset.id') + .innerJoin('album_user', (join) => + join + .onRef('album_user.albumId', '=', 'album_asset.albumId') + .onRef('album_user.userId', '=', userIdRef as never), + ) + .$if(!ignoreTimelineVisibility, (qb) => qb.where('album_user.inTimeline', '=', true)) + .where('album_user.albumId', 'in', (eb) => + eb + .selectFrom('album_user') + .select('album_user.albumId') + .whereRef('album_user.userId', '=', 'asset.ownerId') + .where((eb) => + eb.or([ + eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')), + eb('album_user.permissions', '@>', eb.val(permissions)), + ]), + ), + ), + ), + ]); + @Injectable() export class AssetRepository { constructor(@InjectKysely() private db: Kysely) {} diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index c55584c33f..26152a0a0d 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely'; import { AssetFace } from 'src/database'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { AssetFileType, AssetVisibility, SharingPermission, SourceType } from 'src/enum'; -import { hasAssetPermissions } from 'src/repositories/asset.repository'; +import { hasAssetPermissions, hasAssetPermissionsRef } from 'src/repositories/asset.repository'; import { DB } from 'src/schema'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; @@ -557,7 +557,10 @@ export class PersonRepository { join.onRef('asset_face.faceClusterId', '=', 'person.faceClusterId').on('person.id', '=', personId), ) .where('asset_face.assetId', 'in', (eb) => - eb.selectFrom('asset').select('asset.id').whereRef('asset.ownerId', '=', 'person.ownerId'), + eb + .selectFrom('asset') + .select('asset.id') + .where((eb) => hasAssetPermissionsRef(eb, 'person.ownerId', [SharingPermission.AssetRead], true)), ) .where('asset_face.deletedAt', 'is', null) .where('asset_face.isVisible', 'is', true) diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index a3204aa57b..02dba95611 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import e from 'express'; import { Insertable, Updateable } from 'kysely'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { Person } from 'src/database'; @@ -557,6 +556,7 @@ export class PersonService extends BaseService { this.logger.warn(`Face ${id} does not have an embedding`); continue; } + if (face.asset.ownerId === userId) { continue; } @@ -566,7 +566,7 @@ export class PersonService extends BaseService { userIds: [face.asset.ownerId], embedding: face.faceSearch.embedding, maxDistance: machineLearning.facialRecognition.maxDistance, - numResults: 10, + numResults: 100, hasPerson: true, minBirthDate: new Date(face.asset.fileCreatedAt), }); @@ -600,7 +600,12 @@ export class PersonService extends BaseService { }); const match = matches.find((match) => match.faceClusterId); - if (match && match.faceClusterId && face.asset.ownerId !== match.ownerId) { + if ( + match && + match.faceClusterId && + face.asset.ownerId !== match.ownerId && + matches.length >= machineLearning.facialRecognition.minFaces + ) { // TODO should probably be a DB constraint? const people = await this.personRepository.getByFaceClusterId(match.faceClusterId);