1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-01-10 04:07:35 +02:00

Refactoring messenger to prepare extension support #753

This commit is contained in:
Patrik J. Braun 2023-11-18 16:26:42 +01:00
parent 7208a3b4fe
commit 50b8f7a81d
19 changed files with 370 additions and 181 deletions

View File

@ -2,18 +2,19 @@ import {NextFunction, Request, Response} from 'express';
import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error'; import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error';
import {ObjectManagers} from '../../model/ObjectManagers'; import {ObjectManagers} from '../../model/ObjectManagers';
import {StatisticDTO} from '../../../common/entities/settings/StatisticDTO'; import {StatisticDTO} from '../../../common/entities/settings/StatisticDTO';
import {MessengerRepository} from '../../model/messenger/MessengerRepository';
export class AdminMWs { export class AdminMWs {
public static async loadStatistic( public static async loadStatistic(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
const galleryManager = ObjectManagers.getInstance() const galleryManager = ObjectManagers.getInstance()
.GalleryManager; .GalleryManager;
const personManager = ObjectManagers.getInstance() const personManager = ObjectManagers.getInstance()
.PersonManager; .PersonManager;
try { try {
req.resultPipe = { req.resultPipe = {
directories: await galleryManager.countDirectories(), directories: await galleryManager.countDirectories(),
@ -26,87 +27,87 @@ export class AdminMWs {
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error while getting statistic: ' + err.toString(), 'Error while getting statistic: ' + err.toString(),
err err
) )
); );
} }
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error while getting statistic', 'Error while getting statistic',
err err
) )
); );
} }
} }
public static async getDuplicates( public static async getDuplicates(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
try { try {
req.resultPipe = await ObjectManagers.getInstance() req.resultPipe = await ObjectManagers.getInstance()
.GalleryManager.getPossibleDuplicates(); .GalleryManager.getPossibleDuplicates();
return next(); return next();
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error while getting duplicates: ' + err.toString(), 'Error while getting duplicates: ' + err.toString(),
err err
) )
); );
} }
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.GENERAL_ERROR, ErrorCodes.GENERAL_ERROR,
'Error while getting duplicates', 'Error while getting duplicates',
err err
) )
); );
} }
} }
public static async startJob( public static async startJob(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
try { try {
const id = req.params['id']; const id = req.params['id'];
const JobConfig: unknown = req.body.config; const JobConfig: Record<string, unknown> = req.body.config;
const soloRun: boolean = req.body.soloRun; const soloRun: boolean = req.body.soloRun;
const allowParallelRun: boolean = req.body.allowParallelRun; const allowParallelRun: boolean = req.body.allowParallelRun;
await ObjectManagers.getInstance().JobManager.run( await ObjectManagers.getInstance().JobManager.run(
id, id,
JobConfig, JobConfig,
soloRun, soloRun,
allowParallelRun allowParallelRun
); );
req.resultPipe = 'ok'; req.resultPipe = 'ok';
return next(); return next();
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + err.toString(), 'Job error: ' + err.toString(),
err err
) )
); );
} }
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + JSON.stringify(err, null, ' '), 'Job error: ' + JSON.stringify(err, null, ' '),
err err
) )
); );
} }
} }
@ -120,56 +121,85 @@ export class AdminMWs {
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + err.toString(), 'Job error: ' + err.toString(),
err err
) )
); );
} }
return next( return next(
new ErrorDTO(
ErrorCodes.JOB_ERROR,
'Job error: ' + JSON.stringify(err, null, ' '),
err
)
);
}
}
public static getAvailableMessengers(
req: Request,
res: Response,
next: NextFunction
): void {
try {
req.resultPipe = MessengerRepository.Instance.getAll().map(msgr => msgr.Name);
return next();
} catch (err) {
if (err instanceof Error) {
return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + JSON.stringify(err, null, ' '), 'Messenger error: ' + err.toString(),
err err
) )
);
}
return next(
new ErrorDTO(
ErrorCodes.JOB_ERROR,
'Messenger error: ' + JSON.stringify(err, null, ' '),
err
)
); );
} }
} }
public static getAvailableJobs( public static getAvailableJobs(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): void { ): void {
try { try {
req.resultPipe = req.resultPipe =
ObjectManagers.getInstance().JobManager.getAvailableJobs(); ObjectManagers.getInstance().JobManager.getAvailableJobs();
return next(); return next();
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + err.toString(), 'Job error: ' + err.toString(),
err err
) )
); );
} }
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + JSON.stringify(err, null, ' '), 'Job error: ' + JSON.stringify(err, null, ' '),
err err
) )
); );
} }
} }
public static getJobProgresses( public static getJobProgresses(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): void { ): void {
try { try {
req.resultPipe = ObjectManagers.getInstance().JobManager.getProgresses(); req.resultPipe = ObjectManagers.getInstance().JobManager.getProgresses();
@ -177,19 +207,19 @@ export class AdminMWs {
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + err.toString(), 'Job error: ' + err.toString(),
err err
) )
); );
} }
return next( return next(
new ErrorDTO( new ErrorDTO(
ErrorCodes.JOB_ERROR, ErrorCodes.JOB_ERROR,
'Job error: ' + JSON.stringify(err, null, ' '), 'Job error: ' + JSON.stringify(err, null, ' '),
err err
) )
); );
} }
} }

