immich/server/src/services/timeline.service.ts
Jason Rasmussen 11b1aa5ecf
feat: cluster groups (#30739)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-08-20 20:03:00 +02:00

89 lines
3.4 KiB
TypeScript

import { BadRequestException, Injectable } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketsResponseDto } from 'src/dtos/time-bucket.dto';
import { AssetVisibility, Permission } from 'src/enum';
import { TimeBucketOptions } from 'src/repositories/asset.repository';
import { BaseService } from 'src/services/base.service';
import { requireElevatedPermission } from 'src/utils/access';
import { getMyPartnerIds } from 'src/utils/asset.util';
@Injectable()
export class TimelineService extends BaseService {
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketsResponseDto[]> {
await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
return await this.assetRepository.getTimeBuckets(timeBucketOptions, auth);
}
// pre-jsonified response
async getTimeBucket(auth: AuthDto, dto: TimeBucketAssetDto): Promise<string> {
await this.timeBucketChecks(auth, dto);
const timeBucketOptions = await this.buildTimeBucketOptions(auth, { ...dto });
// TODO: use id cursor for pagination
const bucket = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions, auth);
return bucket.assets;
}
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
const { userId, ...options } = dto;
let userIds: string[] | undefined;
if (userId) {
userIds = [userId];
if (dto.withPartners) {
const partnerIds = await getMyPartnerIds({
userId: auth.user.id,
repository: this.partnerRepository,
timelineEnabled: true,
});
userIds.push(...partnerIds);
}
}
return { ...options, userIds };
}
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
if (dto.visibility === AssetVisibility.Locked) {
requireElevatedPermission(auth);
}
if (dto.albumId) {
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] });
} else {
dto.userId ||= auth.user.id;
}
if (dto.userId) {
await this.requireAccess({ auth, permission: Permission.TimelineRead, ids: [dto.userId] });
if (dto.visibility === AssetVisibility.Archive) {
await this.requireAccess({ auth, permission: Permission.ArchiveRead, ids: [dto.userId] });
}
if (dto.visibility === AssetVisibility.Locked && dto.userId !== auth.user.id) {
throw new BadRequestException("You may not access another user's locked timeline");
}
}
if (dto.tagId) {
await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.tagId] });
}
if (auth.sharedLink && !auth.sharedLink.showExif) {
dto.withCoordinates = false;
}
if (dto.withPartners) {
const isRequestedLocked = dto.visibility === AssetVisibility.Locked;
const isRequestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
const isRequestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
const isRequestedTrash = dto.isTrashed === true;
if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) {
throw new BadRequestException(
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
);
}
}
}
}