From fc7550e196165409b52a0a305ebcd3557fba6f84 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 21 Jul 2022 00:57:19 -0400 Subject: [PATCH] factored KaleidoscopeEvent out of UserNotification --- app/models/kaleidoscope-event.js | 53 +++++++ app/models/user-notification.js | 33 +---- app/services/hive.js | 140 +++++++++++++++++- app/services/user-notification.js | 134 +---------------- .../notification/components/notification.pug | 4 +- 5 files changed, 201 insertions(+), 163 deletions(-) create mode 100644 app/models/kaleidoscope-event.js diff --git a/app/models/kaleidoscope-event.js b/app/models/kaleidoscope-event.js new file mode 100644 index 0000000..f601386 --- /dev/null +++ b/app/models/kaleidoscope-event.js @@ -0,0 +1,53 @@ +// kaleidoscope-event.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const KaleidoscopeEventSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: -1, expires: '30d' }, + action: { type: String, required: true, lowercase: true }, + label: { type: String }, + content: { type: String }, + href: { type: String }, + source: { + pkg: { + name: { type: String, required: true }, + version: { type: String, required: true }, + }, + site: { + name: { type: String, required: true }, + description: { type: String }, + domain: { type: String, lowercase: true, required: true }, + domainKey: { type: String, lowercase: true, required: true }, + company: { type: String }, + coreAuth: { + scopes: { type: [String] }, + }, + }, + author: { + userId: { type: Schema.ObjectId, required: true }, + displayName: { type: String }, + username: { type: String }, + href: { type: String }, + }, + }, + attachmentType: { type: String }, + attachment: { type: Schema.ObjectId, refPath: 'attachmentType' }, +}); + +/* + * Compound index for site.domainKey, author.userId for when local members + * subscribe to a specific author on a domain, or the whole domain. + */ +KaleidoscopeEventSchema.index({ + 'source.site.domainKey': 1, + 'source.author.userId': 1, +}, { + name: 'evtsrc_site_author_index', +}); + +module.exports = mongoose.model('KaleidoscopeEvent', KaleidoscopeEventSchema); \ No newline at end of file diff --git a/app/models/user-notification.js b/app/models/user-notification.js index efaf68d..c10620d 100644 --- a/app/models/user-notification.js +++ b/app/models/user-notification.js @@ -7,38 +7,15 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; +/* + * A notification is really just a user-specific bookmark to an event we know + * the user is interested in. These are the "timelines" presented to members. + */ const UserNotificationSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: -1, expires: '7d' }, user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, status: { type: String, enum: ['new', 'seen'], default: 'new', required: true }, - action: { type: String, required: true, lowercase: true }, - label: { type: String }, - content: { type: String }, - href: { type: String }, - source: { - pkg: { - name: { type: String, required: true }, - version: { type: String, required: true }, - }, - site: { - name: { type: String, required: true }, - description: { type: String }, - domain: { type: String, lowercase: true, required: true }, - domainKey: { type: String, lowercase: true, required: true }, - company: { type: String }, - coreAuth: { - scopes: { type: [String] }, - }, - }, - author: { - userId: { type: Schema.ObjectId, required: true }, - displayName: { type: String }, - username: { type: String }, - href: { type: String }, - }, - }, - attachmentType: { type: String }, - attachment: { type: Schema.ObjectId, refPath: 'attachmentType' }, + event: { type: Schema.ObjectId, required: true, ref: 'KaleidoscopeEvent' }, }); module.exports = mongoose.model('UserNotification', UserNotificationSchema); \ No newline at end of file diff --git a/app/services/hive.js b/app/services/hive.js index 5820b8d..481e6e0 100644 --- a/app/services/hive.js +++ b/app/services/hive.js @@ -7,8 +7,10 @@ const mongoose = require('mongoose'); const UserSubscription = mongoose.model('UserSubscription'); +const KaleidoscopeEvent = mongoose.model('KaleidoscopeEvent'); const slug = require('slug'); +const striptags = require('striptags'); const { SiteService, SiteError } = require('../../lib/site-lib'); @@ -72,21 +74,22 @@ class HiveService extends SiteService { await this.resolver.add('resolve-link', jobData); } - async processKaleidoscopeEvent (event) { + async processKaleidoscopeEvent (eventDefinition) { const { userNotification: userNotificationService, oauth2: oauth2Service, } = this.dtp.services; - const client = await oauth2Service.getClientByDomainKey(event.source.site.domainKey); + const client = await oauth2Service.getClientByDomainKey(eventDefinition.source.site.domainKey); if (!client) { throw new SiteError(403, 'Unknown client domain key'); } + const event = await this.createKaleidoscopeEvent(eventDefinition); await UserSubscription .find({ 'subscriptions.client': client._id, - 'subscriptions.emitterId': event.source.emitter._id, + 'subscriptions.emitterId': eventDefinition.source.emitter._id, }) .select('-subscriptions') .cursor() @@ -96,6 +99,137 @@ class HiveService extends SiteService { this.emit('kaleidoscope:event', event, client); } + + async createKaleidoscopeEvent (eventDefinition) { + const NOW = new Date(); + + /* + * Validate general notification data + */ + + if (!eventDefinition.action) { + throw new SiteError(406, 'Missing action'); + } + if (!eventDefinition.content) { + throw new SiteError(406, 'Missing content'); + } + if (!eventDefinition.href) { + throw new SiteError(406, 'Missing href'); + } + + /* + * Validate source data + */ + + if (!eventDefinition.source) { + throw new SiteError(406, 'Missing source information'); + } + + /* + * Validate source site + */ + + if (!eventDefinition.source.site) { + throw new SiteError(406, 'Missing source site information'); + } + if (!eventDefinition.source.site.name) { + throw new SiteError(406, 'Missing source site name'); + } + if (!eventDefinition.source.site.description) { + throw new SiteError(406, 'Missing source site description'); + } + if (!eventDefinition.source.site.domain) { + throw new SiteError(406, 'Missing source site domain'); + } + if (!eventDefinition.source.site.domainKey) { + throw new SiteError(406, 'Missing source site domain key'); + } + if (!eventDefinition.source.site.company) { + throw new SiteError(406, 'Missing source site company name'); + } + if (!eventDefinition.source.site.coreAuth || + !eventDefinition.source.site.coreAuth.scopes || + !Array.isArray(eventDefinition.source.site.coreAuth.scopes) || + (eventDefinition.source.site.coreAuth.scopes.length === 0)) { + throw new SiteError(406, 'Missing source site Core auth or scope information'); + } + + /* + * Validate source package + */ + + if (!eventDefinition.source.pkg) { + throw new SiteError(406, 'Missing source package information'); + } + if (!eventDefinition.source.pkg.name) { + throw new SiteError(406, 'Missing source package name'); + } + if (!eventDefinition.source.pkg.version) { + throw new SiteError(406, 'Missing source package version'); + } + + /* + * Validate source emitter + */ + + if (!eventDefinition.source.emitter) { + throw new SiteError(406, 'Missing source emitter information'); + } + if (!eventDefinition.source.emitter.emitterId) { + throw new SiteError(406, 'Missing source emitter ID'); + } + if (!eventDefinition.source.emitter.username) { + throw new SiteError(406, 'Missing source emitter username'); + } + if (!eventDefinition.source.emitter.href) { + throw new SiteError(406, 'Missing source emitter href'); + } + + const event = new KaleidoscopeEvent(); + event.created = NOW; + + event.action = striptags(eventDefinition.action.trim().toLowerCase()); + if (eventDefinition.label) { + event.label = striptags(eventDefinition.label.trim()); + } + event.content = striptags(eventDefinition.content.trim()); + event.href = striptags(eventDefinition.href.trim()); + + event.source = { + pkg: { + name: striptags(eventDefinition.source.pkt.name.trim().toLowerCase()), + version: striptags(eventDefinition.source.pkt.version.trim()), + }, + site: { + name: striptags(eventDefinition.source.site.name.trim()), + domain: striptags(eventDefinition.source.site.domain.trim().toLowerCase()), + domainKey: striptags(eventDefinition.source.site.domainKey.trim().toLowerCase()), + coreAuth: { + scopes: eventDefinition.source.site.coreAuth.scopes.map((scope) => scope.trim().toLowerCase()), + }, + }, + emitter: { + emitterId: mongoose.Types.ObjectId(eventDefinition.source.emitter.emitterId), + username: striptags(eventDefinition.source.emitter.username.trim()), + href: striptags(eventDefinition.source.emitter.href.trim()), + }, + }; + + if (eventDefinition.source.site.company) { + event.source.site.company = striptags(eventDefinition.source.site.company.trim()); + } + if (eventDefinition.source.site.description) { + event.source.site.description = striptags(eventDefinition.source.site.description.trim()); + } + + if (eventDefinition.source.emitter.displayName) { + event.source.emitter.displayName = striptags(eventDefinition.source.emitter.displayName); + } + + event.attachmentType = eventDefinition.attachmentType; + event.attachment = eventDefinition.attachment; + + } } module.exports = { diff --git a/app/services/user-notification.js b/app/services/user-notification.js index 3bf0283..922a280 100644 --- a/app/services/user-notification.js +++ b/app/services/user-notification.js @@ -10,9 +10,8 @@ const mongoose = require('mongoose'); const UserNotification = mongoose.model('UserNotification'); const pug = require('pug'); -const striptags = require('striptags'); -const { SiteService, SiteError } = require('../../lib/site-lib'); +const { SiteService } = require('../../lib/site-lib'); class UserNotificationService extends SiteService { @@ -34,137 +33,12 @@ class UserNotificationService extends SiteService { this.templates.notification = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'notification', 'components', 'notification-standalone.pug')); } - async create (user, notificationDefinition) { - const NOW = new Date(); - - /* - * Validate general notification data - */ - - if (!notificationDefinition.action) { - throw new SiteError(406, 'Missing action'); - } - if (!notificationDefinition.content) { - throw new SiteError(406, 'Missing content'); - } - if (!notificationDefinition.href) { - throw new SiteError(406, 'Missing href'); - } - - /* - * Validate source data - */ - - if (!notificationDefinition.source) { - throw new SiteError(406, 'Missing source information'); - } - - /* - * Validate source site - */ - - if (!notificationDefinition.source.site) { - throw new SiteError(406, 'Missing source site information'); - } - if (!notificationDefinition.source.site.name) { - throw new SiteError(406, 'Missing source site name'); - } - if (!notificationDefinition.source.site.description) { - throw new SiteError(406, 'Missing source site description'); - } - if (!notificationDefinition.source.site.domain) { - throw new SiteError(406, 'Missing source site domain'); - } - if (!notificationDefinition.source.site.domainKey) { - throw new SiteError(406, 'Missing source site domain key'); - } - if (!notificationDefinition.source.site.company) { - throw new SiteError(406, 'Missing source site company name'); - } - if (!notificationDefinition.source.site.coreAuth || - !notificationDefinition.source.site.coreAuth.scopes || - !Array.isArray(notificationDefinition.source.site.coreAuth.scopes) || - (notificationDefinition.source.site.coreAuth.scopes.length === 0)) { - throw new SiteError(406, 'Missing source site Core auth or scope information'); - } - - /* - * Validate source package - */ - - if (!notificationDefinition.source.pkg) { - throw new SiteError(406, 'Missing source package information'); - } - if (!notificationDefinition.source.pkg.name) { - throw new SiteError(406, 'Missing source package name'); - } - if (!notificationDefinition.source.pkg.version) { - throw new SiteError(406, 'Missing source package version'); - } - - /* - * Validate source emitter - */ - - if (!notificationDefinition.source.emitter) { - throw new SiteError(406, 'Missing source emitter information'); - } - if (!notificationDefinition.source.emitter.emitterId) { - throw new SiteError(406, 'Missing source emitter ID'); - } - if (!notificationDefinition.source.emitter.username) { - throw new SiteError(406, 'Missing source emitter username'); - } - if (!notificationDefinition.source.emitter.href) { - throw new SiteError(406, 'Missing source emitter href'); - } - + async create (user, event) { const notification = new UserNotification(); - notification.created = NOW; + notification.created = event.created; notification.user = user._id; notification.status = 'new'; - - notification.action = striptags(notificationDefinition.action.trim().toLowerCase()); - if (notificationDefinition.label) { - notification.label = striptags(notificationDefinition.label.trim()); - } - notification.content = striptags(notificationDefinition.content.trim()); - notification.href = striptags(notificationDefinition.href.trim()); - - notification.source = { - pkg: { - name: striptags(notificationDefinition.source.pkt.name.trim().toLowerCase()), - version: striptags(notificationDefinition.source.pkt.version.trim()), - }, - site: { - name: striptags(notificationDefinition.source.site.name.trim()), - domain: striptags(notificationDefinition.source.site.domain.trim().toLowerCase()), - domainKey: striptags(notificationDefinition.source.site.domainKey.trim().toLowerCase()), - coreAuth: { - scopes: notificationDefinition.source.site.coreAuth.scopes.map((scope) => scope.trim().toLowerCase()), - }, - }, - emitter: { - emitterId: mongoose.Types.ObjectId(notificationDefinition.source.emitter.emitterId), - username: striptags(notificationDefinition.source.emitter.username.trim()), - href: striptags(notificationDefinition.source.emitter.href.trim()), - }, - }; - - if (notificationDefinition.source.site.company) { - notification.source.site.company = striptags(notificationDefinition.source.site.company.trim()); - } - if (notificationDefinition.source.site.description) { - notification.source.site.description = striptags(notificationDefinition.source.site.description.trim()); - } - - if (notificationDefinition.source.emitter.displayName) { - notification.source.emitter.displayName = striptags(notificationDefinition.source.emitter.displayName); - } - - notification.attachmentType = notificationDefinition.attachmentType; - notification.attachment = notificationDefinition.attachment; - + notification.event = event._id; await notification.save(); return notification.toObject(); diff --git a/app/views/notification/components/notification.pug b/app/views/notification/components/notification.pug index cca4b70..8ac59ed 100644 --- a/app/views/notification/components/notification.pug +++ b/app/views/notification/components/notification.pug @@ -1,5 +1,5 @@ mixin renderUserNotification (userNotification) div div= moment(userNotification.created).fromNow() - div= userNotification.source - div= userNotification.message \ No newline at end of file + div= userNotification.event.source + div= userNotification.event.message \ No newline at end of file