View File

@ -50,7 +50,7 @@ export class JobManager implements IJobListener, IObjectManager {
return prg; return prg;
} }
public async run<T>( public async run<T extends Record<string, unknown>>(
jobName: string, jobName: string,
config: T, config: T,
soloRun: boolean, soloRun: boolean,
@ -86,7 +86,7 @@ export class JobManager implements IJobListener, IObjectManager {
}; };
onJobFinished = async ( onJobFinished = async (
job: IJob<unknown>, job: IJob,
state: JobProgressStates, state: JobProgressStates,
soloRun: boolean soloRun: boolean
): Promise<void> => { ): Promise<void> => {
@ -121,7 +121,7 @@ export class JobManager implements IJobListener, IObjectManager {
} }
}; };
getAvailableJobs(): IJob<unknown>[] { getAvailableJobs(): IJob[] {
return JobRepository.Instance.getAvailableJobs(); return JobRepository.Instance.getAvailableJobs();
} }
@ -144,7 +144,7 @@ export class JobManager implements IJobListener, IObjectManager {
Config.Jobs.scheduled.forEach((s): void => this.runSchedule(s)); Config.Jobs.scheduled.forEach((s): void => this.runSchedule(s));
} }
protected findJob<T = unknown>(jobName: string): IJob<T> { protected findJob(jobName: string): IJob {
return this.getAvailableJobs().find((t): boolean => t.Name === jobName); return this.getAvailableJobs().find((t): boolean => t.Name === jobName);
} }

View File

