// resource.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; const { SiteService } = require('../../lib/site-lib'); const geoip = require('geoip-lite'); const mongoose = require('mongoose'); const ResourceView = mongoose.model('ResourceView'); const ResourceVisit = mongoose.model('ResourceVisit'); class ResourceService extends SiteService { constructor (dtp) { super(dtp, module.exports); this.populateResourceView = [ { path: 'resource', }, ]; } /** * Records 24-hour unique view counts for a given resource happening on a * given ExpressJS Request. Views are uniqued by stripping time from the * current Date, and upserting a tracking object in MongoDB. * * @param {Request} req * @param {String} resourceType 'Post', 'Page', or 'Newsletter' * @param {mongoose.Types.ObjectId} resourceId The _id of the object for which * a view is being tracked. */ async recordView (req, resourceType, resourceId, res) { const Model = mongoose.model(resourceType); const modelUpdate = { $inc: { } }; const NOW = new Date(); const CURRENT_DAY = new Date(NOW); CURRENT_DAY.setHours(0, 0, 0, 0); let uniqueKey = req.ip.toString().trim().toLowerCase(); if (req.user) { if (resourceType === 'Post') { if (req.user._id.equals(res.locals.post.author._id)) { return; } } if (resourceType === 'Page') { if (req.user._id.equals(res.locals.page.author._id)) { return; } } uniqueKey += `:user:${req.user._id.toString()}`; } const response = await ResourceView.updateOne( { created: CURRENT_DAY, resourceType, resource: resourceId, uniqueKey, }, { $inc: { 'stats.visitCount': 1 }, }, { upsert: true }, ); if (response.upsertedCount > 0) { modelUpdate.$inc['stats.uniqueViewCount'] = 1; } /* * Record the ResourceVisit */ const visit = new ResourceVisit(); visit.created = NOW; visit.resourceType = resourceType; visit.resource = resourceId; if (req.user) { visit.user = req.user._id; } /* * We geo-analyze (but do not store) the IP address. */ const ipAddress = req.ip; const geo = geoip.lookup(ipAddress); if (geo) { visit.geoip = { country: geo.country, region: geo.region, eu: geo.eu, timezone: geo.timezone, city: geo.city, }; if (Array.isArray(geo.ll) && (geo.ll.length === 2)) { visit.geoip.location = { type: 'Point', coordinates: geo.ll, }; } } await visit.save(); modelUpdate.$inc['stats.totalVisitCount'] = 1; await Model.updateOne({ _id: resourceId }, modelUpdate); } async remove (resourceType, resource) { this.log.debug('removing resource view records', { resourceType, resourceId: resource._id }); await ResourceView.deleteMany({ resource: resource._id }); this.log.debug('removing resource visit records', { resourceType, resourceId: resource._id }); await ResourceVisit.deleteMany({ resource: resource._id }); this.log.debug('removing resource', { resourceType, resourceId: resource._id }); const Model = mongoose.model(resourceType); await Model.deleteOne({ _id: resource._id }); } } module.exports = { slug: 'resource', name: 'resource', create: (dtp) => { return new ResourceService(dtp); }, };