Comment notification emails (#4978)
* Comment notification emails * fix links fix threading in email inboxes from is now commenter name * fix * refactor * fix async filter
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { subHours } from "date-fns";
|
||||
import { uniqBy } from "lodash";
|
||||
import { Op } from "sequelize";
|
||||
import { Minute } from "@shared/utils/time";
|
||||
import subscriptionCreator from "@server/commands/subscriptionCreator";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import CollectionNotificationEmail from "@server/emails/templates/CollectionNotificationEmail";
|
||||
@@ -8,23 +8,24 @@ import DocumentNotificationEmail from "@server/emails/templates/DocumentNotifica
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import {
|
||||
View,
|
||||
Document,
|
||||
Team,
|
||||
Collection,
|
||||
User,
|
||||
NotificationSetting,
|
||||
Subscription,
|
||||
Notification,
|
||||
Revision,
|
||||
User,
|
||||
View,
|
||||
} from "@server/models";
|
||||
import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
||||
import NotificationHelper from "@server/models/helpers/NotificationHelper";
|
||||
import {
|
||||
CollectionEvent,
|
||||
RevisionEvent,
|
||||
Event,
|
||||
DocumentEvent,
|
||||
CommentEvent,
|
||||
} from "@server/types";
|
||||
import CommentCreatedNotificationTask from "../tasks/CommentCreatedNotificationTask";
|
||||
import BaseProcessor from "./BaseProcessor";
|
||||
|
||||
export default class NotificationsProcessor extends BaseProcessor {
|
||||
@@ -32,6 +33,7 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
"documents.publish",
|
||||
"revisions.create",
|
||||
"collections.create",
|
||||
"comments.create",
|
||||
];
|
||||
|
||||
async perform(event: Event) {
|
||||
@@ -42,11 +44,18 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
return this.revisionCreated(event);
|
||||
case "collections.create":
|
||||
return this.collectionCreated(event);
|
||||
|
||||
case "comments.create":
|
||||
return this.commentCreated(event);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
async commentCreated(event: CommentEvent) {
|
||||
await CommentCreatedNotificationTask.schedule(event, {
|
||||
delay: Minute,
|
||||
});
|
||||
}
|
||||
|
||||
async documentPublished(event: DocumentEvent) {
|
||||
// never send notifications when batch importing documents
|
||||
if (
|
||||
@@ -69,9 +78,10 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
|
||||
await this.createDocumentSubscriptions(document, event);
|
||||
|
||||
const recipients = await this.getDocumentNotificationRecipients(
|
||||
const recipients = await NotificationHelper.getDocumentNotificationRecipients(
|
||||
document,
|
||||
"documents.publish"
|
||||
"documents.publish",
|
||||
document.lastModifiedById
|
||||
);
|
||||
|
||||
for (const recipient of recipients) {
|
||||
@@ -115,27 +125,26 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
|
||||
await this.createDocumentSubscriptions(document, event);
|
||||
|
||||
const recipients = await this.getDocumentNotificationRecipients(
|
||||
const recipients = await NotificationHelper.getDocumentNotificationRecipients(
|
||||
document,
|
||||
"documents.update"
|
||||
"documents.update",
|
||||
document.lastModifiedById
|
||||
);
|
||||
if (!recipients.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// generate the diff html for the email
|
||||
const before = await revision.previous();
|
||||
let content = await DocumentHelper.toEmailDiff(before, revision, {
|
||||
const content = await DocumentHelper.toEmailDiff(before, revision, {
|
||||
includeTitle: false,
|
||||
centered: false,
|
||||
signedUrls: 86400 * 4,
|
||||
});
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
content = await DocumentHelper.attachmentsToSignedUrls(
|
||||
content,
|
||||
event.teamId,
|
||||
86400 * 4
|
||||
);
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const notify = await this.shouldNotify(document, recipient.user);
|
||||
|
||||
@@ -174,7 +183,7 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipients = await this.getCollectionNotificationRecipients(
|
||||
const recipients = await NotificationHelper.getCollectionNotificationRecipients(
|
||||
collection,
|
||||
event.name
|
||||
);
|
||||
@@ -194,128 +203,10 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create any new subscriptions that might be missing for collaborators in the
|
||||
* document on publish and revision creation. This does mean that there is a
|
||||
* short period of time where the user is not subscribed after editing until a
|
||||
* revision is created.
|
||||
*
|
||||
* @param document The document to create subscriptions for
|
||||
* @param event The event that triggered the subscription creation
|
||||
*/
|
||||
private createDocumentSubscriptions = async (
|
||||
document: Document,
|
||||
event: DocumentEvent | RevisionEvent
|
||||
): Promise<void> => {
|
||||
await sequelize.transaction(async (transaction) => {
|
||||
const users = await document.collaborators({ transaction });
|
||||
|
||||
for (const user of users) {
|
||||
await subscriptionCreator({
|
||||
user,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
resubscribe: false,
|
||||
transaction,
|
||||
ip: event.ip,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the recipients of a notification for a collection event.
|
||||
*
|
||||
* @param collection The collection to get recipients for
|
||||
* @param eventName The event name
|
||||
* @returns A list of recipients
|
||||
*/
|
||||
private getCollectionNotificationRecipients = async (
|
||||
collection: Collection,
|
||||
eventName: string
|
||||
): Promise<NotificationSetting[]> => {
|
||||
// First find all the users that have notifications enabled for this event
|
||||
// type at all and aren't the one that performed the action.
|
||||
const recipients = await NotificationSetting.scope("withUser").findAll({
|
||||
where: {
|
||||
userId: {
|
||||
[Op.ne]: collection.createdById,
|
||||
},
|
||||
teamId: collection.teamId,
|
||||
event: eventName,
|
||||
},
|
||||
});
|
||||
|
||||
// Ensure we only have one recipient per user as a safety measure
|
||||
return uniqBy(recipients, "userId");
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the recipients of a notification for a document event.
|
||||
*
|
||||
* @param document The document to get recipients for
|
||||
* @param eventName The event name
|
||||
* @returns A list of recipients
|
||||
*/
|
||||
private getDocumentNotificationRecipients = async (
|
||||
document: Document,
|
||||
eventName: string
|
||||
): Promise<NotificationSetting[]> => {
|
||||
// First find all the users that have notifications enabled for this event
|
||||
// type at all and aren't the one that performed the action.
|
||||
let recipients = await NotificationSetting.scope("withUser").findAll({
|
||||
where: {
|
||||
userId: {
|
||||
[Op.ne]: document.lastModifiedById,
|
||||
},
|
||||
teamId: document.teamId,
|
||||
event: eventName,
|
||||
},
|
||||
});
|
||||
|
||||
// If the event is a revision creation we can filter further to only those
|
||||
// that have a subscription to the document…
|
||||
if (eventName === "documents.update") {
|
||||
const subscriptions = await Subscription.findAll({
|
||||
attributes: ["userId"],
|
||||
where: {
|
||||
userId: recipients.map((recipient) => recipient.user.id),
|
||||
documentId: document.id,
|
||||
event: eventName,
|
||||
},
|
||||
});
|
||||
|
||||
const subscribedUserIds = subscriptions.map(
|
||||
(subscription) => subscription.userId
|
||||
);
|
||||
|
||||
recipients = recipients.filter((recipient) =>
|
||||
subscribedUserIds.includes(recipient.user.id)
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure we only have one recipient per user as a safety measure
|
||||
return uniqBy(recipients, "userId");
|
||||
};
|
||||
|
||||
private shouldNotify = async (
|
||||
document: Document,
|
||||
user: User
|
||||
): Promise<boolean> => {
|
||||
// Suppress notifications for suspended and users with no email address
|
||||
if (user.isSuspended || !user.email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the recipient has access to the collection this document is in. Just
|
||||
// because they are subscribed doesn't meant they still have access to read
|
||||
// the document.
|
||||
const collectionIds = await user.collectionIds();
|
||||
|
||||
if (!collectionIds.includes(document.collectionId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deliver only a single notification in a 12 hour window
|
||||
const notification = await Notification.findOne({
|
||||
order: [["createdAt", "DESC"]],
|
||||
@@ -366,4 +257,33 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create any new subscriptions that might be missing for collaborators in the
|
||||
* document on publish and revision creation. This does mean that there is a
|
||||
* short period of time where the user is not subscribed after editing until a
|
||||
* revision is created.
|
||||
*
|
||||
* @param document The document to create subscriptions for
|
||||
* @param event The event that triggered the subscription creation
|
||||
*/
|
||||
private createDocumentSubscriptions = async (
|
||||
document: Document,
|
||||
event: DocumentEvent | RevisionEvent
|
||||
): Promise<void> => {
|
||||
await sequelize.transaction(async (transaction) => {
|
||||
const users = await document.collaborators({ transaction });
|
||||
|
||||
for (const user of users) {
|
||||
await subscriptionCreator({
|
||||
user,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
resubscribe: false,
|
||||
transaction,
|
||||
ip: event.ip,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user