@ -14,7 +14,7 @@ import {AlbumCoverRestJob} from './jobs/AlbumCoverResetJob';
export class JobRepository { export class JobRepository {
private static instance: JobRepository = null; private static instance: JobRepository = null;
availableJobs: { [key: string]: IJob<unknown> } = {}; availableJobs: { [key: string]: IJob } = {};
public static get Instance(): JobRepository { public static get Instance(): JobRepository {
if (JobRepository.instance == null) { if (JobRepository.instance == null) {
@ -23,11 +23,11 @@ export class JobRepository {
return JobRepository.instance; return JobRepository.instance;
} }
getAvailableJobs(): IJob<unknown>[] { getAvailableJobs(): IJob[] {
return Object.values(this.availableJobs).filter((t) => t.Supported); return Object.values(this.availableJobs).filter((t) => t.Supported);
} }
register(job: IJob<unknown>): void { register(job: IJob): void {
if (typeof this.availableJobs[job.Name] !== 'undefined') { if (typeof this.availableJobs[job.Name] !== 'undefined') {
throw new Error('Job already exist:' + job.Name); throw new Error('Job already exist:' + job.Name);
} }

View File

@ -2,7 +2,7 @@ import {JobDTO} from '../../../../common/entities/job/JobDTO';
import {JobProgress} from './JobProgress'; import {JobProgress} from './JobProgress';
import {IJobListener} from './IJobListener'; import {IJobListener} from './IJobListener';
export interface IJob<T> extends JobDTO { export interface IJob<T extends Record<string, unknown> = Record<string, unknown>> extends JobDTO {
Name: string; Name: string;
Supported: boolean; Supported: boolean;
Progress: JobProgress; Progress: JobProgress;

View File

@ -4,7 +4,7 @@ import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO'
export interface IJobListener { export interface IJobListener {
onJobFinished( onJobFinished(
job: IJob<unknown>, job: IJob,
state: JobProgressStates, state: JobProgressStates,
soloRun: boolean soloRun: boolean
): void; ): void;

View File

@ -35,7 +35,7 @@ export class ThumbnailGenerationJob extends FileJob<{
): Promise<void> { ): Promise<void> {
if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) { if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) {
config = config || {}; config = config || {};
config.sizes = this.ConfigTemplate.find(ct => ct.id == 'sizes').defaultValue; config.sizes = this.ConfigTemplate.find(ct => ct.id == 'sizes').defaultValue as number[];
} }
for (const item of config.sizes) { for (const item of config.sizes) {
if (Config.Media.Thumbnail.thumbnailSizes.indexOf(item) === -1) { if (Config.Media.Thumbnail.thumbnailSizes.indexOf(item) === -1) {

View File

@ -1,64 +1,67 @@
import {ConfigTemplateEntry, DefaultsJobs,} from '../../../../common/entities/job/JobDTO'; import {DefaultMessengers, DefaultsJobs,} from '../../../../common/entities/job/JobDTO';
import {Job} from './Job'; import {Job} from './Job';
import {backendTexts} from '../../../../common/BackendTexts'; import {backendTexts} from '../../../../common/BackendTexts';
import {SortByTypes} from '../../../../common/entities/SortingMethods'; import {SortByTypes} from '../../../../common/entities/SortingMethods';
import {DatePatternFrequency, DatePatternSearch, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO'; import {DatePatternFrequency, DatePatternSearch, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
import {ObjectManagers} from '../../ObjectManagers'; import {ObjectManagers} from '../../ObjectManagers';
import {PhotoEntity} from '../../database/enitites/PhotoEntity'; import {PhotoEntity} from '../../database/enitites/PhotoEntity';
import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger';
import {MediaPickDTO} from '../../../../common/entities/MediaPickDTO'; import {MediaPickDTO} from '../../../../common/entities/MediaPickDTO';
import {MediaDTOUtils} from '../../../../common/entities/MediaDTO'; import {MediaDTOUtils} from '../../../../common/entities/MediaDTO';
import {DynamicConfig} from '../../../../common/entities/DynamicConfig'; import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
import {MessengerRepository} from '../../messenger/MessengerRepository';
import {Utils} from '../../../../common/Utils';
export class TopPickSendJob extends Job<{ export class TopPickSendJob extends Job<{
mediaPick: MediaPickDTO[], mediaPick: MediaPickDTO[],
messenger: string,
emailTo: string, emailTo: string,
emailFrom: string,
emailSubject: string, emailSubject: string,
emailText: string, emailText: string,
}> { }> {
public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']]; public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']];
public readonly Supported: boolean = true; public readonly Supported: boolean = true;
public readonly ConfigTemplate: DynamicConfig[] = [ public readonly ConfigTemplate: DynamicConfig[];
{
id: 'mediaPick',
type: 'MediaPickDTO-array',
name: backendTexts.mediaPick.name,
description: backendTexts.mediaPick.description,
defaultValue: [{
searchQuery: {
type: SearchQueryTypes.date_pattern,
daysLength: 7,
frequency: DatePatternFrequency.every_year
} as DatePatternSearch,
sortBy: [{method: SortByTypes.Rating, ascending: false},
{method: SortByTypes.PersonCount, ascending: false}],
pick: 5
}] as MediaPickDTO[],
}, {
id: 'emailTo',
type: 'string-array',
name: backendTexts.emailTo.name,
description: backendTexts.emailTo.description,
defaultValue: [],
}, {
id: 'emailSubject',
type: 'string',
name: backendTexts.emailSubject.name,
description: backendTexts.emailSubject.description,
defaultValue: 'Latest photos for you',
}, {
id: 'emailText',
type: 'string',
name: backendTexts.emailText.name,
description: backendTexts.emailText.description,
defaultValue: 'I hand picked these photos just for you:',
},
];
private status: 'Listing' | 'Sending' = 'Listing'; private status: 'Listing' | 'Sending' = 'Listing';
private mediaList: PhotoEntity[] = []; private mediaList: PhotoEntity[] = [];
constructor() {
super();
this.ConfigTemplate = [
{
id: 'mediaPick',
type: 'MediaPickDTO-array',
name: backendTexts.mediaPick.name,
description: backendTexts.mediaPick.description,
defaultValue: [{
searchQuery: {
type: SearchQueryTypes.date_pattern,
daysLength: 7,
frequency: DatePatternFrequency.every_year
} as DatePatternSearch,
sortBy: [{method: SortByTypes.Rating, ascending: false},
{method: SortByTypes.PersonCount, ascending: false}],
pick: 5
}] as MediaPickDTO[],
}, {
id: 'messenger',
type: 'messenger',
name: backendTexts.messenger.name,
description: backendTexts.messenger.description,
defaultValue: DefaultMessengers[DefaultMessengers.Email]
}
];
// add all messenger's config to the config template
MessengerRepository.Instance.getAll()
.forEach(msgr => Utils.clone(msgr.ConfigTemplate)
.forEach(ct => {
const c = Utils.clone(ct);
c.validIf = {configFiled: 'messenger', equalsValue: msgr.Name};
this.ConfigTemplate.push(c);
}));
}
protected async init(): Promise<void> { protected async init(): Promise<void> {
this.status = 'Listing'; this.status = 'Listing';
@ -86,15 +89,15 @@ export class TopPickSendJob extends Job<{
this.mediaList = []; this.mediaList = [];
for (let i = 0; i < this.config.mediaPick.length; ++i) { for (let i = 0; i < this.config.mediaPick.length; ++i) {
const media = await ObjectManagers.getInstance().SearchManager const media = await ObjectManagers.getInstance().SearchManager
.getNMedia(this.config.mediaPick[i].searchQuery, this.config.mediaPick[i].sortBy, this.config.mediaPick[i].pick); .getNMedia(this.config.mediaPick[i].searchQuery, this.config.mediaPick[i].sortBy, this.config.mediaPick[i].pick);
this.Progress.log('Find ' + media.length + ' photos and videos from ' + (i + 1) + '. load'); this.Progress.log('Find ' + media.length + ' photos and videos from ' + (i + 1) + '. load');
this.mediaList = this.mediaList.concat(media); this.mediaList = this.mediaList.concat(media);
} }
// make the list unique // make the list unique
this.mediaList = this.mediaList this.mediaList = this.mediaList
.filter((value, index, arr) => .filter((value, index, arr) =>
arr.findIndex(m => MediaDTOUtils.equals(m, value)) === index); arr.findIndex(m => MediaDTOUtils.equals(m, value)) === index);
this.Progress.Processed++; this.Progress.Processed++;
// console.log(this.mediaList); // console.log(this.mediaList);
@ -103,17 +106,16 @@ export class TopPickSendJob extends Job<{
private async stepSending(): Promise<boolean> { private async stepSending(): Promise<boolean> {
if (this.mediaList.length <= 0) { if (this.mediaList.length <= 0) {
this.Progress.log('No photos found skipping e-mail sending.'); this.Progress.log('No photos found skipping sending.');
this.Progress.Skipped++; this.Progress.Skipped++;
return false; return false;
} }
this.Progress.log('Sending emails of ' + this.mediaList.length + ' photos.'); const msgr = MessengerRepository.Instance.get(this.config.messenger);
const messenger = new EmailMediaMessenger(); if (!msgr) {
await messenger.sendMedia({ throw new Error('Can\t find "' + this.config.messenger + '" messenger.');
to: this.config.emailTo, }
subject: this.config.emailSubject, this.Progress.log('Sending ' + this.mediaList.length + ' photos.');
text: this.config.emailText await msgr.send(this.config, this.mediaList);
}, this.mediaList);
this.Progress.Processed++; this.Progress.Processed++;
return false; return false;
} }

View File

@ -1,18 +1,40 @@
import {createTransport, Transporter} from 'nodemailer'; import {createTransport, Transporter} from 'nodemailer';
import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO';
import {Config} from '../../../common/config/private/Config'; import {Config} from '../../../common/config/private/Config';
import {PhotoProcessing} from '../fileaccess/fileprocessing/PhotoProcessing';
import {ThumbnailSourceType} from '../fileaccess/PhotoWorker';
import {ProjectPath} from '../../ProjectPath';
import * as path from 'path';
import {PhotoMetadata} from '../../../common/entities/PhotoDTO'; import {PhotoMetadata} from '../../../common/entities/PhotoDTO';
import {Utils} from '../../../common/Utils'; import {MediaDTOWithThPath, Messenger} from './Messenger';
import {QueryParams} from '../../../common/QueryParams'; import {backendTexts} from '../../../common/BackendTexts';
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
import {DefaultMessengers} from '../../../common/entities/job/JobDTO';
export class EmailMediaMessenger { export class EmailMessenger extends Messenger<{
emailTo: string,
emailSubject: string,
emailText: string,
}> {
public readonly Name = DefaultMessengers[DefaultMessengers.Email];
public readonly ConfigTemplate: DynamicConfig[] = [{
id: 'emailTo',
type: 'string-array',
name: backendTexts.emailTo.name,
description: backendTexts.emailTo.description,
defaultValue: [],
}, {
id: 'emailSubject',
type: 'string',
name: backendTexts.emailSubject.name,
description: backendTexts.emailSubject.description,
defaultValue: 'Latest photos for you',
}, {
id: 'emailText',
type: 'string',
name: backendTexts.emailText.name,
description: backendTexts.emailText.description,
defaultValue: 'I hand picked these photos just for you:',
}];
transporter: Transporter; transporter: Transporter;
constructor() { constructor() {
super();
this.transporter = createTransport({ this.transporter = createTransport({
host: Config.Messaging.Email.smtp.host, host: Config.Messaging.Email.smtp.host,
port: Config.Messaging.Email.smtp.port, port: Config.Messaging.Email.smtp.port,
@ -25,24 +47,16 @@ export class EmailMediaMessenger {
}); });
} }
private async getThumbnail(m: MediaDTO) {
return await PhotoProcessing.generateThumbnail(
path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name),
Config.Media.Thumbnail.thumbnailSizes[0],
MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video,
false
);
}
public async sendMedia(mailSettings: { protected async sendMedia(mailSettings: {
to: string, emailTo: string,
subject: string, emailSubject: string,
text: string emailText: string
}, media: MediaDTO[]) { }, media: MediaDTOWithThPath[]) {
const attachments = []; const attachments = [];
const htmlStart = '<h1 style="text-align: center; margin-bottom: 2em">' + Config.Server.applicationTitle + '</h1>\n' + const htmlStart = '<h1 style="text-align: center; margin-bottom: 2em">' + Config.Server.applicationTitle + '</h1>\n' +
'<h3>' + mailSettings.text + '</h3>\n' + '<h3>' + mailSettings.emailText + '</h3>\n' +
'<table style="margin-left: auto; margin-right: auto;">\n' + '<table style="margin-left: auto; margin-right: auto;">\n' +
' <tbody>\n'; ' <tbody>\n';
const htmlEnd = ' </tr>\n' + const htmlEnd = ' </tr>\n' +
@ -51,9 +65,6 @@ export class EmailMediaMessenger {
let htmlMiddle = ''; let htmlMiddle = '';
const numberOfColumns = media.length >= 6 ? 3 : 2; const numberOfColumns = media.length >= 6 ? 3 : 2;
for (let i = 0; i < media.length; ++i) { for (let i = 0; i < media.length; ++i) {
const thPath = await this.getThumbnail(media[i]);
const linkUrl = Utils.concatUrls(Config.Server.publicUrl, '/gallery/', encodeURIComponent(path.join(media[i].directory.path, media[i].directory.name))) +
'?' + QueryParams.gallery.photo + '=' + encodeURIComponent(media[i].name);
const location = (media[i].metadata as PhotoMetadata).positionData?.country ? const location = (media[i].metadata as PhotoMetadata).positionData?.country ?
(media[i].metadata as PhotoMetadata).positionData?.country : (media[i].metadata as PhotoMetadata).positionData?.country :
((media[i].metadata as PhotoMetadata).positionData?.city ? ((media[i].metadata as PhotoMetadata).positionData?.city ?
@ -61,14 +72,14 @@ export class EmailMediaMessenger {
const caption = (new Date(media[i].metadata.creationDate)).getFullYear() + (location ? ', ' + location : ''); const caption = (new Date(media[i].metadata.creationDate)).getFullYear() + (location ? ', ' + location : '');
attachments.push({ attachments.push({
filename: media[i].name, filename: media[i].name,
path: thPath, path: media[i].thumbnailPath,
cid: 'img' + i cid: 'img' + i
}); });
if (i % numberOfColumns == 0) { if (i % numberOfColumns == 0) {
htmlMiddle += '<tr>'; htmlMiddle += '<tr>';
} }
htmlMiddle += '<td>\n' + htmlMiddle += '<td>\n' +
' <a style="display: block;text-align: center;" href="' + linkUrl + '"><img alt="' + media[i].name + '" style="max-width: 200px; max-height: 150px; height:auto; width:auto;" src="cid:img' + i + '"/></a>\n' + ' <a style="display: block;text-align: center;" href="' + media[i].thumbnailUrl + '"><img alt="' + media[i].name + '" style="max-width: 200px; max-height: 150px; height:auto; width:auto;" src="cid:img' + i + '"/></a>\n' +
caption + caption +
' </td>\n'; ' </td>\n';
@ -79,8 +90,8 @@ export class EmailMediaMessenger {
return await this.transporter.sendMail({ return await this.transporter.sendMail({
from: Config.Messaging.Email.emailFrom, from: Config.Messaging.Email.emailFrom,
to: mailSettings.to, to: mailSettings.emailTo,
subject: mailSettings.subject, subject: mailSettings.emailSubject,
html: htmlStart + htmlMiddle + htmlEnd, html: htmlStart + htmlMiddle + htmlEnd,
attachments: attachments attachments: attachments
}); });

View File

@ -0,0 +1,50 @@
import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO';
import {PhotoProcessing} from '../fileaccess/fileprocessing/PhotoProcessing';
import {ProjectPath} from '../../ProjectPath';
import {Config} from '../../../common/config/private/Config';
import {ThumbnailSourceType} from '../fileaccess/PhotoWorker';
import * as path from 'path';
import {Utils} from '../../../common/Utils';
import {QueryParams} from '../../../common/QueryParams';
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
export interface MediaDTOWithThPath extends MediaDTO {
thumbnailPath: string;
thumbnailUrl: string;
}
export abstract class Messenger<C extends Record<string, unknown> = Record<string, unknown>> {
public abstract get Name(): string;
protected config: C;
public readonly ConfigTemplate: DynamicConfig[] = [];
private async getThumbnail(m: MediaDTO) {
return await PhotoProcessing.generateThumbnail(
path.join(ProjectPath.ImageFolder, m.directory.path, m.directory.name, m.name),
Config.Media.Thumbnail.thumbnailSizes[0],
MediaDTOUtils.isPhoto(m) ? ThumbnailSourceType.Photo : ThumbnailSourceType.Video,
false
);
}
public async send(config: C, input: string | MediaDTO[] | unknown) {
if (Array.isArray(input) && input.length > 0
&& (input as MediaDTO[])[0]?.name
&& (input as MediaDTO[])[0]?.directory
&& (input as MediaDTO[])[0]?.metadata?.creationDate) {
const media = input as MediaDTOWithThPath[];
for (let i = 0; i < media.length; ++i) {
media[i].thumbnailPath = await this.getThumbnail(media[i]);
media[i].thumbnailUrl = Utils.concatUrls(Config.Server.publicUrl, '/gallery/', encodeURIComponent(path.join(media[i].directory.path, media[i].directory.name))) +
'?' + QueryParams.gallery.photo + '=' + encodeURIComponent(media[i].name);
}
return await this.sendMedia(config, media);
}
// TODO: implement other branches
throw new Error('Not yet implemented');
}
protected abstract sendMedia(config: C, media: MediaDTOWithThPath[]): Promise<void> ;
}

View File

@ -0,0 +1,34 @@
import {Messenger} from './Messenger';
import {EmailMessenger} from './EmailMessenger';
import {StdoutMessenger} from './StdoutMessenger';
export class MessengerRepository {
private static instance: MessengerRepository = null;
messengers: { [key: string]: Messenger } = {};
public static get Instance(): MessengerRepository {
if (MessengerRepository.instance == null) {
MessengerRepository.instance = new MessengerRepository();
}
return MessengerRepository.instance;
}
getAll(): Messenger[] {
return Object.values(this.messengers);
}
register(msgr: Messenger): void {
if (typeof this.messengers[msgr.Name] !== 'undefined') {
throw new Error('Messenger already exist:' + msgr.Name);
}
this.messengers[msgr.Name] = msgr;
}
get(name: string): Messenger {
return this.messengers[name];
}
}
MessengerRepository.Instance.register(new EmailMessenger());
MessengerRepository.Instance.register(new StdoutMessenger());

View File

@ -0,0 +1,17 @@
import {MediaDTOWithThPath, Messenger} from './Messenger';
import {DynamicConfig} from '../../../common/entities/DynamicConfig';
import {DefaultMessengers} from '../../../common/entities/job/JobDTO';
export class StdoutMessenger extends Messenger {
public readonly Name = DefaultMessengers[DefaultMessengers.Stdout];
public readonly ConfigTemplate: DynamicConfig[] = [];
constructor() {
super();
}
protected async sendMedia(config: never, media: MediaDTOWithThPath[]) {
console.log(media.map(m => m.thumbnailPath));
}
}

View File

@ -10,6 +10,7 @@ export class AdminRouter {
this.addGetStatistic(app); this.addGetStatistic(app);
this.addGetDuplicates(app); this.addGetDuplicates(app);
this.addJobs(app); this.addJobs(app);
this.addMessengers(app);
} }
private static addGetStatistic(app: Express): void { private static addGetStatistic(app: Express): void {
@ -32,6 +33,15 @@ export class AdminRouter {
); );
} }
private static addMessengers(app: Express): void {
app.get(
Config.Server.apiPath + '/admin/messengers/available',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
AdminMWs.getAvailableMessengers,
RenderingMWs.renderResult
);
}
private static addJobs(app: Express): void { private static addJobs(app: Express): void {
app.get( app.get(
Config.Server.apiPath + '/admin/jobs/available', Config.Server.apiPath + '/admin/jobs/available',

View File

@ -1,4 +1,5 @@
export type backendText = number; export type backendText = number;
// keep the numbering sparse to support later addition
export const backendTexts = { export const backendTexts = {
indexedFilesOnly: {name: 10, description: 12}, indexedFilesOnly: {name: 10, description: 12},
sizeToGenerate: {name: 20, description: 22}, sizeToGenerate: {name: 20, description: 22},
@ -6,6 +7,7 @@ export const backendTexts = {
mediaPick: {name: 40, description: 42}, mediaPick: {name: 40, description: 42},
emailTo: {name: 70, description: 72}, emailTo: {name: 70, description: 72},
emailSubject: {name: 90, description: 92}, emailSubject: {name: 90, description: 92},
emailText: {name: 100, description: 102} emailText: {name: 100, description: 102},
messenger: {name: 110,description: 112}
}; };

View File

@ -1,5 +1,8 @@
import {backendText} from '../BackendTexts'; import {backendText} from '../BackendTexts';
import {fieldType} from './job/JobDTO';
export type fieldType = 'string' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'MediaPickDTO-array' | 'messenger';
/** /**
* Dynamic configs are not part of the typeconfig maintained config. * Dynamic configs are not part of the typeconfig maintained config.
@ -14,4 +17,5 @@ export interface DynamicConfig {
description: backendText | string; description: backendText | string;
type: fieldType; type: fieldType;
defaultValue: unknown; defaultValue: unknown;
validIf?: { configFiled: string, equalsValue: string }; // only shows this config if this predicate is true
} }

View File

@ -1,8 +1,5 @@
import {backendText} from '../../BackendTexts';
import {DynamicConfig} from '../DynamicConfig'; import {DynamicConfig} from '../DynamicConfig';
export type fieldType = 'string' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'MediaPickDTO-array';
export enum DefaultsJobs { export enum DefaultsJobs {
Indexing = 1, Indexing = 1,
'Gallery Reset' = 2, 'Gallery Reset' = 2,
@ -19,6 +16,12 @@ export enum DefaultsJobs {
} }
export enum DefaultMessengers {
Email = 1,
Stdout = 2
}
export interface JobDTO { export interface JobDTO {
Name: string; Name: string;
ConfigTemplate: DynamicConfig[]; ConfigTemplate: DynamicConfig[];

View File

@ -5,7 +5,10 @@ import {DefaultsJobs} from '../../../common/entities/job/JobDTO';
@Injectable() @Injectable()
export class BackendtextService { export class BackendtextService {
public get(id: backendText): string { public get(id: backendText | string): string {
if (typeof id === 'string') {
return id;
}
switch (id) { switch (id) {
case backendTexts.sizeToGenerate.name: case backendTexts.sizeToGenerate.name:
return $localize`Size to generate`; return $localize`Size to generate`;

View File

@ -3,9 +3,10 @@ import {BehaviorSubject} from 'rxjs';
import {JobProgressDTO, JobProgressStates, OnTimerJobProgressDTO,} from '../../../../common/entities/job/JobProgressDTO'; import {JobProgressDTO, JobProgressStates, OnTimerJobProgressDTO,} from '../../../../common/entities/job/JobProgressDTO';
import {NetworkService} from '../../model/network/network.service'; import {NetworkService} from '../../model/network/network.service';
import {JobScheduleDTO} from '../../../../common/entities/job/JobScheduleDTO'; import {JobScheduleDTO} from '../../../../common/entities/job/JobScheduleDTO';
import {ConfigTemplateEntry, JobDTO, JobDTOUtils} from '../../../../common/entities/job/JobDTO'; import {JobDTO, JobDTOUtils} from '../../../../common/entities/job/JobDTO';
import {BackendtextService} from '../../model/backendtext.service'; import {BackendtextService} from '../../model/backendtext.service';
import {NotificationService} from '../../model/notification.service'; import {NotificationService} from '../../model/notification.service';
import {DynamicConfig} from '../../../../common/entities/DynamicConfig';
@Injectable() @Injectable()
export class ScheduledJobsService { export class ScheduledJobsService {
@ -13,6 +14,7 @@ export class ScheduledJobsService {
public onJobFinish: EventEmitter<string> = new EventEmitter<string>(); public onJobFinish: EventEmitter<string> = new EventEmitter<string>();
timer: number = null; timer: number = null;
public availableJobs: BehaviorSubject<JobDTO[]>; public availableJobs: BehaviorSubject<JobDTO[]>;
public availableMessengers: BehaviorSubject<string[]>;
public jobStartingStopping: { [key: string]: boolean } = {}; public jobStartingStopping: { [key: string]: boolean } = {};
private subscribers = 0; private subscribers = 0;
@ -23,6 +25,7 @@ export class ScheduledJobsService {
) { ) {
this.progress = new BehaviorSubject({}); this.progress = new BehaviorSubject({});
this.availableJobs = new BehaviorSubject([]); this.availableJobs = new BehaviorSubject([]);
this.availableMessengers = new BehaviorSubject([]);
} }
@ -32,7 +35,13 @@ export class ScheduledJobsService {
); );
} }
public getConfigTemplate(JobName: string): ConfigTemplateEntry[] { public async getAvailableMessengers(): Promise<void> {
this.availableMessengers.next(
await this.networkService.getJson<string[]>('/admin/messengers/available')
);
}
public getConfigTemplate(JobName: string): DynamicConfig[] {
const job = this.availableJobs.value.find( const job = this.availableJobs.value.find(
(t) => t.Name === JobName (t) => t.Name === JobName
); );

View File

@ -170,6 +170,7 @@
<div *ngFor="let configEntry of jobsService.getConfigTemplate(schedule.jobName)"> <div *ngFor="let configEntry of jobsService.getConfigTemplate(schedule.jobName)">
<div class="mb-1 row" <div class="mb-1 row"
*ngIf="!configEntry.validIf || schedule.config[configEntry.validIf.configFiled] == configEntry.validIf.equalsValue"
[class.mb-3]="settingsService.configStyle == ConfigStyle.full"> [class.mb-3]="settingsService.configStyle == ConfigStyle.full">
<label class="col-md-2 control-label" <label class="col-md-2 control-label"
[for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label> [for]="configEntry.id+'_'+i">{{backendTextService.get(configEntry.name)}}</label>
@ -227,6 +228,18 @@
placeholder="Search Query"> placeholder="Search Query">
</app-gallery-search-field> </app-gallery-search-field>
<select
*ngSwitchCase="'messenger'"
[id]="configEntry.id+'_'+i"
[name]="configEntry.id+'_'+i"
(ngModelChange)="onChange($event)"
[(ngModel)]="schedule.config[configEntry.id]"
class="form-select">
<option *ngFor="let msg of jobsService.availableMessengers | async" [ngValue]="msg">{{msg}}
</option>
</select>
<ng-container *ngSwitchCase="'MediaPickDTO-array'"> <ng-container *ngSwitchCase="'MediaPickDTO-array'">
<ng-container *ngFor="let mp of AsMediaPickDTOArray(schedule.config[configEntry.id]); let j=index"> <ng-container *ngFor="let mp of AsMediaPickDTOArray(schedule.config[configEntry.id]); let j=index">

View File

@ -108,6 +108,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
ngOnInit(): void { ngOnInit(): void {
this.jobsService.subscribeToProgress(); this.jobsService.subscribeToProgress();
this.jobsService.getAvailableJobs().catch(console.error); this.jobsService.getAvailableJobs().catch(console.error);
this.jobsService.getAvailableMessengers().catch(console.error);
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -128,7 +129,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
schedule.config = schedule.config || {}; schedule.config = schedule.config || {};
if (job.ConfigTemplate) { if (job.ConfigTemplate) {
job.ConfigTemplate.forEach( job.ConfigTemplate.forEach(
(ct) => (schedule.config[ct.id] = ct.defaultValue) (ct) => (schedule.config[ct.id] = ct.defaultValue as never)
); );
} }
} }
@ -216,7 +217,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni
this.newSchedule.config = this.newSchedule.config || {}; this.newSchedule.config = this.newSchedule.config || {};
if (job.ConfigTemplate) { if (job.ConfigTemplate) {
job.ConfigTemplate.forEach( job.ConfigTemplate.forEach(
(ct) => (this.newSchedule.config[ct.id] = ct.defaultValue) (ct) => (this.newSchedule.config[ct.id] = ct.defaultValue as never)
); );
} }
this.jobModalQL.first.show(); this.jobModalQL.first.show();