// hive.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; 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'); class HiveService extends SiteService { constructor (dtp) { super(dtp, module.exports); } async subscribe (user, client, emitterId) { await UserSubscription.updateOne( { user: user._id }, { $addToSet: { subscriptions: { client: client._id, emitterId, }, }, }, { upsert: true, }, ); } async unsubscribe (user, subscription) { await UserSubscription.updateOne( { user: user._id }, { $pull: { subscriptions: subscription } }, ); } extractHashtags (content) { const hashtags = content .split(/ \r\n/g) .filter((tag) => tag[0] === '#') .map((tag) => slug(tag.slice(1))) ; return hashtags; } extractLinks (content) { let links = content .split(/( |\r|\n)/g) .filter((tag) => { const test = tag.trim().toLowerCase(); return test.startsWith('http://') || test.startsWith('https://'); }); return links; } async resolveLink (author, url) { const jobData = { authorType: author.type, author: author._id, url, }; this.log.info('creating job to resolve link', { jobData }); await this.resolver.add('resolve-link', jobData); } async processKaleidoscopeEvent (eventDefinition) { const { userNotification: userNotificationService, oauth2: oauth2Service, } = this.dtp.services; 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': eventDefinition.source.emitter._id, }) .select('-subscriptions') .cursor() .eachAsync(async (subscription) => { await userNotificationService.create(subscription.user, event); }, 3); 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; if (eventDefinition.recipientType && eventDefinition.recipient) { event.recipientType = eventDefinition.recipientType; event.recipient = mongoose.Types.ObjectId(eventDefinition.recipient); } 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()); if (eventDefinition.thumbnail) { event.thumbnail = striptags(eventDefinition.thumbnail); } event.source = { pkg: { name: striptags(eventDefinition.source.pkg.name.trim().toLowerCase()), version: striptags(eventDefinition.source.pkg.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()), }, }, }; if (eventDefinition.source.emitter) { event.source.emitter = { emitterType: striptags(eventDefinition.source.emitter.emitterType), emitterId: mongoose.Types.ObjectId(eventDefinition.source.emitter.emitterId), href: striptags(eventDefinition.source.emitter.href.trim()), }; if (eventDefinition.source.emitter.displayName) { event.source.emitter.displayName = striptags(eventDefinition.source.emitter.displayName.trim()); } if (eventDefinition.source.emitter.username) { event.source.emitter.username = striptags(eventDefinition.source.emitter.username.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; await event.save(); return event.toObject(); } async getConstellationTimeline (user, pagination) { const totalEventCount = await KaleidoscopeEvent.estimatedDocumentCount(); const job = { }; if (user) { job.search = { $or: [ { recipient: { $exists: false } }, { recipient: user._id }, ], }; } else { job.search = { recipient: { $exists: false } }; } const events = await KaleidoscopeEvent .find(job.search) .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .lean(); return { events, totalEventCount }; } } module.exports = { slug: 'hive', name: 'hive', create: (dtp) => { return new HiveService(dtp); }, };