diff --git a/src/backend/middlewares/admin/AdminMWs.ts b/src/backend/middlewares/admin/AdminMWs.ts index e7bc288a..68fc7f90 100644 --- a/src/backend/middlewares/admin/AdminMWs.ts +++ b/src/backend/middlewares/admin/AdminMWs.ts @@ -2,18 +2,19 @@ import {NextFunction, Request, Response} from 'express'; import {ErrorCodes, ErrorDTO} from '../../../common/entities/Error'; import {ObjectManagers} from '../../model/ObjectManagers'; import {StatisticDTO} from '../../../common/entities/settings/StatisticDTO'; +import {MessengerRepository} from '../../model/messenger/MessengerRepository'; export class AdminMWs { public static async loadStatistic( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction ): Promise { const galleryManager = ObjectManagers.getInstance() - .GalleryManager; + .GalleryManager; const personManager = ObjectManagers.getInstance() - .PersonManager; + .PersonManager; try { req.resultPipe = { directories: await galleryManager.countDirectories(), @@ -26,87 +27,87 @@ export class AdminMWs { } catch (err) { if (err instanceof Error) { return next( - new ErrorDTO( - ErrorCodes.GENERAL_ERROR, - 'Error while getting statistic: ' + err.toString(), - err - ) + new ErrorDTO( + ErrorCodes.GENERAL_ERROR, + 'Error while getting statistic: ' + err.toString(), + err + ) ); } return next( - new ErrorDTO( - ErrorCodes.GENERAL_ERROR, - 'Error while getting statistic', - err - ) + new ErrorDTO( + ErrorCodes.GENERAL_ERROR, + 'Error while getting statistic', + err + ) ); } } public static async getDuplicates( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction ): Promise { try { req.resultPipe = await ObjectManagers.getInstance() - .GalleryManager.getPossibleDuplicates(); + .GalleryManager.getPossibleDuplicates(); return next(); } catch (err) { if (err instanceof Error) { return next( - new ErrorDTO( - ErrorCodes.GENERAL_ERROR, - 'Error while getting duplicates: ' + err.toString(), - err - ) + new ErrorDTO( + ErrorCodes.GENERAL_ERROR, + 'Error while getting duplicates: ' + err.toString(), + err + ) ); } return next( - new ErrorDTO( - ErrorCodes.GENERAL_ERROR, - 'Error while getting duplicates', - err - ) + new ErrorDTO( + ErrorCodes.GENERAL_ERROR, + 'Error while getting duplicates', + err + ) ); } } public static async startJob( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction ): Promise { try { const id = req.params['id']; - const JobConfig: unknown = req.body.config; + const JobConfig: Record = req.body.config; const soloRun: boolean = req.body.soloRun; const allowParallelRun: boolean = req.body.allowParallelRun; await ObjectManagers.getInstance().JobManager.run( - id, - JobConfig, - soloRun, - allowParallelRun + id, + JobConfig, + soloRun, + allowParallelRun ); req.resultPipe = 'ok'; return next(); } catch (err) { if (err instanceof Error) { return next( - new ErrorDTO( - ErrorCodes.JOB_ERROR, - 'Job error: ' + err.toString(), - err - ) + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Job error: ' + err.toString(), + err + ) ); } return next( - new ErrorDTO( - ErrorCodes.JOB_ERROR, - 'Job error: ' + JSON.stringify(err, null, ' '), - err - ) + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Job error: ' + JSON.stringify(err, null, ' '), + err + ) ); } } @@ -120,56 +121,85 @@ export class AdminMWs { } catch (err) { if (err instanceof Error) { return next( - new ErrorDTO( - ErrorCodes.JOB_ERROR, - 'Job error: ' + err.toString(), - err - ) + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Job error: ' + err.toString(), + err + ) ); } 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( - ErrorCodes.JOB_ERROR, - 'Job error: ' + JSON.stringify(err, null, ' '), - err + ErrorCodes.JOB_ERROR, + 'Messenger error: ' + err.toString(), + err ) + ); + } + return next( + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Messenger error: ' + JSON.stringify(err, null, ' '), + err + ) ); } } public static getAvailableJobs( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction ): void { try { req.resultPipe = - ObjectManagers.getInstance().JobManager.getAvailableJobs(); + ObjectManagers.getInstance().JobManager.getAvailableJobs(); return next(); } catch (err) { if (err instanceof Error) { return next( - new ErrorDTO( - ErrorCodes.JOB_ERROR, - 'Job error: ' + err.toString(), - err - ) + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Job error: ' + err.toString(), + err + ) ); } return next( - new ErrorDTO( - ErrorCodes.JOB_ERROR, - 'Job error: ' + JSON.stringify(err, null, ' '), - err - ) + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Job error: ' + JSON.stringify(err, null, ' '), + err + ) ); } } public static getJobProgresses( - req: Request, - res: Response, - next: NextFunction + req: Request, + res: Response, + next: NextFunction ): void { try { req.resultPipe = ObjectManagers.getInstance().JobManager.getProgresses(); @@ -177,19 +207,19 @@ export class AdminMWs { } catch (err) { if (err instanceof Error) { return next( - new ErrorDTO( - ErrorCodes.JOB_ERROR, - 'Job error: ' + err.toString(), - err - ) + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Job error: ' + err.toString(), + err + ) ); } return next( - new ErrorDTO( - ErrorCodes.JOB_ERROR, - 'Job error: ' + JSON.stringify(err, null, ' '), - err - ) + new ErrorDTO( + ErrorCodes.JOB_ERROR, + 'Job error: ' + JSON.stringify(err, null, ' '), + err + ) ); } } diff --git a/src/backend/model/jobs/JobManager.ts b/src/backend/model/jobs/JobManager.ts index dedc8db1..fc0f7a87 100644 --- a/src/backend/model/jobs/JobManager.ts +++ b/src/backend/model/jobs/JobManager.ts @@ -50,7 +50,7 @@ export class JobManager implements IJobListener, IObjectManager { return prg; } - public async run( + public async run>( jobName: string, config: T, soloRun: boolean, @@ -86,7 +86,7 @@ export class JobManager implements IJobListener, IObjectManager { }; onJobFinished = async ( - job: IJob, + job: IJob, state: JobProgressStates, soloRun: boolean ): Promise => { @@ -121,7 +121,7 @@ export class JobManager implements IJobListener, IObjectManager { } }; - getAvailableJobs(): IJob[] { + getAvailableJobs(): IJob[] { return JobRepository.Instance.getAvailableJobs(); } @@ -144,7 +144,7 @@ export class JobManager implements IJobListener, IObjectManager { Config.Jobs.scheduled.forEach((s): void => this.runSchedule(s)); } - protected findJob(jobName: string): IJob { + protected findJob(jobName: string): IJob { return this.getAvailableJobs().find((t): boolean => t.Name === jobName); } diff --git a/src/backend/model/jobs/JobRepository.ts b/src/backend/model/jobs/JobRepository.ts index f195bf21..92a10bcd 100644 --- a/src/backend/model/jobs/JobRepository.ts +++ b/src/backend/model/jobs/JobRepository.ts @@ -14,7 +14,7 @@ import {AlbumCoverRestJob} from './jobs/AlbumCoverResetJob'; export class JobRepository { private static instance: JobRepository = null; - availableJobs: { [key: string]: IJob } = {}; + availableJobs: { [key: string]: IJob } = {}; public static get Instance(): JobRepository { if (JobRepository.instance == null) { @@ -23,11 +23,11 @@ export class JobRepository { return JobRepository.instance; } - getAvailableJobs(): IJob[] { + getAvailableJobs(): IJob[] { return Object.values(this.availableJobs).filter((t) => t.Supported); } - register(job: IJob): void { + register(job: IJob): void { if (typeof this.availableJobs[job.Name] !== 'undefined') { throw new Error('Job already exist:' + job.Name); } diff --git a/src/backend/model/jobs/jobs/IJob.ts b/src/backend/model/jobs/jobs/IJob.ts index acd84d5a..4c08d78e 100644 --- a/src/backend/model/jobs/jobs/IJob.ts +++ b/src/backend/model/jobs/jobs/IJob.ts @@ -2,7 +2,7 @@ import {JobDTO} from '../../../../common/entities/job/JobDTO'; import {JobProgress} from './JobProgress'; import {IJobListener} from './IJobListener'; -export interface IJob extends JobDTO { +export interface IJob = Record> extends JobDTO { Name: string; Supported: boolean; Progress: JobProgress; diff --git a/src/backend/model/jobs/jobs/IJobListener.ts b/src/backend/model/jobs/jobs/IJobListener.ts index bf1f1b1f..73b7f87f 100644 --- a/src/backend/model/jobs/jobs/IJobListener.ts +++ b/src/backend/model/jobs/jobs/IJobListener.ts @@ -4,7 +4,7 @@ import {JobProgressStates} from '../../../../common/entities/job/JobProgressDTO' export interface IJobListener { onJobFinished( - job: IJob, + job: IJob, state: JobProgressStates, soloRun: boolean ): void; diff --git a/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts b/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts index f6132039..e8a25f84 100644 --- a/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts +++ b/src/backend/model/jobs/jobs/ThumbnailGenerationJob.ts @@ -35,7 +35,7 @@ export class ThumbnailGenerationJob extends FileJob<{ ): Promise { if (!config || !config.sizes || !Array.isArray(config.sizes) || config.sizes.length === 0) { 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) { if (Config.Media.Thumbnail.thumbnailSizes.indexOf(item) === -1) { diff --git a/src/backend/model/jobs/jobs/TopPickSendJob.ts b/src/backend/model/jobs/jobs/TopPickSendJob.ts index b893feae..2ba0450c 100644 --- a/src/backend/model/jobs/jobs/TopPickSendJob.ts +++ b/src/backend/model/jobs/jobs/TopPickSendJob.ts @@ -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 {backendTexts} from '../../../../common/BackendTexts'; import {SortByTypes} from '../../../../common/entities/SortingMethods'; import {DatePatternFrequency, DatePatternSearch, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO'; import {ObjectManagers} from '../../ObjectManagers'; import {PhotoEntity} from '../../database/enitites/PhotoEntity'; -import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger'; import {MediaPickDTO} from '../../../../common/entities/MediaPickDTO'; import {MediaDTOUtils} from '../../../../common/entities/MediaDTO'; import {DynamicConfig} from '../../../../common/entities/DynamicConfig'; +import {MessengerRepository} from '../../messenger/MessengerRepository'; +import {Utils} from '../../../../common/Utils'; export class TopPickSendJob extends Job<{ mediaPick: MediaPickDTO[], + messenger: string, emailTo: string, - emailFrom: string, emailSubject: string, emailText: string, }> { public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']]; public readonly Supported: boolean = true; - 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:', - }, - ]; + public readonly ConfigTemplate: DynamicConfig[]; private status: 'Listing' | 'Sending' = 'Listing'; 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 { this.status = 'Listing'; @@ -86,15 +89,15 @@ export class TopPickSendJob extends Job<{ this.mediaList = []; for (let i = 0; i < this.config.mediaPick.length; ++i) { 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.mediaList = this.mediaList.concat(media); } // make the list unique this.mediaList = this.mediaList - .filter((value, index, arr) => - arr.findIndex(m => MediaDTOUtils.equals(m, value)) === index); + .filter((value, index, arr) => + arr.findIndex(m => MediaDTOUtils.equals(m, value)) === index); this.Progress.Processed++; // console.log(this.mediaList); @@ -103,17 +106,16 @@ export class TopPickSendJob extends Job<{ private async stepSending(): Promise { 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++; return false; } - this.Progress.log('Sending emails of ' + this.mediaList.length + ' photos.'); - const messenger = new EmailMediaMessenger(); - await messenger.sendMedia({ - to: this.config.emailTo, - subject: this.config.emailSubject, - text: this.config.emailText - }, this.mediaList); + const msgr = MessengerRepository.Instance.get(this.config.messenger); + if (!msgr) { + throw new Error('Can\t find "' + this.config.messenger + '" messenger.'); + } + this.Progress.log('Sending ' + this.mediaList.length + ' photos.'); + await msgr.send(this.config, this.mediaList); this.Progress.Processed++; return false; } diff --git a/src/backend/model/mediamessengers/EmailMediaMessenger.ts b/src/backend/model/messenger/EmailMessenger.ts similarity index 57% rename from src/backend/model/mediamessengers/EmailMediaMessenger.ts rename to src/backend/model/messenger/EmailMessenger.ts index a4108449..c412c55f 100644 --- a/src/backend/model/mediamessengers/EmailMediaMessenger.ts +++ b/src/backend/model/messenger/EmailMessenger.ts @@ -1,18 +1,40 @@ import {createTransport, Transporter} from 'nodemailer'; -import {MediaDTO, MediaDTOUtils} from '../../../common/entities/MediaDTO'; 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 {Utils} from '../../../common/Utils'; -import {QueryParams} from '../../../common/QueryParams'; +import {MediaDTOWithThPath, Messenger} from './Messenger'; +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; constructor() { + super(); this.transporter = createTransport({ host: Config.Messaging.Email.smtp.host, 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: { - to: string, - subject: string, - text: string - }, media: MediaDTO[]) { + protected async sendMedia(mailSettings: { + emailTo: string, + emailSubject: string, + emailText: string + }, media: MediaDTOWithThPath[]) { const attachments = []; const htmlStart = '

' + Config.Server.applicationTitle + '

\n' + - '

' + mailSettings.text + '

\n' + + '

' + mailSettings.emailText + '

\n' + '\n' + ' \n'; const htmlEnd = ' \n' + @@ -51,9 +65,6 @@ export class EmailMediaMessenger { let htmlMiddle = ''; const numberOfColumns = media.length >= 6 ? 3 : 2; 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 ? (media[i].metadata as PhotoMetadata).positionData?.country : ((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 : ''); attachments.push({ filename: media[i].name, - path: thPath, + path: media[i].thumbnailPath, cid: 'img' + i }); if (i % numberOfColumns == 0) { htmlMiddle += ''; } htmlMiddle += '\n'; @@ -79,8 +90,8 @@ export class EmailMediaMessenger { return await this.transporter.sendMail({ from: Config.Messaging.Email.emailFrom, - to: mailSettings.to, - subject: mailSettings.subject, + to: mailSettings.emailTo, + subject: mailSettings.emailSubject, html: htmlStart + htmlMiddle + htmlEnd, attachments: attachments }); diff --git a/src/backend/model/messenger/Messenger.ts b/src/backend/model/messenger/Messenger.ts new file mode 100644 index 00000000..3d2cf338 --- /dev/null +++ b/src/backend/model/messenger/Messenger.ts @@ -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 = Record> { + + 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 ; +} diff --git a/src/backend/model/messenger/MessengerRepository.ts b/src/backend/model/messenger/MessengerRepository.ts new file mode 100644 index 00000000..c6d162ee --- /dev/null +++ b/src/backend/model/messenger/MessengerRepository.ts @@ -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()); diff --git a/src/backend/model/messenger/StdoutMessenger.ts b/src/backend/model/messenger/StdoutMessenger.ts new file mode 100644 index 00000000..1edbf20a --- /dev/null +++ b/src/backend/model/messenger/StdoutMessenger.ts @@ -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)); + } +} diff --git a/src/backend/routes/admin/AdminRouter.ts b/src/backend/routes/admin/AdminRouter.ts index 26a1f949..1ad4e31d 100644 --- a/src/backend/routes/admin/AdminRouter.ts +++ b/src/backend/routes/admin/AdminRouter.ts @@ -10,6 +10,7 @@ export class AdminRouter { this.addGetStatistic(app); this.addGetDuplicates(app); this.addJobs(app); + this.addMessengers(app); } 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 { app.get( Config.Server.apiPath + '/admin/jobs/available', diff --git a/src/common/BackendTexts.ts b/src/common/BackendTexts.ts index 966d198b..de825cd9 100644 --- a/src/common/BackendTexts.ts +++ b/src/common/BackendTexts.ts @@ -1,4 +1,5 @@ export type backendText = number; +// keep the numbering sparse to support later addition export const backendTexts = { indexedFilesOnly: {name: 10, description: 12}, sizeToGenerate: {name: 20, description: 22}, @@ -6,6 +7,7 @@ export const backendTexts = { mediaPick: {name: 40, description: 42}, emailTo: {name: 70, description: 72}, emailSubject: {name: 90, description: 92}, - emailText: {name: 100, description: 102} + emailText: {name: 100, description: 102}, + messenger: {name: 110,description: 112} }; diff --git a/src/common/entities/DynamicConfig.ts b/src/common/entities/DynamicConfig.ts index becfc97b..14b5129e 100644 --- a/src/common/entities/DynamicConfig.ts +++ b/src/common/entities/DynamicConfig.ts @@ -1,5 +1,8 @@ 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. @@ -14,4 +17,5 @@ export interface DynamicConfig { description: backendText | string; type: fieldType; defaultValue: unknown; + validIf?: { configFiled: string, equalsValue: string }; // only shows this config if this predicate is true } diff --git a/src/common/entities/job/JobDTO.ts b/src/common/entities/job/JobDTO.ts index 3001a80f..60ed59ca 100644 --- a/src/common/entities/job/JobDTO.ts +++ b/src/common/entities/job/JobDTO.ts @@ -1,8 +1,5 @@ -import {backendText} from '../../BackendTexts'; import {DynamicConfig} from '../DynamicConfig'; -export type fieldType = 'string' | 'string-array' | 'number' | 'boolean' | 'number-array' | 'MediaPickDTO-array'; - export enum DefaultsJobs { Indexing = 1, 'Gallery Reset' = 2, @@ -19,6 +16,12 @@ export enum DefaultsJobs { } +export enum DefaultMessengers { + Email = 1, + Stdout = 2 +} + + export interface JobDTO { Name: string; ConfigTemplate: DynamicConfig[]; diff --git a/src/frontend/app/model/backendtext.service.ts b/src/frontend/app/model/backendtext.service.ts index d814b103..d228ccb1 100644 --- a/src/frontend/app/model/backendtext.service.ts +++ b/src/frontend/app/model/backendtext.service.ts @@ -5,7 +5,10 @@ import {DefaultsJobs} from '../../../common/entities/job/JobDTO'; @Injectable() export class BackendtextService { - public get(id: backendText): string { + public get(id: backendText | string): string { + if (typeof id === 'string') { + return id; + } switch (id) { case backendTexts.sizeToGenerate.name: return $localize`Size to generate`; diff --git a/src/frontend/app/ui/settings/scheduled-jobs.service.ts b/src/frontend/app/ui/settings/scheduled-jobs.service.ts index 77a74227..fde6dd27 100644 --- a/src/frontend/app/ui/settings/scheduled-jobs.service.ts +++ b/src/frontend/app/ui/settings/scheduled-jobs.service.ts @@ -3,9 +3,10 @@ import {BehaviorSubject} from 'rxjs'; import {JobProgressDTO, JobProgressStates, OnTimerJobProgressDTO,} from '../../../../common/entities/job/JobProgressDTO'; import {NetworkService} from '../../model/network/network.service'; 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 {NotificationService} from '../../model/notification.service'; +import {DynamicConfig} from '../../../../common/entities/DynamicConfig'; @Injectable() export class ScheduledJobsService { @@ -13,6 +14,7 @@ export class ScheduledJobsService { public onJobFinish: EventEmitter = new EventEmitter(); timer: number = null; public availableJobs: BehaviorSubject; + public availableMessengers: BehaviorSubject; public jobStartingStopping: { [key: string]: boolean } = {}; private subscribers = 0; @@ -23,6 +25,7 @@ export class ScheduledJobsService { ) { this.progress = 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 { + this.availableMessengers.next( + await this.networkService.getJson('/admin/messengers/available') + ); + } + + public getConfigTemplate(JobName: string): DynamicConfig[] { const job = this.availableJobs.value.find( (t) => t.Name === JobName ); diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.html b/src/frontend/app/ui/settings/workflow/workflow.component.html index 8d4b8b61..01d8fc63 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.html +++ b/src/frontend/app/ui/settings/workflow/workflow.component.html @@ -170,6 +170,7 @@
@@ -227,6 +228,18 @@ placeholder="Search Query"> + + + diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.ts b/src/frontend/app/ui/settings/workflow/workflow.component.ts index 6b8be5b0..95633782 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.ts +++ b/src/frontend/app/ui/settings/workflow/workflow.component.ts @@ -108,6 +108,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni ngOnInit(): void { this.jobsService.subscribeToProgress(); this.jobsService.getAvailableJobs().catch(console.error); + this.jobsService.getAvailableMessengers().catch(console.error); } ngOnDestroy(): void { @@ -128,7 +129,7 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni schedule.config = schedule.config || {}; if (job.ConfigTemplate) { 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 || {}; if (job.ConfigTemplate) { 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();
\n' + - ' ' + media[i].name + '\n' + + ' ' + media[i].name + '\n' + caption + '