feat: Document subscriptions (#3834)
Co-authored-by: Tom Moor <tom.moor@gmail.com>
This commit is contained in:
committed by
GitHub
parent
864f585e5b
commit
24c71c38a5
@@ -1,5 +1,5 @@
|
||||
import DocumentNotificationEmail from "@server/emails/templates/DocumentNotificationEmail";
|
||||
import { View, NotificationSetting } from "@server/models";
|
||||
import { View, NotificationSetting, Subscription, Event } from "@server/models";
|
||||
import {
|
||||
buildDocument,
|
||||
buildCollection,
|
||||
@@ -108,9 +108,7 @@ describe("documents.publish", () => {
|
||||
describe("revisions.create", () => {
|
||||
test("should send a notification to other collaborators", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator = await buildUser({
|
||||
teamId: document.teamId,
|
||||
});
|
||||
const collaborator = await buildUser({ teamId: document.teamId });
|
||||
document.collaboratorIds = [collaborator.id];
|
||||
await document.save();
|
||||
await NotificationSetting.create({
|
||||
@@ -133,9 +131,7 @@ describe("revisions.create", () => {
|
||||
|
||||
test("should not send a notification if viewed since update", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator = await buildUser({
|
||||
teamId: document.teamId,
|
||||
});
|
||||
const collaborator = await buildUser({ teamId: document.teamId });
|
||||
document.collaboratorIds = [collaborator.id];
|
||||
await document.save();
|
||||
await NotificationSetting.create({
|
||||
@@ -181,4 +177,384 @@ describe("revisions.create", () => {
|
||||
});
|
||||
expect(DocumentNotificationEmail.schedule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should send a notification for subscriptions, even to collaborator", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator = await buildUser({ teamId: document.teamId });
|
||||
const subscriber = await buildUser({ teamId: document.teamId });
|
||||
|
||||
document.collaboratorIds = [collaborator.id, subscriber.id];
|
||||
|
||||
await document.save();
|
||||
|
||||
await NotificationSetting.create({
|
||||
userId: collaborator.id,
|
||||
teamId: collaborator.teamId,
|
||||
event: "documents.update",
|
||||
});
|
||||
|
||||
await Subscription.create({
|
||||
userId: subscriber.id,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(DocumentNotificationEmail.schedule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should create subscriptions for collaborator", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator0 = await buildUser({ teamId: document.teamId });
|
||||
const collaborator1 = await buildUser({ teamId: document.teamId });
|
||||
const collaborator2 = await buildUser({ teamId: document.teamId });
|
||||
|
||||
document.collaboratorIds = [
|
||||
collaborator0.id,
|
||||
collaborator1.id,
|
||||
collaborator2.id,
|
||||
];
|
||||
|
||||
await document.save();
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator0.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
const events = await Event.findAll();
|
||||
|
||||
// Should emit 3 `subscriptions.create` events.
|
||||
expect(events.length).toEqual(3);
|
||||
expect(events[0].name).toEqual("subscriptions.create");
|
||||
expect(events[1].name).toEqual("subscriptions.create");
|
||||
expect(events[2].name).toEqual("subscriptions.create");
|
||||
|
||||
// Each event should point to same document.
|
||||
expect(events[0].documentId).toEqual(document.id);
|
||||
expect(events[1].documentId).toEqual(document.id);
|
||||
expect(events[2].documentId).toEqual(document.id);
|
||||
|
||||
// Events should mention correct `userId`.
|
||||
expect(events[0].userId).toEqual(collaborator0.id);
|
||||
expect(events[1].userId).toEqual(collaborator1.id);
|
||||
expect(events[2].userId).toEqual(collaborator2.id);
|
||||
|
||||
// Should send email notification.
|
||||
expect(DocumentNotificationEmail.schedule).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("should not send multiple emails", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator0 = await buildUser({ teamId: document.teamId });
|
||||
const collaborator1 = await buildUser({ teamId: document.teamId });
|
||||
const collaborator2 = await buildUser({ teamId: document.teamId });
|
||||
|
||||
document.collaboratorIds = [
|
||||
collaborator0.id,
|
||||
collaborator1.id,
|
||||
collaborator2.id,
|
||||
];
|
||||
|
||||
await document.save();
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
// Changing document will emit a `documents.update` event.
|
||||
await processor.perform({
|
||||
name: "documents.update",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
createdAt: document.updatedAt.toString(),
|
||||
teamId: document.teamId,
|
||||
data: { title: document.title, autosave: false, done: true },
|
||||
actorId: collaborator2.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
// Those changes will also emit a `revisions.create` event.
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator0.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
const events = await Event.findAll();
|
||||
|
||||
// Should emit 3 `subscriptions.create` events.
|
||||
expect(events.length).toEqual(3);
|
||||
expect(events[0].name).toEqual("subscriptions.create");
|
||||
expect(events[1].name).toEqual("subscriptions.create");
|
||||
expect(events[2].name).toEqual("subscriptions.create");
|
||||
|
||||
// Each event should point to same document.
|
||||
expect(events[0].documentId).toEqual(document.id);
|
||||
expect(events[1].documentId).toEqual(document.id);
|
||||
expect(events[2].documentId).toEqual(document.id);
|
||||
|
||||
// Events should mention correct `userId`.
|
||||
expect(events[0].userId).toEqual(collaborator0.id);
|
||||
expect(events[1].userId).toEqual(collaborator1.id);
|
||||
expect(events[2].userId).toEqual(collaborator2.id);
|
||||
|
||||
// This should send out 3 emails, one for each collaborator,
|
||||
// and not 6, for both `documents.update` and `revisions.create`.
|
||||
expect(DocumentNotificationEmail.schedule).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("should not create subscriptions if previously unsubscribed", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator0 = await buildUser({ teamId: document.teamId });
|
||||
const collaborator1 = await buildUser({ teamId: document.teamId });
|
||||
const collaborator2 = await buildUser({ teamId: document.teamId });
|
||||
|
||||
document.collaboratorIds = [
|
||||
collaborator0.id,
|
||||
collaborator1.id,
|
||||
collaborator2.id,
|
||||
];
|
||||
|
||||
await document.save();
|
||||
|
||||
// `collaborator2` created a subscription.
|
||||
const subscription2 = await Subscription.create({
|
||||
userId: collaborator2.id,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
});
|
||||
|
||||
// `collaborator2` would no longer like to be notified.
|
||||
await subscription2.destroy();
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator0.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
const events = await Event.findAll();
|
||||
|
||||
// Should emit 2 `subscriptions.create` events.
|
||||
expect(events.length).toEqual(2);
|
||||
expect(events[0].name).toEqual("subscriptions.create");
|
||||
expect(events[1].name).toEqual("subscriptions.create");
|
||||
|
||||
// Each event should point to same document.
|
||||
expect(events[0].documentId).toEqual(document.id);
|
||||
expect(events[1].documentId).toEqual(document.id);
|
||||
|
||||
// Events should mention correct `userId`.
|
||||
expect(events[0].userId).toEqual(collaborator0.id);
|
||||
expect(events[1].userId).toEqual(collaborator1.id);
|
||||
|
||||
expect(DocumentNotificationEmail.schedule).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test("should send a notification for subscriptions to non-collaborators", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator = await buildUser({ teamId: document.teamId });
|
||||
const subscriber = await buildUser({ teamId: document.teamId });
|
||||
|
||||
// `subscriber` hasn't collaborated on `document`.
|
||||
document.collaboratorIds = [collaborator.id];
|
||||
|
||||
await document.save();
|
||||
|
||||
await NotificationSetting.create({
|
||||
userId: collaborator.id,
|
||||
teamId: collaborator.teamId,
|
||||
event: "documents.update",
|
||||
});
|
||||
|
||||
// `subscriber` subscribes to `document`'s changes.
|
||||
// Specifically "documents.update" event.
|
||||
await Subscription.create({
|
||||
userId: subscriber.id,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(DocumentNotificationEmail.schedule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should not send a notification for subscriptions to collaborators if unsubscribed", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator = await buildUser({ teamId: document.teamId });
|
||||
const subscriber = await buildUser({ teamId: document.teamId });
|
||||
|
||||
// `subscriber` has collaborated on `document`.
|
||||
document.collaboratorIds = [collaborator.id, subscriber.id];
|
||||
|
||||
await document.save();
|
||||
|
||||
await NotificationSetting.create({
|
||||
userId: collaborator.id,
|
||||
teamId: collaborator.teamId,
|
||||
event: "documents.update",
|
||||
});
|
||||
|
||||
// `subscriber` subscribes to `document`'s changes.
|
||||
// Specifically "documents.update" event.
|
||||
const subscription = await Subscription.create({
|
||||
userId: subscriber.id,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
subscription.destroy();
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
// Should send notification to `collaborator` and not `subscriber`.
|
||||
expect(DocumentNotificationEmail.schedule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should not send a notification for subscriptions to members outside of the team", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator = await buildUser({ teamId: document.teamId });
|
||||
|
||||
// `subscriber` *does not* belong
|
||||
// to `collaborator`'s team,
|
||||
const subscriber = await buildUser();
|
||||
|
||||
// `subscriber` hasn't collaborated on `document`.
|
||||
document.collaboratorIds = [collaborator.id];
|
||||
|
||||
await document.save();
|
||||
|
||||
await NotificationSetting.create({
|
||||
userId: collaborator.id,
|
||||
teamId: collaborator.teamId,
|
||||
event: "documents.update",
|
||||
});
|
||||
|
||||
// `subscriber` subscribes to `document`'s changes.
|
||||
// Specifically "documents.update" event.
|
||||
// Not sure how they got hold of this document,
|
||||
// but let's just pretend they did!
|
||||
await Subscription.create({
|
||||
userId: subscriber.id,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
|
||||
// Should send notification to `collaborator` and not `subscriber`.
|
||||
expect(DocumentNotificationEmail.schedule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should not send a notification if viewed since update", async () => {
|
||||
const document = await buildDocument();
|
||||
const collaborator = await buildUser({ teamId: document.teamId });
|
||||
document.collaboratorIds = [collaborator.id];
|
||||
await document.save();
|
||||
await NotificationSetting.create({
|
||||
userId: collaborator.id,
|
||||
teamId: collaborator.teamId,
|
||||
event: "documents.update",
|
||||
});
|
||||
await View.touch(document.id, collaborator.id, true);
|
||||
|
||||
const processor = new NotificationsProcessor();
|
||||
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: collaborator.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
expect(DocumentNotificationEmail.schedule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should not send a notification to last editor", async () => {
|
||||
const user = await buildUser();
|
||||
const document = await buildDocument({
|
||||
teamId: user.teamId,
|
||||
lastModifiedById: user.id,
|
||||
});
|
||||
await NotificationSetting.create({
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
event: "documents.update",
|
||||
});
|
||||
const processor = new NotificationsProcessor();
|
||||
await processor.perform({
|
||||
name: "revisions.create",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
modelId: document.id,
|
||||
ip,
|
||||
});
|
||||
expect(DocumentNotificationEmail.schedule).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { uniqBy } from "lodash";
|
||||
import { Op } from "sequelize";
|
||||
import subscriptionCreator from "@server/commands/subscriptionCreator";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import CollectionNotificationEmail from "@server/emails/templates/CollectionNotificationEmail";
|
||||
import DocumentNotificationEmail from "@server/emails/templates/DocumentNotificationEmail";
|
||||
import Logger from "@server/logging/Logger";
|
||||
@@ -9,12 +12,14 @@ import {
|
||||
Collection,
|
||||
User,
|
||||
NotificationSetting,
|
||||
Subscription,
|
||||
} from "@server/models";
|
||||
import { can } from "@server/policies";
|
||||
import {
|
||||
DocumentEvent,
|
||||
CollectionEvent,
|
||||
RevisionEvent,
|
||||
Event,
|
||||
DocumentEvent,
|
||||
} from "@server/types";
|
||||
import BaseProcessor from "./BaseProcessor";
|
||||
|
||||
@@ -44,141 +49,217 @@ export default class NotificationsProcessor extends BaseProcessor {
|
||||
if (event.data?.source === "import") {
|
||||
return;
|
||||
}
|
||||
|
||||
const [collection, document, team] = await Promise.all([
|
||||
Collection.findByPk(event.collectionId),
|
||||
Document.findByPk(event.documentId),
|
||||
Team.findByPk(event.teamId),
|
||||
]);
|
||||
|
||||
if (!document || !team || !collection) {
|
||||
return;
|
||||
}
|
||||
const notificationSettings = await NotificationSetting.findAll({
|
||||
where: {
|
||||
userId: {
|
||||
[Op.ne]: document.lastModifiedById,
|
||||
},
|
||||
teamId: document.teamId,
|
||||
event:
|
||||
event.name === "documents.publish"
|
||||
? "documents.publish"
|
||||
: "documents.update",
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
required: true,
|
||||
as: "user",
|
||||
},
|
||||
],
|
||||
});
|
||||
const eventName =
|
||||
event.name === "documents.publish" ? "published" : "updated";
|
||||
|
||||
for (const setting of notificationSettings) {
|
||||
// Suppress notifications for suspended users
|
||||
if (setting.user.isSuspended) {
|
||||
continue;
|
||||
await this.createDocumentSubscriptions(document, event);
|
||||
|
||||
const recipients = await this.getDocumentNotificationRecipients(
|
||||
document,
|
||||
event.name === "documents.publish"
|
||||
? "documents.publish"
|
||||
: "documents.update"
|
||||
);
|
||||
|
||||
for (const recipient of recipients) {
|
||||
const notify = await this.shouldNotify(document, recipient.user);
|
||||
|
||||
if (notify) {
|
||||
await DocumentNotificationEmail.schedule({
|
||||
to: recipient.user.email,
|
||||
eventName:
|
||||
event.name === "documents.publish" ? "published" : "updated",
|
||||
documentId: document.id,
|
||||
teamUrl: team.url,
|
||||
actorName: document.updatedBy.name,
|
||||
collectionName: collection.name,
|
||||
unsubscribeUrl: recipient.unsubscribeUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// For document updates we only want to send notifications if
|
||||
// the document has been edited by the user with this notification setting
|
||||
// This could be replaced with ability to "follow" in the future
|
||||
if (
|
||||
eventName === "updated" &&
|
||||
!document.collaboratorIds.includes(setting.userId)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the user has access to the collection this document is in. Just
|
||||
// because they were a collaborator once doesn't mean they still are.
|
||||
const collectionIds = await setting.user.collectionIds();
|
||||
|
||||
if (!collectionIds.includes(document.collectionId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If this user has viewed the document since the last update was made
|
||||
// then we can avoid sending them a useless notification, yay.
|
||||
const view = await View.findOne({
|
||||
where: {
|
||||
userId: setting.userId,
|
||||
documentId: event.documentId,
|
||||
updatedAt: {
|
||||
[Op.gt]: document.updatedAt,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (view) {
|
||||
Logger.info(
|
||||
"processor",
|
||||
`suppressing notification to ${setting.userId} because update viewed`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!setting.user.email) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await DocumentNotificationEmail.schedule({
|
||||
to: setting.user.email,
|
||||
eventName,
|
||||
documentId: document.id,
|
||||
teamUrl: team.url,
|
||||
actorName: document.updatedBy.name,
|
||||
collectionName: collection.name,
|
||||
unsubscribeUrl: setting.unsubscribeUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async collectionCreated(event: CollectionEvent) {
|
||||
const collection = await Collection.findByPk(event.collectionId, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
required: true,
|
||||
as: "user",
|
||||
},
|
||||
],
|
||||
const collection = await Collection.scope("withUser").findByPk(
|
||||
event.collectionId
|
||||
);
|
||||
|
||||
if (!collection || !collection.permission) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recipients = await this.getCollectionNotificationRecipients(
|
||||
collection,
|
||||
event.name
|
||||
);
|
||||
|
||||
for (const recipient of recipients) {
|
||||
// Suppress notifications for suspended users
|
||||
if (recipient.user.isSuspended || !recipient.user.email) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await CollectionNotificationEmail.schedule({
|
||||
to: recipient.user.email,
|
||||
eventName: "created",
|
||||
collectionId: collection.id,
|
||||
unsubscribeUrl: recipient.unsubscribeUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (user && can(user, "subscribe", document)) {
|
||||
await subscriptionCreator({
|
||||
user,
|
||||
documentId: document.id,
|
||||
event: "documents.update",
|
||||
resubscribe: false,
|
||||
transaction,
|
||||
ip: event.ip,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
if (!collection.permission) {
|
||||
return;
|
||||
}
|
||||
const notificationSettings = await NotificationSetting.findAll({
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: event.name,
|
||||
event: eventName,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
required: true,
|
||||
as: "user",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
for (const setting of notificationSettings) {
|
||||
// Suppress notifications for suspended users
|
||||
if (setting.user.isSuspended || !setting.user.email) {
|
||||
continue;
|
||||
}
|
||||
// Ensure we only have one recipient per user as a safety measure
|
||||
return uniqBy(recipients, "userId");
|
||||
};
|
||||
|
||||
await CollectionNotificationEmail.schedule({
|
||||
to: setting.user.email,
|
||||
eventName: "created",
|
||||
collectionId: collection.id,
|
||||
unsubscribeUrl: setting.unsubscribeUrl,
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// If this recipient has viewed the document since the last update was made
|
||||
// then we can avoid sending them a useless notification, yay.
|
||||
const view = await View.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
documentId: document.id,
|
||||
updatedAt: {
|
||||
[Op.gt]: document.updatedAt,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (view) {
|
||||
Logger.info(
|
||||
"processor",
|
||||
`suppressing notification to ${user.id} because update viewed`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
|
||||
case "api_keys.delete":
|
||||
case "attachments.create":
|
||||
case "attachments.delete":
|
||||
case "subscriptions.create":
|
||||
case "subscriptions.delete":
|
||||
case "authenticationProviders.update":
|
||||
// Ignored
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user