"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var NotificationService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NotificationService = void 0;
const common_1 = require("@nestjs/common");
const decorators_1 = require("../decorators");
const asset_response_dto_1 = require("../dtos/asset-response.dto");
const notification_dto_1 = require("../dtos/notification.dto");
const enum_1 = require("../enum");
const email_repository_1 = require("../repositories/email.repository");
const base_service_1 = require("./base.service");
const file_1 = require("../utils/file");
const misc_1 = require("../utils/misc");
const object_1 = require("../utils/object");
const preferences_1 = require("../utils/preferences");
let NotificationService = class NotificationService extends base_service_1.BaseService {
    static { NotificationService_1 = this; }
    static albumUpdateEmailDelayMs = 300_000;
    async search(auth, dto) {
        const items = await this.notificationRepository.search(auth.user.id, dto);
        return items.map((item) => (0, notification_dto_1.mapNotification)(item));
    }
    async updateAll(auth, dto) {
        await this.requireAccess({ auth, ids: dto.ids, permission: enum_1.Permission.NotificationUpdate });
        await this.notificationRepository.updateAll(dto.ids, {
            readAt: dto.readAt,
        });
    }
    async deleteAll(auth, dto) {
        await this.requireAccess({ auth, ids: dto.ids, permission: enum_1.Permission.NotificationDelete });
        await this.notificationRepository.deleteAll(dto.ids);
    }
    async get(auth, id) {
        await this.requireAccess({ auth, ids: [id], permission: enum_1.Permission.NotificationRead });
        const item = await this.notificationRepository.get(id);
        if (!item) {
            throw new common_1.BadRequestException('Notification not found');
        }
        return (0, notification_dto_1.mapNotification)(item);
    }
    async update(auth, id, dto) {
        await this.requireAccess({ auth, ids: [id], permission: enum_1.Permission.NotificationUpdate });
        const item = await this.notificationRepository.update(id, {
            readAt: dto.readAt,
        });
        return (0, notification_dto_1.mapNotification)(item);
    }
    async delete(auth, id) {
        await this.requireAccess({ auth, ids: [id], permission: enum_1.Permission.NotificationDelete });
        await this.notificationRepository.delete(id);
    }
    async onNotificationsCleanup() {
        await this.notificationRepository.cleanup();
    }
    async onJobError({ job, error }) {
        const admin = await this.userRepository.getAdmin();
        if (!admin) {
            return;
        }
        this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data));
        switch (job.name) {
            case enum_1.JobName.DatabaseBackup: {
                const errorMessage = error instanceof Error ? error.message : error;
                const item = await this.notificationRepository.create({
                    userId: admin.id,
                    type: enum_1.NotificationType.JobFailed,
                    level: enum_1.NotificationLevel.Error,
                    title: 'Job Failed',
                    description: `Job ${[job.name]} failed with error: ${errorMessage}`,
                });
                this.websocketRepository.clientSend('on_notification', admin.id, (0, notification_dto_1.mapNotification)(item));
                break;
            }
            default: {
                return;
            }
        }
    }
    onConfigUpdate({ oldConfig, newConfig }) {
        this.websocketRepository.clientBroadcast('on_config_update');
        this.websocketRepository.serverSend('ConfigUpdate', { oldConfig, newConfig });
    }
    onAppRestart(state) {
        this.websocketRepository.clientBroadcast('AppRestartV1', {
            isMaintenanceMode: state.isMaintenanceMode,
        });
        this.websocketRepository.serverSend('AppRestart', state);
    }
    async onConfigValidate({ oldConfig, newConfig }) {
        try {
            if (newConfig.notifications.smtp.enabled &&
                !(0, object_1.isEqualObject)(oldConfig.notifications.smtp, newConfig.notifications.smtp)) {
                await this.emailRepository.verifySmtp(newConfig.notifications.smtp.transport);
            }
        }
        catch (error) {
            this.logger.error(`Failed to validate SMTP configuration: ${error}`, error?.stack);
            throw new Error('Invalid SMTP configuration', { cause: error });
        }
    }
    onAssetHide({ assetId, userId }) {
        this.websocketRepository.clientSend('on_asset_hidden', userId, assetId);
    }
    async onAssetShow({ assetId }) {
        await this.jobRepository.queue({ name: enum_1.JobName.AssetGenerateThumbnails, data: { id: assetId, notify: true } });
    }
    onAssetTrash({ assetId, userId }) {
        this.websocketRepository.clientSend('on_asset_trash', userId, [assetId]);
    }
    onAssetDelete({ assetId, userId }) {
        this.websocketRepository.clientSend('on_asset_delete', userId, assetId);
    }
    onAssetsTrash({ assetIds, userId }) {
        this.websocketRepository.clientSend('on_asset_trash', userId, assetIds);
    }
    async onAssetMetadataExtracted({ assetId, userId, source }) {
        if (source !== 'sidecar-write') {
            return;
        }
        const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]);
        if (asset) {
            this.websocketRepository.clientSend('on_asset_update', userId, (0, asset_response_dto_1.mapAsset)(asset, { auth: { user: { id: userId } } }));
        }
    }
    onAssetsRestore({ assetIds, userId }) {
        this.websocketRepository.clientSend('on_asset_restore', userId, assetIds);
    }
    onStackCreate({ userId }) {
        this.websocketRepository.clientSend('on_asset_stack_update', userId);
    }
    onStackUpdate({ userId }) {
        this.websocketRepository.clientSend('on_asset_stack_update', userId);
    }
    onStackDelete({ userId }) {
        this.websocketRepository.clientSend('on_asset_stack_update', userId);
    }
    onStacksDelete({ userId }) {
        this.websocketRepository.clientSend('on_asset_stack_update', userId);
    }
    async onUserSignup({ notify, id, password: password }) {
        if (notify) {
            await this.jobRepository.queue({ name: enum_1.JobName.NotifyUserSignup, data: { id, password } });
        }
    }
    onUserDelete({ id }) {
        this.websocketRepository.clientBroadcast('on_user_delete', id);
    }
    async onAlbumUpdate({ id, recipientId }) {
        await this.jobRepository.removeJob(enum_1.JobName.NotifyAlbumUpdate, `${id}/${recipientId}`);
        await this.jobRepository.queue({
            name: enum_1.JobName.NotifyAlbumUpdate,
            data: { id, recipientId, delay: NotificationService_1.albumUpdateEmailDelayMs },
        });
    }
    async onAlbumInvite({ id, userId, senderName }) {
        await this.jobRepository.queue({ name: enum_1.JobName.NotifyAlbumInvite, data: { id, recipientId: userId, senderName } });
    }
    onSessionDelete({ sessionId }) {
        setTimeout(() => this.websocketRepository.clientSend('on_session_delete', sessionId, sessionId), 500);
    }
    async sendTestEmail(id, dto, tempTemplate) {
        const user = await this.userRepository.get(id, { withDeleted: false });
        if (!user) {
            throw new Error('User not found');
        }
        try {
            await this.emailRepository.verifySmtp(dto.transport);
        }
        catch (error) {
            throw new common_1.BadRequestException('Failed to verify SMTP configuration', { cause: error });
        }
        const { server } = await this.getConfig({ withCache: false });
        const { html, text } = await this.emailRepository.renderEmail({
            template: email_repository_1.EmailTemplate.TEST_EMAIL,
            data: {
                baseUrl: (0, misc_1.getExternalDomain)(server),
                displayName: user.name,
            },
            customTemplate: tempTemplate,
        });
        const { messageId } = await this.emailRepository.sendEmail({
            to: user.email,
            subject: 'Test email from Immich',
            html,
            text,
            from: dto.from,
            replyTo: dto.replyTo || dto.from,
            smtp: dto.transport,
        });
        return { messageId };
    }
    async handleUserSignup({ id, password }) {
        const user = await this.userRepository.get(id, { withDeleted: false });
        if (!user) {
            return enum_1.JobStatus.Skipped;
        }
        const { server, templates } = await this.getConfig({ withCache: true });
        const { html, text } = await this.emailRepository.renderEmail({
            template: email_repository_1.EmailTemplate.WELCOME,
            data: {
                baseUrl: (0, misc_1.getExternalDomain)(server),
                displayName: user.name,
                username: user.email,
                password,
            },
            customTemplate: templates.email.welcomeTemplate,
        });
        await this.jobRepository.queue({
            name: enum_1.JobName.SendMail,
            data: {
                to: user.email,
                subject: 'Welcome to Immich',
                html,
                text,
            },
        });
        return enum_1.JobStatus.Success;
    }
    async handleAlbumInvite({ id, recipientId, senderName }) {
        const album = await this.albumRepository.getById(id, { withAssets: false });
        if (!album) {
            return enum_1.JobStatus.Skipped;
        }
        const recipient = await this.userRepository.get(recipientId, { withDeleted: false });
        if (!recipient) {
            return enum_1.JobStatus.Skipped;
        }
        await this.sendAlbumLocalNotification(album, recipientId, enum_1.NotificationType.AlbumInvite, senderName);
        const { emailNotifications } = (0, preferences_1.getPreferences)(recipient.metadata);
        if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
            return enum_1.JobStatus.Skipped;
        }
        const attachment = await this.getAlbumThumbnailAttachment(album);
        const { server, templates } = await this.getConfig({ withCache: false });
        const { html, text } = await this.emailRepository.renderEmail({
            template: email_repository_1.EmailTemplate.ALBUM_INVITE,
            data: {
                baseUrl: (0, misc_1.getExternalDomain)(server),
                albumId: album.id,
                albumName: album.albumName,
                senderName,
                recipientName: recipient.name,
                cid: attachment ? attachment.cid : undefined,
            },
            customTemplate: templates.email.albumInviteTemplate,
        });
        await this.jobRepository.queue({
            name: enum_1.JobName.SendMail,
            data: {
                to: recipient.email,
                subject: `You have been added to a shared album - ${album.albumName}`,
                html,
                text,
                imageAttachments: attachment ? [attachment] : undefined,
            },
        });
        return enum_1.JobStatus.Success;
    }
    async handleAlbumUpdate({ id, recipientId }) {
        const album = await this.albumRepository.getById(id, { withAssets: false });
        if (!album) {
            return enum_1.JobStatus.Skipped;
        }
        const recipient = await this.userRepository.get(recipientId, { withDeleted: false });
        if (!recipient) {
            return enum_1.JobStatus.Skipped;
        }
        await this.sendAlbumLocalNotification(album, recipientId, enum_1.NotificationType.AlbumUpdate);
        const attachment = await this.getAlbumThumbnailAttachment(album);
        const { server, templates } = await this.getConfig({ withCache: false });
        const user = await this.userRepository.get(recipientId, { withDeleted: false });
        if (!user) {
            return enum_1.JobStatus.Skipped;
        }
        const { emailNotifications } = (0, preferences_1.getPreferences)(user.metadata);
        if (!emailNotifications.enabled || !emailNotifications.albumUpdate) {
            return enum_1.JobStatus.Skipped;
        }
        const { html, text } = await this.emailRepository.renderEmail({
            template: email_repository_1.EmailTemplate.ALBUM_UPDATE,
            data: {
                baseUrl: (0, misc_1.getExternalDomain)(server),
                albumId: album.id,
                albumName: album.albumName,
                recipientName: user.name,
                cid: attachment ? attachment.cid : undefined,
            },
            customTemplate: templates.email.albumUpdateTemplate,
        });
        await this.jobRepository.queue({
            name: enum_1.JobName.SendMail,
            data: {
                to: user.email,
                subject: `New media has been added to an album - ${album.albumName}`,
                html,
                text,
                imageAttachments: attachment ? [attachment] : undefined,
            },
        });
        return enum_1.JobStatus.Success;
    }
    async handleSendEmail(data) {
        const { notifications } = await this.getConfig({ withCache: false });
        if (!notifications.smtp.enabled) {
            return enum_1.JobStatus.Skipped;
        }
        const { to, subject, html, text: plain } = data;
        const response = await this.emailRepository.sendEmail({
            to,
            subject,
            html,
            text: plain,
            from: notifications.smtp.from,
            replyTo: notifications.smtp.replyTo || notifications.smtp.from,
            smtp: notifications.smtp.transport,
            imageAttachments: data.imageAttachments,
        });
        this.logger.log(`Sent mail with id: ${response.messageId} status: ${response.response}`);
        return enum_1.JobStatus.Success;
    }
    async getAlbumThumbnailAttachment(album) {
        if (!album.albumThumbnailAssetId) {
            return;
        }
        const albumThumbnailFiles = await this.assetJobRepository.getAlbumThumbnailFiles(album.albumThumbnailAssetId, enum_1.AssetFileType.Thumbnail);
        if (albumThumbnailFiles.length !== 1) {
            return;
        }
        return {
            filename: `album-thumbnail${(0, file_1.getFilenameExtension)(albumThumbnailFiles[0].path)}`,
            path: albumThumbnailFiles[0].path,
            cid: 'album-thumbnail',
        };
    }
    async sendAlbumLocalNotification(album, userId, type, senderName) {
        const isInvite = type === enum_1.NotificationType.AlbumInvite;
        const item = await this.notificationRepository.create({
            userId,
            type,
            level: isInvite ? enum_1.NotificationLevel.Success : enum_1.NotificationLevel.Info,
            title: isInvite ? 'Shared Album Invitation' : 'Shared Album Update',
            description: isInvite
                ? `${senderName} shared an album (${album.albumName}) with you`
                : `New media has been added to the album (${album.albumName})`,
            data: JSON.stringify({ albumId: album.id }),
        });
        this.websocketRepository.clientSend('on_notification', userId, (0, notification_dto_1.mapNotification)(item));
    }
};
exports.NotificationService = NotificationService;
__decorate([
    (0, decorators_1.OnJob)({ name: enum_1.JobName.NotificationsCleanup, queue: enum_1.QueueName.BackgroundTask }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onNotificationsCleanup", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'JobError' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onJobError", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'ConfigUpdate' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onConfigUpdate", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AppRestart' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onAppRestart", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'ConfigValidate', priority: -100 }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onConfigValidate", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AssetHide' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onAssetHide", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AssetShow' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onAssetShow", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AssetTrash' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onAssetTrash", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AssetDelete' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onAssetDelete", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AssetTrashAll' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onAssetsTrash", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AssetMetadataExtracted' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onAssetMetadataExtracted", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AssetRestoreAll' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onAssetsRestore", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'StackCreate' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onStackCreate", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'StackUpdate' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onStackUpdate", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'StackDelete' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onStackDelete", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'StackDeleteAll' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onStacksDelete", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'UserSignup' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onUserSignup", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'UserDelete' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onUserDelete", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AlbumUpdate' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onAlbumUpdate", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'AlbumInvite' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "onAlbumInvite", null);
__decorate([
    (0, decorators_1.OnEvent)({ name: 'SessionDelete' }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", void 0)
], NotificationService.prototype, "onSessionDelete", null);
__decorate([
    (0, decorators_1.OnJob)({ name: enum_1.JobName.NotifyUserSignup, queue: enum_1.QueueName.Notification }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "handleUserSignup", null);
__decorate([
    (0, decorators_1.OnJob)({ name: enum_1.JobName.NotifyAlbumInvite, queue: enum_1.QueueName.Notification }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "handleAlbumInvite", null);
__decorate([
    (0, decorators_1.OnJob)({ name: enum_1.JobName.NotifyAlbumUpdate, queue: enum_1.QueueName.Notification }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "handleAlbumUpdate", null);
__decorate([
    (0, decorators_1.OnJob)({ name: enum_1.JobName.SendMail, queue: enum_1.QueueName.Notification }),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], NotificationService.prototype, "handleSendEmail", null);
exports.NotificationService = NotificationService = NotificationService_1 = __decorate([
    (0, common_1.Injectable)()
], NotificationService);
//# sourceMappingURL=notification.service.js.map