Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the option to send daily recap by email (#5733) #5962

Merged
merged 1 commit into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ operatorfabric:
defaultConfig:
subjectPrefix: 'Opfab card received '
bodyPrefix: 'You received a card in opfab : '
dailyEmailTitle: 'Cards received during the day'
hourToSendDailyEmail: 7
minuteToSendDailyEmail: 30
opfabUrlInMailContent: http://localhost:2002
windowInSecondsForCardSearch: 360
secondsAfterPublicationToConsiderCardAsNotRead: 60
Expand Down
3 changes: 3 additions & 0 deletions node-services/cards-external-diffusion/config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ operatorfabric:
mailFrom: [email protected]
subjectPrefix: 'Opfab card received '
bodyPrefix: 'You received a card in opfab : '
dailyEmailTitle: 'Cards received during the day'
hourToSendDailyEmail: 7
minuteToSendDailyEmail: 30
opfabUrlInMailContent: http://localhost:2002
windowInSecondsForCardSearch: 360
secondsAfterPublicationToConsiderCardAsNotRead: 60
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ app.listen(adminPort, () => {
logger.info(`Opfab card external diffusion service listening on port ${adminPort}`);
});

app.post('/sendDailyEmail', (req, res) => {

authorizationService.isAdminUser(req).then(isAdmin => {
if (!isAdmin)
res.status(403).send();
else {
logger.info('Sending email with cards from the last 24 hours');
cardsExternalDiffusionService.sendDailyRecap();
res.send();
}
})
});

async function start() {
await cardsExternalDiffusionDatabaseService.connectToMongoDB();
opfabServicesInterface.startListener();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,41 @@

import SendMailService from '../server-side/sendMailService';
import CardsExternalDiffusionOpfabServicesInterface from '../server-side/cardsExternalDiffusionOpfabServicesInterface';
import CardsRoutingUtilities from './cardRoutingUtilities';
import ConfigDTO from '../client-side/configDTO';
import CardsExternalDiffusionDatabaseService from '../server-side/cardsExternaDiffusionDatabaseService';
import CardsDiffusionRateLimiter from './cardsDiffusionRateLimiter';
import BusinessConfigOpfabServicesInterface from '../server-side/BusinessConfigOpfabServicesInterface';

export default class CardsDiffusionControl {

opfabUrlInMailContent: any;
protected opfabUrlInMailContent: any;
protected cardsExternalDiffusionOpfabServicesInterface: CardsExternalDiffusionOpfabServicesInterface;
protected businessConfigOpfabServicesInterface: BusinessConfigOpfabServicesInterface;
protected cardsExternalDiffusionDatabaseService: CardsExternalDiffusionDatabaseService;
protected logger: any;
protected mailService: SendMailService;
protected from: string;

private cardsExternalDiffusionOpfabServicesInterface: CardsExternalDiffusionOpfabServicesInterface;
private businessConfigOpfabServicesInterface: BusinessConfigOpfabServicesInterface;
private cardsExternalDiffusionDatabaseService: CardsExternalDiffusionDatabaseService;
private logger: any;
private secondsAfterPublicationToConsiderCardAsNotRead: number;
private windowInSecondsForCardSearch: number;
private mailService: SendMailService;
private from: string;
private subjectPrefix: string;
private bodyPrefix: string;
private activateCardsDiffusionRateLimiter: boolean;
private cardsDiffusionRateLimiter: CardsDiffusionRateLimiter;
public setOpfabUrlInMailContent(opfabUrlInMailContent: any) {
this.opfabUrlInMailContent = opfabUrlInMailContent;
return this;
}

public setOpfabServicesInterface(cardsExternalDiffusionOpfabServicesInterface: CardsExternalDiffusionOpfabServicesInterface) {
public setOpfabServicesInterface(
cardsExternalDiffusionOpfabServicesInterface: CardsExternalDiffusionOpfabServicesInterface
) {
this.cardsExternalDiffusionOpfabServicesInterface = cardsExternalDiffusionOpfabServicesInterface;
return this;
}

public setOpfabBusinessConfigServicesInterface(businessConfigOpfabServicesInterface: BusinessConfigOpfabServicesInterface) {
public setOpfabBusinessConfigServicesInterface(
businessConfigOpfabServicesInterface: BusinessConfigOpfabServicesInterface
) {
this.businessConfigOpfabServicesInterface = businessConfigOpfabServicesInterface;
return this;
}

public setCardsExternalDiffusionDatabaseService(cardsExternalDiffusionDatabaseService: CardsExternalDiffusionDatabaseService) {
public setCardsExternalDiffusionDatabaseService(
cardsExternalDiffusionDatabaseService: CardsExternalDiffusionDatabaseService
) {
this.cardsExternalDiffusionDatabaseService = cardsExternalDiffusionDatabaseService;
return this;
}
Expand All @@ -62,178 +63,15 @@ export default class CardsDiffusionControl {
return this;
}

public setSubjectPrefix(subjectPrefix: string) {
this.subjectPrefix = subjectPrefix;
return this;
}

public setBodyPrefix(bodyPrefix: string) {
this.bodyPrefix = bodyPrefix;
return this;
}

public setOpfabUrlInMailContent(opfabUrlInMailContent: any) {
this.opfabUrlInMailContent = opfabUrlInMailContent;
return this;
}

public setSecondsAfterPublicationToConsiderCardAsNotRead(secondsAfterPublicationToConsiderCardAsNotRead: number) {
this.secondsAfterPublicationToConsiderCardAsNotRead = secondsAfterPublicationToConsiderCardAsNotRead;
return this;
}

public setWindowInSecondsForCardSearch(windowInSecondsForCardSearch: number) {
this.windowInSecondsForCardSearch = windowInSecondsForCardSearch;
return this;
}

public setActivateCardsDiffusionRateLimiter(activate: boolean) {
this.activateCardsDiffusionRateLimiter = activate;
return this;
}

public setCardsDiffusionRateLimiter( cardsDiffusionRateLimiter: CardsDiffusionRateLimiter) {
this.cardsDiffusionRateLimiter = cardsDiffusionRateLimiter;
}

public setConfiguration(updated: ConfigDTO) {
this.from = updated.mailFrom;
this.subjectPrefix = updated.subjectPrefix;
this.bodyPrefix = updated.bodyPrefix;
this.secondsAfterPublicationToConsiderCardAsNotRead = updated.secondsAfterPublicationToConsiderCardAsNotRead;
this.windowInSecondsForCardSearch = updated.windowInSecondsForCardSearch;
this.activateCardsDiffusionRateLimiter = updated.activateCardsDiffusionRateLimiter;
if (this.activateCardsDiffusionRateLimiter) {
this.cardsDiffusionRateLimiter = new CardsDiffusionRateLimiter()
.setLimitPeriodInSec(updated.sendRateLimitPeriodInSec)
.setSendRateLimit(updated.sendRateLimit);
}
}

public async checkUnreadCards() {
const users = this.cardsExternalDiffusionOpfabServicesInterface.getUsers();
const userLogins = users.map((u) => u.login);

const connectedResponse = await this.cardsExternalDiffusionOpfabServicesInterface.getUsersConnected();
if (connectedResponse.isValid()) {
const connectedUsers = connectedResponse.getData().map((u: {login: string;}) => u.login);
const usersToCheck = this.removeElementsFromArray(userLogins, connectedUsers);
this.logger.debug('Disconnected users ' + usersToCheck);
if (usersToCheck.length > 0) {
const dateFrom = Date.now() - this.windowInSecondsForCardSearch * 1000;
const cards = await this.cardsExternalDiffusionDatabaseService.getCards(dateFrom);
if (cards.length > 0) {
this.logger.debug('Found cards: ' + cards.length);
usersToCheck.forEach((login) => {
this.sendCardsToUserIfNecessary(cards, login).catch(error =>
this.logger.error("error during sendCardsToUserIfNecessary ", error)
)
});
}
}
await this.cleanCardsAreadySent();
}
}

private async sendCardsToUserIfNecessary(cards: any[], login: string) {
this.logger.debug('Check user ' + login);

const resp = await this.cardsExternalDiffusionOpfabServicesInterface.getUserWithPerimetersByLogin(login);
if (resp.isValid()) {
const userWithPerimeters = resp.getData();
const emailToPlainText = this.shouldEmailBePlainText(userWithPerimeters);
this.logger.debug('Got user with perimeters ' + JSON.stringify(userWithPerimeters));
if (this.isEmailSettingEnabled(userWithPerimeters)) {
const unreadCards = await this.getCardsForUser(cards, userWithPerimeters);
for (let i = 0; i < unreadCards.length; i++) {
await this.sendCardIfAllowed(unreadCards[i], userWithPerimeters.email, emailToPlainText);
}
}
}
}

private async sendCardIfAllowed(unreadCard: any, userEmail: string, emailToPlainText: boolean): Promise<void> {
try {
const alreadySent = await this.wasCardsAlreadySentToUser(unreadCard.uid, userEmail);
if (!alreadySent) {
if (this.isSendingAllowed(userEmail)) {
await this.sendMail(unreadCard, userEmail, emailToPlainText);
} else {
this.logger.warn(`Send rate limit reached for ${userEmail}, not sending mail for card ${unreadCard.uid}`);
await this.cardsExternalDiffusionDatabaseService.persistSentMail(unreadCard.uid, userEmail);
}
}
} catch (error) {
this.logger.error("Error occurred while sending mail: ", error);
}
}

private isSendingAllowed(email: string) {
return !this.activateCardsDiffusionRateLimiter || this.cardsDiffusionRateLimiter.isNewSendingAllowed(email);
}

private registerNewSending(destination: string) {
if (this.activateCardsDiffusionRateLimiter)
this.cardsDiffusionRateLimiter.registerNewSending(destination);
}

private async getCardsForUser(cards : any[], userWithPerimeters: any) : Promise<any[]> {

const perimeters = userWithPerimeters.computedPerimeters;
this.logger.debug('Got user perimeters' + JSON.stringify(perimeters));
return cards
.filter(
(card: any) =>
CardsRoutingUtilities.shouldUserReceiveTheCard(
userWithPerimeters,
card
) && this.isCardUnreadForUser(card, userWithPerimeters.userData)
);
}

private wasCardsAlreadySentToUser(cardUid: string, email: string) {
return this.cardsExternalDiffusionDatabaseService.getSentMail(cardUid, email);
}

private isEmailSettingEnabled(userWithPerimeters: any): boolean {
protected isEmailSettingEnabled(userWithPerimeters: any): boolean {
return userWithPerimeters.sendCardsByEmail && userWithPerimeters.email;

}

private shouldEmailBePlainText(userWithPerimeters: any): boolean {
protected shouldEmailBePlainText(userWithPerimeters: any): boolean {
return userWithPerimeters.emailToPlainText ? userWithPerimeters.emailToPlainText : false;
}

private isCardUnreadForUser(card: any, user: any): boolean {
return (
card.publishDate < Date.now() - 1000 * this.secondsAfterPublicationToConsiderCardAsNotRead &&
!card.usersReads?.includes(user.login)
);
}

private async sendMail(card: any, to: string, emailToPlainText:boolean) {
this.logger.info('Send Mail to ' + to + ' for card ' + card.uid);
let subject =
this.subjectPrefix +
' - ' +
card.titleTranslated +
' - ' +
card.summaryTranslated +
' - ' +
this.getFormattedDateAndTimeFromEpochDate(card.startDate);
if (card.endDate) subject += ' - ' + this.getFormattedDateAndTimeFromEpochDate(card.endDate);
const body = await this.processCardTemplate(card);
try {
await this.mailService.sendMail(subject, body, this.from, to, emailToPlainText);
this.registerNewSending(to);
await this.cardsExternalDiffusionDatabaseService.persistSentMail(card.uid, to);
} catch (e) {
this.logger.error('Error sending mail ', e);
};

}

private removeElementsFromArray(arrayToFilter: string[], arrayToDelete: string[]): string[] {
protected removeElementsFromArray(arrayToFilter: string[], arrayToDelete: string[]): string[] {
if (arrayToDelete && arrayToDelete.length > 0) {
const elementsToDeleteSet = new Set(arrayToDelete);
const newArray = arrayToFilter.filter((name) => {
Expand All @@ -245,43 +83,7 @@ export default class CardsDiffusionControl {
}
}

private async processCardTemplate(card: any): Promise<string> {
let cardBodyHtml =
this.bodyPrefix +
' <a href=" ' +
this.opfabUrlInMailContent +
'/#/feed/cards/' +
card.id +
' ">' +
this.escapeHtml(card.titleTranslated) +
' - ' +
this.escapeHtml(card.summaryTranslated) +
'</a>';
try {
const cardConfig = await this.businessConfigOpfabServicesInterface.fetchProcessConfig(
card.process,
card.processVersion
);
const stateName = card.state;
if (cardConfig?.states?.[stateName]?.emailBodyTemplate) {
const cardContentResponse = await this.cardsExternalDiffusionOpfabServicesInterface.getCard(card.id);
if (cardContentResponse.isValid()) {
const cardContent = cardContentResponse.getData();
const templateCompiler = await this.businessConfigOpfabServicesInterface.fetchTemplate(
card.process,
cardConfig.states[stateName].emailBodyTemplate,
card.processVersion
);
cardBodyHtml = cardBodyHtml + ' <br> ' + templateCompiler(cardContent);
}
}
} catch (e) {
console.warn("Couldn't parse email for : ", card.state, e);
}
return cardBodyHtml;
}

private escapeHtml(text: string): string {
protected escapeHtml(text: string): string {
if (!text) return text;
return text
.replace(/&/g, '&amp;')
Expand All @@ -291,12 +93,7 @@ export default class CardsDiffusionControl {
.replace(/'/g, '&#39;');
}

private async cleanCardsAreadySent() {
const dateLimit = Date.now() - this.windowInSecondsForCardSearch * 1000;
await this.cardsExternalDiffusionDatabaseService.deleteMailsSentBefore(dateLimit);
}

private getFormattedDateAndTimeFromEpochDate(epochDate: number): string {
protected getFormattedDateAndTimeFromEpochDate(epochDate: number): string {
if (!epochDate) return '';
const date = new Date(epochDate);

Expand All @@ -313,7 +110,7 @@ export default class CardsDiffusionControl {
);
}

private pad(num: number): string {
protected pad(num: number): string {
if (num < 10) {
return '0' + num;
}
Expand Down
Loading