diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..64735e67 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + env: { + browser: true, + commonjs: true, + es6: true + }, + extends: [ + 'standard' + ], + globals: { + Atomics: 'readonly', + SharedArrayBuffer: 'readonly' + }, + parserOptions: { + ecmaVersion: 2018 + }, + rules: { + } +} diff --git a/README.md b/README.md index 496865ee..80be87b4 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,68 @@ The code is uploaded here in the hopes it can be made more secure, more efficien If you see a serious safety breach, particularly one involving user data, please submit an issue or - even better - a fix, and I'll upload the updated code to the server as soon as I can. Your support is genuinely appreciated. sweet has been an extremely uphill learning experience for me - I'm entirely self-taught, and am acutely aware that this code is a mess in need of major editing and potentially re-writing. If you'd like to help out, you can find some planned and in-progress work on the [Trello](https://trello.com/b/wzCmHAqi/sweet-development) and more current stuff on the incomplete [functional specification](https://docs.google.com/document/d/1R6jw7jHLAzM-PkLaNzbyalOqKVmUIEzNyhNHpMMlj10/edit?usp=sharing) - get in touch, because I'd love your help. + +## Running in local dev + +Install mongodb however you prefer. On OS X with Homebrew, that's: + +``` +brew install mongodb-community +brew services start mongodb-community +``` + +Then run `yarn` to install dependencies, then `yarn start` to start the server. Go to `http://localhost:8686` to see it running! + +If you do anything that triggers an email, it won't get sent for real in local dev, it'll just get output into your console. + +You need to prepopulate the users collection with Sweetbot to let logins work. Something like: + +``` +db.users.insert({ + _id: ObjectId("5c962bccf0b0d14286e99b68"), + joined: Date.now(), + lastOnline: Date.now(), + lastUpdated: Date.now(), + isVerified: false, + verificationToken: '', + verificationTokenExpiry: Date.now(), + email: 'sweetbo@example.com', + username: 'sweetbot', + password: 'invalid password hash', + passwordResetToken: '', + passwordResetTokenExpiry: Date.now(), + image: '', + imageEnabled: false, + displayName: 'SweetBot', + pronouns: 'bot/bot', + aboutRaw: '', + aboutParsed: '', + websiteRaw: '', + websiteParsed: '', + location: '', + notifications: [], + pushNotifSubscriptions: [], + communities: [], + bannedCommunities: [], + mutedCommunities: [], + hiddenRecommendedUsers: [], + hiddenRecommendedCommunities: [], + settings: { + timezone: "auto", + autoDetectedTimeZone: "", + profileVisibility: "invisible", + newPostPrivacy: "public", + imageQuality: "standard", + homeTagTimelineSorting: "fluid", + userTimelineSorting: "chronological", + communityTimelineSorting: "fluid", + flashRecentComments: true, + digestEmailFrequency: "off", + emailTime: "17:00", + emailDay: "Sunday", + showRecommendations: true, + showHashtags: true, + sendMentionEmails: true, + } +}) +``` diff --git a/app/emailer.js b/app/emailer.js index d9c97f03..192f7bbb 100644 --- a/app/emailer.js +++ b/app/emailer.js @@ -1,179 +1,179 @@ -const nodemailer = require("nodemailer"); -const nodemailerHbs = require('nodemailer-express-handlebars'); -const moment = require('moment-timezone'); -const auth = require(global.appRoot + '/config/auth.js'); +const nodemailer = require('nodemailer') +const nodemailerHbs = require('nodemailer-express-handlebars') +const moment = require('moment-timezone') +const auth = require(global.appRoot + '/config/auth.js') // create reusable transporter object using the default SMTP transport transporter = nodemailer.createTransport({ - host: "box.raphaelkabo.com", - port: 587, - secure: false, // true for 465, false for other ports - auth: { - user: 'updates@sweet.sh', - pass: auth.mailServer - } -}); + host: 'box.raphaelkabo.com', + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: 'updates@sweet.sh', + pass: auth.mailServer + } +}) // verify connection configuration transporter.verify(function (error, success) { - if (error) { - emailLog("email server connection error! " + error); - } else { - emailLog("Server is ready to take our messages! " + success); - } -}); + if (error) { + emailLog('email server connection error! ' + error) + } else { + emailLog('Server is ready to take our messages! ' + success) + } +}) nodemailerHbsOptions = { - viewEngine: { - extName: ".handlebars", - partialsDir: global.appRoot + "/views/emails", - defaultLayout: false // <----- added this - }, - viewPath: global.appRoot + "/views/emails", - extName: ".handlebars" -}; - -transporter.use('compile', nodemailerHbs(nodemailerHbsOptions)); - -function emailLog(message) { - if(process.env.NODE_ENV == "production"){ - console.log(message); - } - fs.appendFileSync("emailLog.txt", message + '\n'); + viewEngine: { + extName: '.handlebars', + partialsDir: global.appRoot + '/views/emails', + defaultLayout: false // <----- added this + }, + viewPath: global.appRoot + '/views/emails', + extName: '.handlebars' } -var scheduledEmails = {}; //will store timeout objects representing scheduled calls to sendUpdateEmail and execution of next email scheduling code -var logFormat = "dddd, MMMM Do YYYY, h:mm a"; +transporter.use('compile', nodemailerHbs(nodemailerHbsOptions)) -//utility function. note that this transforms the input object "in place", rather than returning the changed version -function putInUsersLocalTime(momentObject, user) { - if (user.settings.timezone == "auto") { - momentObject.tz(user.settings.autoDetectedTimeZone); - } else { - momentObject.utcOffset(user.settings.timezone); - } +function emailLog (message) { + if (process.env.NODE_ENV == 'production') { + console.log(message) + } + fs.appendFileSync('emailLog.txt', message + '\n') } -//whenever the server boots, schedule some mf emails -emailLog('\n\n---------server booting up ' + moment().format(logFormat) + ', all above log entries can be considered null and void---------\n'); +var scheduledEmails = {} // will store timeout objects representing scheduled calls to sendUpdateEmail and execution of next email scheduling code +var logFormat = 'dddd, MMMM Do YYYY, h:mm a' + +// utility function. note that this transforms the input object "in place", rather than returning the changed version +function putInUsersLocalTime (momentObject, user) { + if (user.settings.timezone == 'auto') { + momentObject.tz(user.settings.autoDetectedTimeZone) + } else { + momentObject.utcOffset(user.settings.timezone) + } +} + +// whenever the server boots, schedule some mf emails +emailLog('\n\n---------server booting up ' + moment().format(logFormat) + ', all above log entries can be considered null and void---------\n') User.find({ - $or: [{ - 'settings.digestEmailFrequency': 'daily' - }, - { - 'settings.digestEmailFrequency': 'weekly' - } - ] + $or: [{ + 'settings.digestEmailFrequency': 'daily' + }, + { + 'settings.digestEmailFrequency': 'weekly' + } + ] }).then(users => { - for (user of users) { - emailScheduler(user); - } + for (user of users) { + emailScheduler(user) + } }) -//So. When the server boots, emailScheduler is called (above) for every user that's signed up for weekly or daily emails, and that function -//schedules a call to sendUpdateEmail for each signed-up user to be executed at the next time that they're supposed to get an email. After the call to sendUpdateEmail -//executes, emailScheduler is called for that user again to schedule the new next email they're supposed to get. -//Scheduling is done with setTimeout, and the timeout object it returns is stored in scheduledEmails. If a user changes their email settings, -//emailRescheduler is called, it cancels the timeout object stored for them in scheduledEmails, and emailScheduler is called for them -//(if they currently want emails according to the new settings.) +// So. When the server boots, emailScheduler is called (above) for every user that's signed up for weekly or daily emails, and that function +// schedules a call to sendUpdateEmail for each signed-up user to be executed at the next time that they're supposed to get an email. After the call to sendUpdateEmail +// executes, emailScheduler is called for that user again to schedule the new next email they're supposed to get. +// Scheduling is done with setTimeout, and the timeout object it returns is stored in scheduledEmails. If a user changes their email settings, +// emailRescheduler is called, it cancels the timeout object stored for them in scheduledEmails, and emailScheduler is called for them +// (if they currently want emails according to the new settings.) -function emailScheduler(user, justSentOne = false) { - //usersLocalTime starts out as just the current time in the user's time zone and then we change it piece by piece to be the time that we send the next email at! +function emailScheduler (user, justSentOne = false) { + // usersLocalTime starts out as just the current time in the user's time zone and then we change it piece by piece to be the time that we send the next email at! - var usersLocalTime = moment(); //not actually in user's local time yet - putInUsersLocalTime(usersLocalTime, user); //there we go + var usersLocalTime = moment() // not actually in user's local time yet + putInUsersLocalTime(usersLocalTime, user) // there we go - var emailTimeComps = user.settings.emailTime.split(':').map(v => { - return parseInt(v) - }); + var emailTimeComps = user.settings.emailTime.split(':').map(v => { + return parseInt(v) + }) - //set usersLocalTime's day to that of the next email: - if (user.settings.digestEmailFrequency == 'daily') { - // if we're not sending today's email (so, either we've just sent it or the time at which we're supposed to send the email today has past) - if (justSentOne || (usersLocalTime.hour() > emailTimeComps[0]) || (usersLocalTime.hour() == emailTimeComps[0] && usersLocalTime.minute() > emailTimeComps[1])) { - usersLocalTime.add(1, "d"); //then make this moment take place tomorrow - } + // set usersLocalTime's day to that of the next email: + if (user.settings.digestEmailFrequency == 'daily') { + // if we're not sending today's email (so, either we've just sent it or the time at which we're supposed to send the email today has past) + if (justSentOne || (usersLocalTime.hour() > emailTimeComps[0]) || (usersLocalTime.hour() == emailTimeComps[0] && usersLocalTime.minute() > emailTimeComps[1])) { + usersLocalTime.add(1, 'd') // then make this moment take place tomorrow + } + } else { + var weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + var emailDayIndex = weekdays.indexOf(user.settings.emailDay) + // if we aren't sending this week's email (either we just sent one or the point at which we're supposed to send it this week has past) + if (justSentOne || (usersLocalTime.day() > emailDayIndex) || (usersLocalTime.day() == emailDayIndex && usersLocalTime.hour() > emailTimeComps[0]) || (usersLocalTime.day() == emailDayIndex && usersLocalTime.hour() == emailTimeComps[0] && usersLocalTime.minute() > emailTimeComps[1])) { + usersLocalTime.day(user.settings.emailDay) // set the day of the week + usersLocalTime.add(7, 'd') // then make this moment take place next week } else { - var weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - var emailDayIndex = weekdays.indexOf(user.settings.emailDay); - //if we aren't sending this week's email (either we just sent one or the point at which we're supposed to send it this week has past) - if (justSentOne || (usersLocalTime.day() > emailDayIndex) || (usersLocalTime.day() == emailDayIndex && usersLocalTime.hour() > emailTimeComps[0]) || (usersLocalTime.day() == emailDayIndex && usersLocalTime.hour() == emailTimeComps[0] && usersLocalTime.minute() > emailTimeComps[1])) { - usersLocalTime.day(user.settings.emailDay); //set the day of the week - usersLocalTime.add(7, 'd'); //then make this moment take place next week - } else { - usersLocalTime.day(user.settings.emailDay); //if we're sending this week's email, we just need to set the day of the week - } + usersLocalTime.day(user.settings.emailDay) // if we're sending this week's email, we just need to set the day of the week } - - //set its hour and minutes (the seconds and milliseconds are just gonna be what they're gonna be): - usersLocalTime.hour(emailTimeComps[0]).minute(emailTimeComps[1]); //now usersLocalTime stores the time at which the next email will be sent - var msTillSendingTime = usersLocalTime.diff(moment()); //find the difference between that time and the current time - scheduledEmails[user._id.toString()] = setTimeout(() => { //schedule an email sending at that time - sendUpdateEmail(user); - var emailSentTime = moment(); - emailLog("sendUpdateEmail ran for " + user.username + " on " + emailSentTime.format(logFormat) + " our time, our time zone being UTC" + emailSentTime.format('Z z')); - putInUsersLocalTime(emailSentTime, user); - emailLog("that is equivalent to " + emailSentTime.format(logFormat) + " their time!"); - emailLog("their email time is: " + (user.settings.digestEmailFrequency == "weekly" ? user.settings.emailDay + ', ' : '') + user.settings.emailTime); - emailLog("their time zone is: " + (user.settings.timezone == "auto" ? user.settings.autoDetectedTimeZone : user.settings.timezone)); - emailLog("their email frequency preference is: " + user.settings.digestEmailFrequency); - emailLog("their email address is: " + user.email); - emailLog('\n'); - emailScheduler(user, true); //schedule their next email - }, msTillSendingTime); - var nextEmailTime = moment().add(msTillSendingTime, 'ms'); - emailLog("scheduled email for user " + user.username + " to be sent on " + nextEmailTime.format(logFormat) + " our time, our time zone being UTC" + nextEmailTime.format('Z z')); - putInUsersLocalTime(nextEmailTime, user); - emailLog("that is equivalent to " + nextEmailTime.format(logFormat) + " their time!"); - emailLog("their email time is: " + (user.settings.digestEmailFrequency == "weekly" ? user.settings.emailDay + ', ' : '') + user.settings.emailTime); - emailLog("their time zone is: " + (user.settings.timezone == "auto" ? user.settings.autoDetectedTimeZone : user.settings.timezone)); - emailLog("their email frequency preference is: " + user.settings.digestEmailFrequency); - emailLog("their email address is: " + user.email); - emailLog('\n'); + } + + // set its hour and minutes (the seconds and milliseconds are just gonna be what they're gonna be): + usersLocalTime.hour(emailTimeComps[0]).minute(emailTimeComps[1]) // now usersLocalTime stores the time at which the next email will be sent + var msTillSendingTime = usersLocalTime.diff(moment()) // find the difference between that time and the current time + scheduledEmails[user._id.toString()] = setTimeout(() => { // schedule an email sending at that time + sendUpdateEmail(user) + var emailSentTime = moment() + emailLog('sendUpdateEmail ran for ' + user.username + ' on ' + emailSentTime.format(logFormat) + ' our time, our time zone being UTC' + emailSentTime.format('Z z')) + putInUsersLocalTime(emailSentTime, user) + emailLog('that is equivalent to ' + emailSentTime.format(logFormat) + ' their time!') + emailLog('their email time is: ' + (user.settings.digestEmailFrequency == 'weekly' ? user.settings.emailDay + ', ' : '') + user.settings.emailTime) + emailLog('their time zone is: ' + (user.settings.timezone == 'auto' ? user.settings.autoDetectedTimeZone : user.settings.timezone)) + emailLog('their email frequency preference is: ' + user.settings.digestEmailFrequency) + emailLog('their email address is: ' + user.email) + emailLog('\n') + emailScheduler(user, true) // schedule their next email + }, msTillSendingTime) + var nextEmailTime = moment().add(msTillSendingTime, 'ms') + emailLog('scheduled email for user ' + user.username + ' to be sent on ' + nextEmailTime.format(logFormat) + ' our time, our time zone being UTC' + nextEmailTime.format('Z z')) + putInUsersLocalTime(nextEmailTime, user) + emailLog('that is equivalent to ' + nextEmailTime.format(logFormat) + ' their time!') + emailLog('their email time is: ' + (user.settings.digestEmailFrequency == 'weekly' ? user.settings.emailDay + ', ' : '') + user.settings.emailTime) + emailLog('their time zone is: ' + (user.settings.timezone == 'auto' ? user.settings.autoDetectedTimeZone : user.settings.timezone)) + emailLog('their email frequency preference is: ' + user.settings.digestEmailFrequency) + emailLog('their email address is: ' + user.email) + emailLog('\n') } -async function sendUpdateEmail(user) { - try { - email = {}; - if (user.settings.digestEmailFrequency == "daily") { - email.subject = "sweet daily update 🍭" - } else if (user.settings.digestEmailFrequency == "weekly") { - email.subject = "sweet weekly update 🍭" +async function sendUpdateEmail (user) { + try { + email = {} + if (user.settings.digestEmailFrequency == 'daily') { + email.subject = 'sweet daily update 🍭' + } else if (user.settings.digestEmailFrequency == 'weekly') { + email.subject = 'sweet weekly update 🍭' + } else { + emailLog('\n' + 'sendUpdateEmail was called, but ' + user.username + ' does not appear to have their email preference set correctly?') + return + } + const unreadNotifications = user.notifications.filter(n => n.seen == false) + if (unreadNotifications && unreadNotifications.length != 0) { + // send mail with defined transport object + const info = { + from: '"sweet 🍬" ', // sender address + to: user.email, + subject: email.subject, + template: 'update', + context: { + title: 'sweet', + content: [ + 'Hi @' + user.username + '!', + 'Here\'s what went down since you last visted sweet:' + ], + notifications: unreadNotifications, + action: { + url: 'https://sweet.sh', + text: 'Visit sweet' + }, + signoff: '— sweet x' + } + } + await transporter.sendMail(info, function (error, info) { + if (error) { + emailLog('could not send email to ' + user.username + ': ' + error) } else { - emailLog("\n" + "sendUpdateEmail was called, but " + user.username + " does not appear to have their email preference set correctly?"); - return; + emailLog('\n---email sent to ' + user.username + '! contained ' + unreadNotifications.length + ' unread notifications---') + emailLog('info:\n' + JSON.stringify(info, null, 4) + '\n') } - const unreadNotifications = user.notifications.filter(n => n.seen == false); - if (unreadNotifications && unreadNotifications.length != 0) { - // send mail with defined transport object - let info = { - from: '"sweet 🍬" ', // sender address - to: user.email, - subject: email.subject, - template: "update", - context: { - title: 'sweet', - content: [ - 'Hi @' + user.username + '!', - 'Here\'s what went down since you last visted sweet:' - ], - notifications: unreadNotifications, - action: { - url: 'https://sweet.sh', - text: 'Visit sweet' - }, - signoff: '— sweet x' - } - }; - await transporter.sendMail(info, function (error, info) { - if (error) { - emailLog("could not send email to " + user.username + ": " + error); - } else { - emailLog('\n---email sent to ' + user.username + '! contained ' + unreadNotifications.length + ' unread notifications---'); - emailLog("info:\n" + JSON.stringify(info, null, 4) + "\n"); - } - }) - /* + }) + /* //not for production, i only have this here bc i can't actually send emails and then look at them: console.log(info); var emailHTML = await hbs.render('./views/emails/update.handlebars', info.context); @@ -184,67 +184,67 @@ async function sendUpdateEmail(user) { } }); */ - } else { - emailLog("\nlooks like " + user.username + ' had no unread notifications! no email will be forthcoming'); - } - } catch (err) { - emailLog("something went really catastrophically wrong when trying to send an email!") - emailLog(err) - emailLog(err.message) - if (!user) { - emailLog("user undefined!!!") - } else { - emailLog("with user " + user.username); - } + } else { + emailLog('\nlooks like ' + user.username + ' had no unread notifications! no email will be forthcoming') } + } catch (err) { + emailLog('something went really catastrophically wrong when trying to send an email!') + emailLog(err) + emailLog(err.message) + if (!user) { + emailLog('user undefined!!!') + } else { + emailLog('with user ' + user.username) + } + } } -//this is called over in the settings changing code in personalAccountActions.js whenever a setting related to emails changes -function emailRescheduler(user) { - if (scheduledEmails[user._id.toString()]) { - clearTimeout(scheduledEmails[user._id.toString()]); - emailLog('cancelled emails for ' + user.username + '!'); - } - if (user.settings.digestEmailFrequency != "off") { - emailScheduler(user); - } +// this is called over in the settings changing code in personalAccountActions.js whenever a setting related to emails changes +function emailRescheduler (user) { + if (scheduledEmails[user._id.toString()]) { + clearTimeout(scheduledEmails[user._id.toString()]) + emailLog('cancelled emails for ' + user.username + '!') + } + if (user.settings.digestEmailFrequency != 'off') { + emailScheduler(user) + } } -async function sendSingleNotificationEmail(user, notification, link){ - let singleNotification = [{ - url: link, - image: notification.image, - text: notification.emailText - }]; - let info = { - from: '"sweet 🍬" ', - to: user.email, - subject: notification.emailText, - template: "update", - context: { - title: 'sweet', - content: [ - 'Hi @' + user.username + '!', - '' + notification.emailText - ], - action: { - url: 'https://sweet.sh' + link, - text: 'take a look' - }, - signoff: '— sweet x' - } - }; - await transporter.sendMail(info, function (error, info) { - if (error) { - emailLog("could not send email to " + user.username + ": " + error); - } else { - emailLog('\n--- single notification email sent to ' + user.username + '! ---'); - emailLog("info:\n" + JSON.stringify(info, null, 4) + "\n"); - } - }) +async function sendSingleNotificationEmail (user, notification, link) { + const singleNotification = [{ + url: link, + image: notification.image, + text: notification.emailText + }] + const info = { + from: '"sweet 🍬" ', + to: user.email, + subject: notification.emailText, + template: 'update', + context: { + title: 'sweet', + content: [ + 'Hi @' + user.username + '!', + '' + notification.emailText + ], + action: { + url: 'https://sweet.sh' + link, + text: 'take a look' + }, + signoff: '— sweet x' + } + } + await transporter.sendMail(info, function (error, info) { + if (error) { + emailLog('could not send email to ' + user.username + ': ' + error) + } else { + emailLog('\n--- single notification email sent to ' + user.username + '! ---') + emailLog('info:\n' + JSON.stringify(info, null, 4) + '\n') + } + }) } -module.exports.sendSingleNotificationEmail = sendSingleNotificationEmail; -module.exports.sendUpdateEmail = sendUpdateEmail; -//export this so it can be called upon email settings changing -module.exports.emailRescheduler = emailRescheduler; +module.exports.sendSingleNotificationEmail = sendSingleNotificationEmail +module.exports.sendUpdateEmail = sendUpdateEmail +// export this so it can be called upon email settings changing +module.exports.emailRescheduler = emailRescheduler diff --git a/app/inhabitingCommunities.js b/app/inhabitingCommunities.js index b635f61c..9edbc276 100644 --- a/app/inhabitingCommunities.js +++ b/app/inhabitingCommunities.js @@ -1,783 +1,549 @@ -const CommunityPlaceholder = mongoose.model('Community Placeholder'); +const CommunityPlaceholder = mongoose.model('Community Placeholder') -//this is never read from but it could be someday i guess -const expiryTimers = []; +// this is never read from but it could be someday i guess +const expiryTimers = [] -//set vote expiration timers when the server starts up +// set vote expiration timers when the server starts up Vote.find({}).then(votes => { - for (vote of votes) { - //account for votes that expired while server was down - if (vote.expiryTime.getTime() < new Date() && vote.status != "expired") { - vote.status = "expired"; - vote.save(); - //account for currently active votes that need to be scheduled to expire - } else if (vote.status == "active") { - var expireVote = schedule.scheduleJob(vote.expiryTime, function() { - if (vote) { - vote.status = "expired" - vote.save(); - } - }) - expiryTimers.push(expireVote); + for (vote of votes) { + // account for votes that expired while server was down + if (vote.expiryTime.getTime() < new Date() && vote.status != 'expired') { + vote.status = 'expired' + vote.save() + // account for currently active votes that need to be scheduled to expire + } else if (vote.status == 'active') { + var expireVote = schedule.scheduleJob(vote.expiryTime, function () { + if (vote) { + vote.status = 'expired' + vote.save() } + }) + expiryTimers.push(expireVote) } + } }) -module.exports = function(app, passport) { +module.exports = function (app, passport) { + app.get('/api/community/getall/:page', isLoggedIn, function (req, res) { + const postsPerPage = 10 + const page = req.params.page - 1 - app.get('/api/community/getall/:page', isLoggedIn, function(req, res) { - let postsPerPage = 10; - let page = req.params.page - 1; + Community.find() + .sort('-lastUpdated') + .skip(postsPerPage * page) + .limit(postsPerPage) + .then(communities => { + if (!communities.length) { + res.status(404) + .send('Not found') + } else { + res.render('partials/communities', { + layout: false, + loggedInUserData: req.user, + communities: communities + }) + } + }) + }) - Community.find() - .sort('-lastUpdated') - .skip(postsPerPage * page) - .limit(postsPerPage) - .then(communities => { - if (!communities.length) { - res.status(404) - .send('Not found'); - } else { - res.render('partials/communities', { - layout: false, - loggedInUserData: req.user, - communities: communities - }); - } - }) + app.get('/communities', isLoggedIn, function (req, res) { + Community.find({ + members: req.user._id }) + .collation({ + locale: 'en' + }) + .sort('name') + .then((communities) => { + res.render('communities', { + loggedIn: true, + loggedInUserData: req.user, + communities: communities, + activePage: 'communities' + }) + }) + .catch((err) => { + console.log('Error in profileData.') + console.log(err) + }) + }) - app.get('/communities', isLoggedIn, function(req, res) { - Community.find({ - members: req.user._id - }) - .collation({ - locale: "en" - }) - .sort('name') - .then((communities) => { - res.render('communities', { - loggedIn: true, - loggedInUserData: req.user, - communities: communities, - activePage: 'communities' - }) - }) - .catch((err) => { - console.log("Error in profileData.") - console.log(err); - }); - }); + app.get('/community', function (req, res) { + res.redirect('../communities') + }) - app.get('/community', function(req, res) { - res.redirect('../communities'); + app.get('/community/:slug', function (req, res) { + const today = moment().clone().startOf('day') + const thisyear = moment().clone().startOf('year') + if (req.isAuthenticated()) { + isLoggedIn = true + } else { + isLoggedIn = false + } + let isMember = false + let hasRequested = false + let isBanned = false + let isMuted = false + Community.findOne({ + slug: req.params.slug }) - - app.get('/community/:slug', function(req, res) { - const today = moment().clone().startOf('day'); - const thisyear = moment().clone().startOf('year'); - if (req.isAuthenticated()) { - isLoggedIn = true; - } else { - isLoggedIn = false; - } - let isMember = false; - let hasRequested = false; - let isBanned = false; - let isMuted = false; - Community.findOne({ - slug: req.params.slug + .populate('members') + .populate('membershipRequests') + .populate('bannedMembers') + .populate('mutedMembers') + .populate('welcomeMessageAuthor') + .then(community => { + if (community) { + if (isLoggedIn) { + const memberIds = community.members.map(a => a._id.toString()) + const bannedMemberIds = community.bannedMembers.map(a => a._id.toString()) + const mutedMemberIds = community.mutedMembers.map(a => a._id.toString()) + const membershipRequestIds = community.membershipRequests.map(a => a._id.toString()) + if (memberIds.includes(req.user._id.toString())) { + isMember = true + } + if (membershipRequestIds.includes(req.user._id.toString())) { + hasRequested = true + } + if (bannedMemberIds.includes(req.user._id.toString())) { + isBanned = true + } + if (mutedMemberIds.includes(req.user._id.toString())) { + isMuted = true + } + Vote.find({ + community: community._id, + status: 'active' }) - .populate('members') - .populate('membershipRequests') - .populate('bannedMembers') - .populate('mutedMembers') - .populate('welcomeMessageAuthor') - .then(community => { - if (community) { - if (isLoggedIn) { - let memberIds = community.members.map(a => a._id.toString()); - let bannedMemberIds = community.bannedMembers.map(a => a._id.toString()); - let mutedMemberIds = community.mutedMembers.map(a => a._id.toString()); - let membershipRequestIds = community.membershipRequests.map(a => a._id.toString()); - if (memberIds.includes(req.user._id.toString())) { - isMember = true; - } - if (membershipRequestIds.includes(req.user._id.toString())) { - hasRequested = true; - } - if (bannedMemberIds.includes(req.user._id.toString())) { - isBanned = true; - } - if (mutedMemberIds.includes(req.user._id.toString())) { - isMuted = true; - } - Vote.find({ - community: community._id, - status: 'active' - }) - .populate('creator') - .sort('-timestamp') - .then(votes => { - votes.forEach(function(vote) { - if (mutedMemberIds.includes(vote.creator._id.toString())) { - vote.canDisplay = false; - } else { - vote.canDisplay = true; - } - if (moment(vote.timestamp).isSame(today, 'd')) { - vote.parsedTimestamp = moment(vote.timestamp).fromNow(); - } else if (moment(vote.timestamp).isSame(thisyear, 'y')) { - vote.parsedTimestamp = moment(vote.timestamp).format('D MMM'); - } else { - vote.parsedTimestamp = moment(vote.timestamp).format('D MMM YYYY'); - } - vote.parsedExpiry = moment(vote.expiryTime).locale('en-GB').fromNow(); - if (vote.reference == "userban" || vote.reference == "userunban" || vote.reference == "usermute" || vote.reference == "userunmute") { - vote.group = "uservotes"; - User.findOne({ - _id: vote.proposedValue - }) - .then(user => { - vote.userData = user; - }) - } - }) - community.mutedMembers.forEach(member => { - if (bannedMemberIds.includes(member._id.toString())) - member.isBanned = true; - }) + .populate('creator') + .sort('-timestamp') + .then(votes => { + votes.forEach(function (vote) { + if (mutedMemberIds.includes(vote.creator._id.toString())) { + vote.canDisplay = false + } else { + vote.canDisplay = true + } + if (moment(vote.timestamp).isSame(today, 'd')) { + vote.parsedTimestamp = moment(vote.timestamp).fromNow() + } else if (moment(vote.timestamp).isSame(thisyear, 'y')) { + vote.parsedTimestamp = moment(vote.timestamp).format('D MMM') + } else { + vote.parsedTimestamp = moment(vote.timestamp).format('D MMM YYYY') + } + vote.parsedExpiry = moment(vote.expiryTime).locale('en-GB').fromNow() + if (vote.reference == 'userban' || vote.reference == 'userunban' || vote.reference == 'usermute' || vote.reference == 'userunmute') { + vote.group = 'uservotes' + User.findOne({ + _id: vote.proposedValue + }) + .then(user => { + vote.userData = user + }) + } + }) + community.mutedMembers.forEach(member => { + if (bannedMemberIds.includes(member._id.toString())) { member.isBanned = true } + }) - // Find the number of members who have been active on sweet in the last 2 weeks - // and are not muted - these are members allowed to vote - then work out the - // majority margin required for a vote to pass based on the number of those members - var currentFortnight = moment().clone().subtract(14, 'days').startOf('day'); - let recentlyActiveMembers = community.members.filter((member) => { - return moment(member.lastUpdated).isBetween(currentFortnight, moment()) && - !mutedMemberIds.includes(member._id.toString()); - }) - let recentlyActiveMemberIds = recentlyActiveMembers.map(a => a._id.toString()); - let majorityMargin = helper.isOdd(recentlyActiveMembers.length) ? (recentlyActiveMembers.length / 2) + 0.5 : (recentlyActiveMembers.length / 2) + 1 - notifier.markRead(req.user._id, community._id); - community.members.forEach((member) => { - if (recentlyActiveMemberIds.includes(member._id.toString())) { - member.isRecentlyActive = true; - } - }) - res.render('community', { - loggedIn: isLoggedIn, - loggedInUserData: req.user, - communityData: community, - isMember: isMember, - hasRequested: hasRequested, - votes: votes, - majorityMargin: majorityMargin, - isBanned: isBanned, - isMuted: isMuted, - bannedMemberIds: bannedMemberIds - }) - }) - } else { - res.render('community', { - loggedIn: false, - loggedInUserData: "", - communityData: community, - isMember: false, - hasRequested: false, - votes: "", - majorityMargin: "", - isBanned: false - }) - } - } else { - res.status(404).redirect('/404'); - } + // Find the number of members who have been active on sweet in the last 2 weeks + // and are not muted - these are members allowed to vote - then work out the + // majority margin required for a vote to pass based on the number of those members + var currentFortnight = moment().clone().subtract(14, 'days').startOf('day') + const recentlyActiveMembers = community.members.filter((member) => { + return moment(member.lastUpdated).isBetween(currentFortnight, moment()) && + !mutedMemberIds.includes(member._id.toString()) + }) + const recentlyActiveMemberIds = recentlyActiveMembers.map(a => a._id.toString()) + const majorityMargin = helper.isOdd(recentlyActiveMembers.length) ? (recentlyActiveMembers.length / 2) + 0.5 : (recentlyActiveMembers.length / 2) + 1 + notifier.markRead(req.user._id, community._id) + community.members.forEach((member) => { + if (recentlyActiveMemberIds.includes(member._id.toString())) { + member.isRecentlyActive = true + } + }) + res.render('community', { + loggedIn: isLoggedIn, + loggedInUserData: req.user, + communityData: community, + isMember: isMember, + hasRequested: hasRequested, + votes: votes, + majorityMargin: majorityMargin, + isBanned: isBanned, + isMuted: isMuted, + bannedMemberIds: bannedMemberIds + }) + }) + } else { + res.render('community', { + loggedIn: false, + loggedInUserData: '', + communityData: community, + isMember: false, + hasRequested: false, + votes: '', + majorityMargin: '', + isBanned: false }) - }); + } + } else { + res.status(404).redirect('/404') + } + }) + }) - app.get('/api/community/getbyid/:communityid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }).then((community) => { - res.redirect('/community/' + community.slug); - }) - }); + app.get('/api/community/getbyid/:communityid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }).then((community) => { + res.redirect('/community/' + community.slug) + }) + }) - app.post('/api/community/create', isLoggedIn, function(req, res) { - console.log("Creating community") - let newCommunityData = req.body; - let newCommunitySlug = helper.slugify(newCommunityData.communityName); - Community.findOne({ - slug: newCommunitySlug - }) - .then(async community => { - if (community) { - req.session.sessionFlash = { - type: 'warning', - message: 'A community with this URL (' + newCommunitySlug + ') already exists.', - newCommunityData: newCommunityData - } - return res.redirect('back'); - } else { - let imageEnabled = false; - let imageUrl = ""; - let communityUrl = shortid.generate(); - if (req.files.imageUpload) { - if (req.files.imageUpload.data.length > 3145728) { - console.error("Image too large!") - req.session.sessionFlash = { - type: 'warning', - message: 'File too large. The file size limit is 3MB.', - communityData: newCommunityData - } - return res.redirect('back'); - } else { - console.log("Saving image") - imageEnabled = true; - sharp(req.files.imageUpload.data) - .resize({ width: 600, height: 600 }) - .jpeg({ quality: 70 }) - .toFile('./public/images/communities/' + communityUrl + '.jpg') - .catch(err => { - console.error(err); - }); - } - } - var parsedDesc = (await helper.parseText(newCommunityData.communityDescription)).text; - var parsedRules = (await helper.parseText(newCommunityData.communityRules)).text; + app.post('/api/community/create', isLoggedIn, function (req, res) { + console.log('Creating community') + const newCommunityData = req.body + const newCommunitySlug = helper.slugify(newCommunityData.communityName) + Community.findOne({ + slug: newCommunitySlug + }) + .then(async community => { + if (community) { + req.session.sessionFlash = { + type: 'warning', + message: 'A community with this URL (' + newCommunitySlug + ') already exists.', + newCommunityData: newCommunityData + } + return res.redirect('back') + } else { + let imageEnabled = false + const imageUrl = '' + const communityUrl = shortid.generate() + if (req.files.imageUpload) { + if (req.files.imageUpload.data.length > 3145728) { + console.error('Image too large!') + req.session.sessionFlash = { + type: 'warning', + message: 'File too large. The file size limit is 3MB.', + communityData: newCommunityData + } + return res.redirect('back') + } else { + console.log('Saving image') + imageEnabled = true + sharp(req.files.imageUpload.data) + .resize({ width: 600, height: 600 }) + .jpeg({ quality: 70 }) + .toFile('./public/images/communities/' + communityUrl + '.jpg') + .catch(err => { + console.error(err) + }) + } + } + var parsedDesc = (await helper.parseText(newCommunityData.communityDescription)).text + var parsedRules = (await helper.parseText(newCommunityData.communityRules)).text - const community = new Community({ - created: new Date(), - name: newCommunityData.communityName, - slug: newCommunitySlug, - url: communityUrl, - descriptionRaw: newCommunityData.communityDescription, - descriptionParsed: parsedDesc, - rulesRaw: newCommunityData.communityRules, - rulesParsed: parsedRules, - image: imageEnabled ? communityUrl + '.jpg' : 'cake.svg', - imageEnabled: imageEnabled, - settings: { - visibility: newCommunityData.communityVisibility, - joinType: newCommunityData.communityJoinType, - voteThreshold: 50, - voteLength: newCommunityData.communityVoteLength - }, - members: [req.user._id] - }); - community.save() - .then(community => { - User.findOne({ - _id: req.user._id - }) - .then(user => { - user.communities.push(community._id); - user.save() - }) - .then(user => { - touchCommunity(community._id) - console.log("Created community!") - res.redirect('/community/' + newCommunitySlug) - }); - }) - } + const community = new Community({ + created: new Date(), + name: newCommunityData.communityName, + slug: newCommunitySlug, + url: communityUrl, + descriptionRaw: newCommunityData.communityDescription, + descriptionParsed: parsedDesc, + rulesRaw: newCommunityData.communityRules, + rulesParsed: parsedRules, + image: imageEnabled ? communityUrl + '.jpg' : 'cake.svg', + imageEnabled: imageEnabled, + settings: { + visibility: newCommunityData.communityVisibility, + joinType: newCommunityData.communityJoinType, + voteThreshold: 50, + voteLength: newCommunityData.communityVoteLength + }, + members: [req.user._id] + }) + community.save() + .then(community => { + User.findOne({ + _id: req.user._id + }) + .then(user => { + user.communities.push(community._id) + user.save() + }) + .then(user => { + touchCommunity(community._id) + console.log('Created community!') + res.redirect('/community/' + newCommunitySlug) + }) }) - }); - - app.post('/api/community/delete', isLoggedIn, function(req, res) { - - }); - - app.post('/api/community/user/join/:communityid', isLoggedIn, async function(req, res) { - var community = await Community.findOne({ _id: req.params.communityid }); - if (community.bannedMembers.includes(req.user._id)) { - return res.sendStatus(403); - } - if (!community.members.some(v => v.equals(req.user._id))) { - community.members.push(req.user._id) - await community.save(); - touchCommunity(req.params.communityid) - } - var user = await User.findOne({ _id: req.user._id }) - if (!user.communities.some(v => v.toString() == req.params.communityid)) { - user.communities.push(req.params.communityid) - await user.save(); } - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }); + }) + }) - app.post('/api/community/user/request/:communityid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then(community => { - community.membershipRequests.push(req.user._id) - community.save() - .then(save => { - community.members.forEach(member => { - notifier.notify('community', 'request', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'request') - }) - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - }) - }); + app.post('/api/community/delete', isLoggedIn, function (req, res) { - app.post('/api/community/user/leave/:communityid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then(community => { - community.members.pull(req.user._id) - community.save() - }) - .then(community => { - User.findOne({ - _id: req.user._id - }) - .then(user => { - user.communities.pull(req.params.communityid) - user.save() - }) - }) - .then(success => { - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - }); + }) - app.post('/api/community/user/mute/:communityid/:userid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then(community => { - let memberIds = community.members.map(a => a._id.toString()); - if (memberIds.includes(req.user._id.toString())) { - isMember = true; - } - voteUrl = shortid.generate(); - created = new Date(); - expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') - if (community.members.length - community.mutedMembers.length === 1) { - //if there is only one member with permissions, start out with 0 votes total so that at least someone has to click on the 'vote' button to make it pass - votesNumber = 0; - } else { - //otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it - votesNumber = 1; - } - if (isMember) { - const vote = new Vote({ - status: 'active', - community: req.params.communityid, - reference: 'usermute', - proposedValue: req.params.userid, - creatorEmail: req.user.email, - creator: req.user._id, - url: voteUrl, - timestamp: created, - lastUpdated: created, - voteThreshold: 50, - expiryTime: expiryTime, - votes: votesNumber, - voters: votesNumber == 1 ? [req.user._id] : [], - }) - voteId = vote._id; - vote.save() - .then(vote => { - var expireVote = schedule.scheduleJob(expiryTime, function() { - Vote.findOne({ - _id: vote._id - }) - .then(vote => { - vote.status = "expired" - vote.save(); - }) - }); - community.members.forEach(member => { - notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') - }) - expiryTimers.push(expireVote); - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - } else { - console.log("User not authorised to mute user.") - } - }) - }) + app.post('/api/community/user/join/:communityid', isLoggedIn, async function (req, res) { + var community = await Community.findOne({ _id: req.params.communityid }) + if (community.bannedMembers.includes(req.user._id)) { + return res.sendStatus(403) + } + if (!community.members.some(v => v.equals(req.user._id))) { + community.members.push(req.user._id) + await community.save() + touchCommunity(req.params.communityid) + } + var user = await User.findOne({ _id: req.user._id }) + if (!user.communities.some(v => v.toString() == req.params.communityid)) { + user.communities.push(req.params.communityid) + await user.save() + } + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) - app.post('/api/community/user/unmute/:communityid/:userid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then((community) => { - let memberIds = community.members.map(a => a._id.toString()); - if (memberIds.includes(req.user._id.toString())) { - isMember = true; - } - voteUrl = shortid.generate(); - created = new Date(); - expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') - if (community.members.length - community.mutedMembers.length === 1) { - //if there is only one member with permissions, start out with 0 votes total so that at least someone has to click on the 'vote' button to make it pass - votesNumber = 0; - } else { - //otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it - votesNumber = 1; - } - if (isMember) { - const vote = new Vote({ - status: 'active', - community: req.params.communityid, - reference: 'userunmute', - proposedValue: req.params.userid, - creatorEmail: req.user.email, - creator: req.user._id, - url: voteUrl, - timestamp: created, - lastUpdated: created, - voteThreshold: 50, - expiryTime: expiryTime, - votes: votesNumber, - voters: votesNumber == 1 ? [req.user._id] : [], - }) - voteId = vote._id; - vote.save() - .then(vote => { - var expireVote = schedule.scheduleJob(expiryTime, function() { - Vote.findOne({ - _id: vote._id - }) - .then(vote => { - vote.status = "expired" - vote.save(); - }) - }); - expiryTimers.push(expireVote); - community.members.forEach(member => { - notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') - }) - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - } else { - console.log("User not authorised to unmute user.") - } + app.post('/api/community/user/request/:communityid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }) + .then(community => { + community.membershipRequests.push(req.user._id) + community.save() + .then(save => { + community.members.forEach(member => { + notifier.notify('community', 'request', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'request') }) - }); + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + }) + }) - app.post('/api/community/user/ban/:communityid/:userid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then((community) => { - let memberIds = community.members.map(a => a._id.toString()); - if (memberIds.includes(req.user._id.toString())) { - isMember = true; - } - voteUrl = shortid.generate(); - created = new Date(); - expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') - if (community.members.length - community.mutedMembers.length === 1) { - //if there is only one member with permissions, start out with 0 votes total so that someone has to at least click on the 'vote' button to make it pass - votesNumber = 0; - } else { - //otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it - votesNumber = 1; - } - if (isMember) { - const vote = new Vote({ - status: 'active', - community: req.params.communityid, - reference: 'userban', - proposedValue: req.params.userid, - creatorEmail: req.user.email, - creator: req.user._id, - url: voteUrl, - timestamp: created, - lastUpdated: created, - voteThreshold: 50, - expiryTime: expiryTime, - votes: votesNumber, - voters: votesNumber == 1 ? [req.user._id] : [], - }) - voteId = vote._id; - vote.save() - .then(vote => { - var expireVote = schedule.scheduleJob(expiryTime, function() { - Vote.findOne({ - _id: vote._id - }) - .then(vote => { - vote.status = "expired" - vote.save(); - }) - }); - expiryTimers.push(expireVote); - community.members.forEach(member => { - notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') - }) - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - } else { - console.log("User not authorised to ban user.") - } - }) - }); + app.post('/api/community/user/leave/:communityid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }) + .then(community => { + community.members.pull(req.user._id) + community.save() + }) + .then(community => { + User.findOne({ + _id: req.user._id + }) + .then(user => { + user.communities.pull(req.params.communityid) + user.save() + }) + }) + .then(success => { + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + }) - app.post('/api/community/user/unban/:communityid/:userid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then((community) => { - let memberIds = community.members.map(a => a._id.toString()); - if (memberIds.includes(req.user._id.toString())) { - isMember = true; - } - voteUrl = shortid.generate(); - created = new Date(); - expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') - if (community.members.length - community.mutedMembers.length === 1) { - //if there is only one member with permissions, start out with 0 votes total so that at least someone has to click on the 'vote' button to make it pass - votesNumber = 0; - } else { - //otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it - votesNumber = 1; - } - if (isMember) { - const vote = new Vote({ - status: 'active', - community: req.params.communityid, - reference: 'userunban', - proposedValue: req.params.userid, - creatorEmail: req.user.email, - creator: req.user._id, - url: voteUrl, - timestamp: created, - lastUpdated: created, - voteThreshold: 50, - expiryTime: expiryTime, - votes: votesNumber, - voters: votesNumber == 1 ? [req.user._id] : [], - }) - voteId = vote._id; + app.post('/api/community/user/mute/:communityid/:userid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }) + .then(community => { + const memberIds = community.members.map(a => a._id.toString()) + if (memberIds.includes(req.user._id.toString())) { + isMember = true + } + voteUrl = shortid.generate() + created = new Date() + expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') + if (community.members.length - community.mutedMembers.length === 1) { + // if there is only one member with permissions, start out with 0 votes total so that at least someone has to click on the 'vote' button to make it pass + votesNumber = 0 + } else { + // otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it + votesNumber = 1 + } + if (isMember) { + const vote = new Vote({ + status: 'active', + community: req.params.communityid, + reference: 'usermute', + proposedValue: req.params.userid, + creatorEmail: req.user.email, + creator: req.user._id, + url: voteUrl, + timestamp: created, + lastUpdated: created, + voteThreshold: 50, + expiryTime: expiryTime, + votes: votesNumber, + voters: votesNumber == 1 ? [req.user._id] : [] + }) + voteId = vote._id + vote.save() + .then(vote => { + var expireVote = schedule.scheduleJob(expiryTime, function () { + Vote.findOne({ + _id: vote._id + }) + .then(vote => { + vote.status = 'expired' vote.save() - .then(vote => { - var expireVote = schedule.scheduleJob(expiryTime, function() { - Vote.findOne({ - _id: vote._id - }) - .then(vote => { - vote.status = "expired" - vote.save(); - }) - }); - expiryTimers.push(expireVote); - community.members.forEach(member => { - notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') - }) - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - } else { - console.log("User not authorised to unban user.") - } + }) + }) + community.members.forEach(member => { + notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') + }) + expiryTimers.push(expireVote) + res.end('{"success" : "Updated Successfully", "status" : 200}') }) - }); - - app.post('/api/community/user/accept/:communityid/:userid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then(community => { - let memberIds = community.members.map(a => a._id.toString()); - if (memberIds.includes(req.user._id.toString())) { - isMember = true; - } - if (isMember) { - community.members.push(req.params.userid) - community.membershipRequests.pull(req.params.userid) - community.save() - User.findOne({ - _id: req.params.userid - }) - .then(user => { - user.communities.push(req.params.communityid) - user.save() - console.log("Notifyin' member " + user.username) - notifier.notify('community', 'requestResponse', user._id, req.user._id, community._id, '/api/community/getbyid/' + community._id, 'approved') - touchCommunity(req.params.communityid) - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - } else { - console.log("User not authorised to approve request.") - } - }) - }); - - app.post('/api/community/user/reject/:communityid/:userid', isLoggedIn, function(req, res) { - Community.findOne({ - _id: req.params.communityid - }) - .then(community => { - let memberIds = community.members.map(a => a._id.toString()); - if (memberIds.includes(req.user._id.toString())) { - isMember = true; - } - if (isMember) { - community.membershipRequests.pull(req.params.userid) - community.save() - } else { - console.log("User not authorised to approve request.") - } - if (isMember) { - notifier.notify('community', 'requestResponse', req.params.userid, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'rejected') - res.end('{"success" : "Updated Successfully", "status" : 200}'); - } - }) - }); - - app.post('/api/community/vote/create/:communityid', isLoggedIn, async function(req, res) { - var community = await Community.findOne({ - _id: req.params.communityid - }) - if (!community.members.some(v => v.equals(req.user._id))) { - return res.sendStatus(403); + } else { + console.log('User not authorised to mute user.') } - console.log(req.body) - if (req.body.reference == "image") { - imageUrl = shortid.generate() + '.jpg'; - if (req.files.proposedValue.data.length > 3145728) { - console.error("Image too large!") - req.session.sessionFlash = { - type: 'warning', - message: 'File too large. The file size limit is 3MB.' - } - return res.redirect('back'); - } else { - sharp(req.files.proposedValue.data) - .resize({ - width: 600, - height: 600 - }) - .jpeg({ - quality: 70 - }) - .toFile('./public/images/communities/staging/' + imageUrl) - .catch(err => { - console.error(err); - }); - } + }) + }) + + app.post('/api/community/user/unmute/:communityid/:userid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }) + .then((community) => { + const memberIds = community.members.map(a => a._id.toString()) + if (memberIds.includes(req.user._id.toString())) { + isMember = true } - let parsedReferences = { - name: "name", - description: "description", - rules: "rules", - image: "display image", - visibility: "post visibility", - joinType: "joining method", - voteLength: "vote length" + voteUrl = shortid.generate() + created = new Date() + expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') + if (community.members.length - community.mutedMembers.length === 1) { + // if there is only one member with permissions, start out with 0 votes total so that at least someone has to click on the 'vote' button to make it pass + votesNumber = 0 + } else { + // otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it + votesNumber = 1 } - let parsedJoinType = { - open: 'Open (anyone is free to join)', - approval: 'Approval (requests to join must be approved by a current member)', - closed: 'Closed (only invited users can join)' + if (isMember) { + const vote = new Vote({ + status: 'active', + community: req.params.communityid, + reference: 'userunmute', + proposedValue: req.params.userid, + creatorEmail: req.user.email, + creator: req.user._id, + url: voteUrl, + timestamp: created, + lastUpdated: created, + voteThreshold: 50, + expiryTime: expiryTime, + votes: votesNumber, + voters: votesNumber == 1 ? [req.user._id] : [] + }) + voteId = vote._id + vote.save() + .then(vote => { + var expireVote = schedule.scheduleJob(expiryTime, function () { + Vote.findOne({ + _id: vote._id + }) + .then(vote => { + vote.status = 'expired' + vote.save() + }) + }) + expiryTimers.push(expireVote) + community.members.forEach(member => { + notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') + }) + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + } else { + console.log('User not authorised to unmute user.') } - let parsedVisibility = { - public: 'Public (posts visible to all sweet users)', - private: 'Private (posts visible only to members)' + }) + }) + + app.post('/api/community/user/ban/:communityid/:userid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }) + .then((community) => { + const memberIds = community.members.map(a => a._id.toString()) + if (memberIds.includes(req.user._id.toString())) { + isMember = true } - let parsedVoteLength = { - 1: '1 day', - 3: '3 days', - 7: '7 days', - 14: '14 days', - 30: '30 days' + voteUrl = shortid.generate() + created = new Date() + expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') + if (community.members.length - community.mutedMembers.length === 1) { + // if there is only one member with permissions, start out with 0 votes total so that someone has to at least click on the 'vote' button to make it pass + votesNumber = 0 + } else { + // otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it + votesNumber = 1 } - let parsedReference = parsedReferences[req.body.reference] - var allowedChange = true; //is there a change? and is it allowed? - if (req.body.reference == "description" || req.body.reference == "rules") { - proposedValue = req.body.proposedValue - parsedProposedValue = (await helper.parseText(req.body.proposedValue)).text - if (req.body.reference == "description") { - allowedChange = (community.descriptionRaw != proposedValue); - } else { - allowedChange = (community.rulesRaw != proposedValue); - } - } else if (req.body.reference == "joinType") { - proposedValue = req.body.proposedValue - parsedProposedValue = parsedJoinType[req.body.proposedValue] - allowedChange = (parsedProposedValue && community.settings.joinType != proposedValue); //parsedProposedValue will be undefined if req.body.proposedValue wasn't one of the allowed values - } else if (req.body.reference == "visibility") { - proposedValue = req.body.proposedValue - parsedProposedValue = parsedVisibility[req.body.proposedValue] - allowedChange = (parsedProposedValue && community.settings.visibility != proposedValue); - } else if (req.body.reference == "voteLength") { - proposedValue = req.body.proposedValue - parsedProposedValue = parsedVoteLength[req.body.proposedValue] - allowedChange = (parsedProposedValue && community.settings.voteLength != parseInt(proposedValue)); - } else if (req.body.reference == "image") { - proposedValue = imageUrl - parsedProposedValue = imageUrl - } else if (req.body.reference == "name") { //this is where it gets complicated - proposedValue = req.body.proposedValue - parsedProposedValue = proposedValue; - var slug = helper.slugify(proposedValue); - if (!parsedProposedValue || community.name == proposedValue) { - //not using allowedChange for this non-change bc by the time we get to the code that reacts to allowedChange it will have already returned a duplicate name complaint - req.session.sessionFlash = { - type: 'warning', - message: 'That vote would not change anything! But of course none ever seem to anyway' - } - return res.redirect('back'); - } else { - allowedChange = true; - if (await Community.findOne({ slug: slug }) || await Community.findOne({ name: proposedValue })) { - req.session.sessionFlash = { - type: 'warning', - message: 'The community name/url is unfortunately already taken...' - } - return res.redirect('back'); - } - //first, create the placeholder; then, check if you've just created a duplicate (and remove one and cancel if that's the case.) - //it's a bit weird, but it's bc we have no way of checking in advance if there already is one that guarantees there won't be - //a placeholder with our proposed name created between our check and our creation of the new one (bc we don't know what code will - //execute while we are awaiting the save of the new one.) i think this could still result in two changes to the same name being - //proposed at the same time and both of them being rejected, but that's incredibly unlikely and also oh well - var placeholder = new CommunityPlaceholder({ - name: proposedValue, - slug: helper.slugify(proposedValue) - }); - await placeholder.save(); - if ((await CommunityPlaceholder.find({ slug: slug })).length > 1 || (await CommunityPlaceholder.find({ name: proposedValue })).length > 1) { - req.session.sessionFlash = { - type: 'warning', - message: 'That community name/url is reserved bc another community is currently voting on it, sorry!' - } - CommunityPlaceholder.deleteOne({ name: proposedValue }, function(err) { console.error(err) }) //the "true" makes it just delete one - return res.redirect('back'); - } - } + if (isMember) { + const vote = new Vote({ + status: 'active', + community: req.params.communityid, + reference: 'userban', + proposedValue: req.params.userid, + creatorEmail: req.user.email, + creator: req.user._id, + url: voteUrl, + timestamp: created, + lastUpdated: created, + voteThreshold: 50, + expiryTime: expiryTime, + votes: votesNumber, + voters: votesNumber == 1 ? [req.user._id] : [] + }) + voteId = vote._id + vote.save() + .then(vote => { + var expireVote = schedule.scheduleJob(expiryTime, function () { + Vote.findOne({ + _id: vote._id + }) + .then(vote => { + vote.status = 'expired' + vote.save() + }) + }) + expiryTimers.push(expireVote) + community.members.forEach(member => { + notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') + }) + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + } else { + console.log('User not authorised to ban user.') } - if (!allowedChange) { - req.session.sessionFlash = { - type: 'warning', - message: 'That vote would not change anything! But of course none ever seem to anyway' - } - return res.redirect('back'); + }) + }) + + app.post('/api/community/user/unban/:communityid/:userid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }) + .then((community) => { + const memberIds = community.members.map(a => a._id.toString()) + if (memberIds.includes(req.user._id.toString())) { + isMember = true } - console.log(community) - voteUrl = shortid.generate(); - created = new Date(); + voteUrl = shortid.generate() + created = new Date() expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') if (community.members.length - community.mutedMembers.length === 1) { - //if there is only one member with permissions, start out with 0 votes total so that they have to at least click on the 'vote' button to make it pass - votesNumber = 0; + // if there is only one member with permissions, start out with 0 votes total so that at least someone has to click on the 'vote' button to make it pass + votesNumber = 0 } else { - votesNumber = 1; + // otherwise, assume that the person who created the vote is in favor of it and cause them to have voted for it + votesNumber = 1 } - const vote = new Vote({ + if (isMember) { + const vote = new Vote({ status: 'active', community: req.params.communityid, - reference: req.body.reference, - parsedReference: parsedReference, - proposedValue: proposedValue, - parsedProposedValue: parsedProposedValue, + reference: 'userunban', + proposedValue: req.params.userid, creatorEmail: req.user.email, creator: req.user._id, url: voteUrl, @@ -786,298 +552,528 @@ module.exports = function(app, passport) { voteThreshold: 50, expiryTime: expiryTime, votes: votesNumber, - voters: votesNumber == 1 ? [req.user._id] : [], - }) - voteId = vote._id; - console.log(vote) - vote.save() + voters: votesNumber == 1 ? [req.user._id] : [] + }) + voteId = vote._id + vote.save() .then(vote => { - var expireVote = schedule.scheduleJob(expiryTime, function() { - Vote.findOne({ - _id: vote._id - }) - .then(vote => { - vote.status = "expired" - vote.save(); - }) - }); - expiryTimers.push(expireVote); - community.members.forEach(member => { - if (!member.equals(req.user._id)) { - notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') - } + var expireVote = schedule.scheduleJob(expiryTime, function () { + Vote.findOne({ + _id: vote._id }) - touchCommunity(req.params.communityid) - res.redirect('back') + .then(vote => { + vote.status = 'expired' + vote.save() + }) + }) + expiryTimers.push(expireVote) + community.members.forEach(member => { + notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') + }) + res.end('{"success" : "Updated Successfully", "status" : 200}') }) - }); + } else { + console.log('User not authorised to unban user.') + } + }) + }) - app.post('/api/community/vote/delete/:voteid', isLoggedIn, function(req, res) { - Vote.findById(req.params.voteid).then(vote => { - if (req.user._id.equals(vote.creator)) { - Vote.findByIdAndRemove(req.params.voteid) - .then(vote => { - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - } else { - return res.sendStatus(403); - } - }); + app.post('/api/community/user/accept/:communityid/:userid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid }) - - app.post('/api/community/vote/cast/:communityid/:voteid', isLoggedIn, function(req, res) { - if (!req.user.communities.some(v => v.toString() == req.params.communityid)) { - return res.sendStatus(403); + .then(community => { + const memberIds = community.members.map(a => a._id.toString()) + if (memberIds.includes(req.user._id.toString())) { + isMember = true } - Vote.findOne({ - _id: req.params.voteid + if (isMember) { + community.members.push(req.params.userid) + community.membershipRequests.pull(req.params.userid) + community.save() + User.findOne({ + _id: req.params.userid + }) + .then(user => { + user.communities.push(req.params.communityid) + user.save() + console.log("Notifyin' member " + user.username) + notifier.notify('community', 'requestResponse', user._id, req.user._id, community._id, '/api/community/getbyid/' + community._id, 'approved') + touchCommunity(req.params.communityid) + res.end('{"success" : "Updated Successfully", "status" : 200}') }) - .then(vote => { - if (vote.voters.some(v => v.equals(req.user._id))) { - return res.sendStatus(403); - } - vote.votes++; - vote.voters.push(req.user._id); - vote.save() - .then(vote => { - Community.findOne({ - _id: req.params.communityid - }) - .populate('members') - .then(community => { - let mutedMemberIds = community.mutedMembers.map(a => a._id.toString()); - // Find the number of members who have been active on sweet in the last 2 weeks - // and are not muted - these are members allowed to vote - then work out the - // majority margin required for a vote to pass based on the number of those members - var currentFortnight = moment().clone().subtract(14, 'days').startOf('day'); - let recentlyActiveMembers = community.members.filter((member) => { - return moment(member.lastUpdated).isBetween(currentFortnight, moment()) && - !mutedMemberIds.includes(member._id.toString()); - }) - let majorityMargin = helper.isOdd(recentlyActiveMembers.length) ? (recentlyActiveMembers.length / 2) + 0.5 : (recentlyActiveMembers.length / 2) + 1 + } else { + console.log('User not authorised to approve request.') + } + }) + }) - if (vote.votes >= majorityMargin) { - console.log("Vote passed!") - if (vote.reference == "visibility") { - community.settings[vote.reference] = vote.proposedValue; - Image.find({ - context: "community", - community: community._id - }).then(images => { - console.log(images); - images.forEach(function(image) { - console.log(image); - image.privacy = vote.proposedValue; - image.save(); - }) - }) - } else if (vote.reference == "joinType" || vote.reference == "voteLength") { - community.settings[vote.reference] = vote.proposedValue; - } else if (vote.reference == "description" || vote.reference == "rules") { - community[vote.reference + "Raw"] = vote.proposedValue; - community[vote.reference + "Parsed"] = vote.parsedProposedValue; - } else if (vote.reference == "image") { - fs.rename(global.appRoot + "/public/images/communities/staging/" + vote.proposedValue, global.appRoot + "/public/images/communities/" + vote.proposedValue, function() { - community[vote.reference] = vote.proposedValue; - community['imageEnabled'] = true; - community.save(); - }) - } else if (vote.reference == "name") { - var oldName = community.name; - community.name = vote.proposedValue; - CommunityPlaceholder.deleteOne({ name: vote.proposedValue }, function(err) { console.error(err) }); - community.slug = helper.slugify(vote.proposedValue); //i guess i'm assuming the "slugify" function hasn't been modified since the vote was created - } else if (vote.reference == "userban") { - community.members.pull(vote.proposedValue) - community.bannedMembers.push(vote.proposedValue) - User.findOne({ - _id: vote.proposedValue - }) - .then(user => { - user.communities.pull(req.params.communityid); - user.bannedCommunities.push(req.params.communityid); - user.save() - }) - } else if (vote.reference == "usermute") { - community.mutedMembers.push(vote.proposedValue) - User.findOne({ - _id: vote.proposedValue - }) - .then(user => { - user.mutedCommunities.push(req.params.communityid); - user.save() - }) - } else if (vote.reference == "userunban") { - community.bannedMembers.pull(vote.proposedValue) - User.findOne({ - _id: vote.proposedValue - }) - .then(user => { - user.bannedCommunities.pull(req.params.communityid); - user.save() - }) - } else if (vote.reference == "userunmute") { - community.mutedMembers.pull(vote.proposedValue) - User.findOne({ - _id: vote.proposedValue - }) - .then(user => { - user.mutedCommunities.pull(req.params.communityid); - user.save() - }) - } - community.save() - .then(community => { - Vote.findOne({ - _id: req.params.voteid - }) - .then(vote => { - vote.status = "passed" - vote.save() - if (vote.reference == "userban") { - User.findOne({ - _id: vote.proposedValue - }) - .then(user => { - console.log(user) - Post.deleteMany({ - community: req.params.communityid, - authorEmail: user.email - }, function(callback) { - console.log(callback) - }) - Vote.deleteMany({ - community: req.params.communityid, - creatorEmail: user.email - }, function(callback) { - console.log(callback) - }) - }); - community.members.forEach(member => { - notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'banned') - }) - notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'banned') - } else if (vote.reference == "userunban") { - community.members.forEach(member => { - notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unbanned') - }) - notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unbanned') - } else if (vote.reference == "usermute") { - console.log("User muted - sending notifications") - community.members.forEach(member => { - console.log("Notification sending to " + member); - if (member.equals(vote.proposedValue)) { - notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'muted') - } else { - notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'muted') - } - }) + app.post('/api/community/user/reject/:communityid/:userid', isLoggedIn, function (req, res) { + Community.findOne({ + _id: req.params.communityid + }) + .then(community => { + const memberIds = community.members.map(a => a._id.toString()) + if (memberIds.includes(req.user._id.toString())) { + isMember = true + } + if (isMember) { + community.membershipRequests.pull(req.params.userid) + community.save() + } else { + console.log('User not authorised to approve request.') + } + if (isMember) { + notifier.notify('community', 'requestResponse', req.params.userid, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'rejected') + res.end('{"success" : "Updated Successfully", "status" : 200}') + } + }) + }) - } else if (vote.reference == "userunmute") { - community.members.forEach(member => { - if (member.equals(vote.proposedValue)) { - notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unmuted') - } else { - notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unmuted') - } - }) - } else if (vote.reference == "name") { - community.members.forEach(member => { - if (!member.equals(req.user._id)) { - notifier.notify('community', 'nameChange', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, oldName) - } - }) - } else { - community.members.forEach(member => { - if (member.equals(vote.creator)) { - notifier.notify('community', 'yourVote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'passed') - } else { - notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'passed') - } - }) - } - touchCommunity(req.params.communityid); - if (vote.reference == "name") { - res.end('{"success" : "Updated Successfully", "status" : 302, "redirect": "/community/' + community.slug + '"}'); - } else { - res.end('{"success" : "Updated Successfully", "status" : 200}'); - } - }) - }) - } else { - console.log("Vote cast.") - touchCommunity(req.params.communityid) - res.end('{"success" : "Updated Successfully", "status" : 200}'); - } - }) - }) + app.post('/api/community/vote/create/:communityid', isLoggedIn, async function (req, res) { + var community = await Community.findOne({ + _id: req.params.communityid + }) + if (!community.members.some(v => v.equals(req.user._id))) { + return res.sendStatus(403) + } + console.log(req.body) + if (req.body.reference == 'image') { + imageUrl = shortid.generate() + '.jpg' + if (req.files.proposedValue.data.length > 3145728) { + console.error('Image too large!') + req.session.sessionFlash = { + type: 'warning', + message: 'File too large. The file size limit is 3MB.' + } + return res.redirect('back') + } else { + sharp(req.files.proposedValue.data) + .resize({ + width: 600, + height: 600 + }) + .jpeg({ + quality: 70 + }) + .toFile('./public/images/communities/staging/' + imageUrl) + .catch(err => { + console.error(err) + }) + } + } + const parsedReferences = { + name: 'name', + description: 'description', + rules: 'rules', + image: 'display image', + visibility: 'post visibility', + joinType: 'joining method', + voteLength: 'vote length' + } + const parsedJoinType = { + open: 'Open (anyone is free to join)', + approval: 'Approval (requests to join must be approved by a current member)', + closed: 'Closed (only invited users can join)' + } + const parsedVisibility = { + public: 'Public (posts visible to all sweet users)', + private: 'Private (posts visible only to members)' + } + const parsedVoteLength = { + 1: '1 day', + 3: '3 days', + 7: '7 days', + 14: '14 days', + 30: '30 days' + } + const parsedReference = parsedReferences[req.body.reference] + var allowedChange = true // is there a change? and is it allowed? + if (req.body.reference == 'description' || req.body.reference == 'rules') { + proposedValue = req.body.proposedValue + parsedProposedValue = (await helper.parseText(req.body.proposedValue)).text + if (req.body.reference == 'description') { + allowedChange = (community.descriptionRaw != proposedValue) + } else { + allowedChange = (community.rulesRaw != proposedValue) + } + } else if (req.body.reference == 'joinType') { + proposedValue = req.body.proposedValue + parsedProposedValue = parsedJoinType[req.body.proposedValue] + allowedChange = (parsedProposedValue && community.settings.joinType != proposedValue) // parsedProposedValue will be undefined if req.body.proposedValue wasn't one of the allowed values + } else if (req.body.reference == 'visibility') { + proposedValue = req.body.proposedValue + parsedProposedValue = parsedVisibility[req.body.proposedValue] + allowedChange = (parsedProposedValue && community.settings.visibility != proposedValue) + } else if (req.body.reference == 'voteLength') { + proposedValue = req.body.proposedValue + parsedProposedValue = parsedVoteLength[req.body.proposedValue] + allowedChange = (parsedProposedValue && community.settings.voteLength != parseInt(proposedValue)) + } else if (req.body.reference == 'image') { + proposedValue = imageUrl + parsedProposedValue = imageUrl + } else if (req.body.reference == 'name') { // this is where it gets complicated + proposedValue = req.body.proposedValue + parsedProposedValue = proposedValue + var slug = helper.slugify(proposedValue) + if (!parsedProposedValue || community.name == proposedValue) { + // not using allowedChange for this non-change bc by the time we get to the code that reacts to allowedChange it will have already returned a duplicate name complaint + req.session.sessionFlash = { + type: 'warning', + message: 'That vote would not change anything! But of course none ever seem to anyway' + } + return res.redirect('back') + } else { + allowedChange = true + if (await Community.findOne({ slug: slug }) || await Community.findOne({ name: proposedValue })) { + req.session.sessionFlash = { + type: 'warning', + message: 'The community name/url is unfortunately already taken...' + } + return res.redirect('back') + } + // first, create the placeholder; then, check if you've just created a duplicate (and remove one and cancel if that's the case.) + // it's a bit weird, but it's bc we have no way of checking in advance if there already is one that guarantees there won't be + // a placeholder with our proposed name created between our check and our creation of the new one (bc we don't know what code will + // execute while we are awaiting the save of the new one.) i think this could still result in two changes to the same name being + // proposed at the same time and both of them being rejected, but that's incredibly unlikely and also oh well + var placeholder = new CommunityPlaceholder({ + name: proposedValue, + slug: helper.slugify(proposedValue) + }) + await placeholder.save() + if ((await CommunityPlaceholder.find({ slug: slug })).length > 1 || (await CommunityPlaceholder.find({ name: proposedValue })).length > 1) { + req.session.sessionFlash = { + type: 'warning', + message: 'That community name/url is reserved bc another community is currently voting on it, sorry!' + } + CommunityPlaceholder.deleteOne({ name: proposedValue }, function (err) { console.error(err) }) // the "true" makes it just delete one + return res.redirect('back') + } + } + } + if (!allowedChange) { + req.session.sessionFlash = { + type: 'warning', + message: 'That vote would not change anything! But of course none ever seem to anyway' + } + return res.redirect('back') + } + console.log(community) + voteUrl = shortid.generate() + created = new Date() + expiryTime = moment(created).add((community.settings.voteLength ? community.settings.voteLength : 7), 'd') + if (community.members.length - community.mutedMembers.length === 1) { + // if there is only one member with permissions, start out with 0 votes total so that they have to at least click on the 'vote' button to make it pass + votesNumber = 0 + } else { + votesNumber = 1 + } + const vote = new Vote({ + status: 'active', + community: req.params.communityid, + reference: req.body.reference, + parsedReference: parsedReference, + proposedValue: proposedValue, + parsedProposedValue: parsedProposedValue, + creatorEmail: req.user.email, + creator: req.user._id, + url: voteUrl, + timestamp: created, + lastUpdated: created, + voteThreshold: 50, + expiryTime: expiryTime, + votes: votesNumber, + voters: votesNumber == 1 ? [req.user._id] : [] + }) + voteId = vote._id + console.log(vote) + vote.save() + .then(vote => { + var expireVote = schedule.scheduleJob(expiryTime, function () { + Vote.findOne({ + _id: vote._id + }) + .then(vote => { + vote.status = 'expired' + vote.save() }) - }); + }) + expiryTimers.push(expireVote) + community.members.forEach(member => { + if (!member.equals(req.user._id)) { + notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'created') + } + }) + touchCommunity(req.params.communityid) + res.redirect('back') + }) + }) - app.post('/api/community/welcomemessage/update/:communityid', isLoggedIn, function(req, res) { + app.post('/api/community/vote/delete/:voteid', isLoggedIn, function (req, res) { + Vote.findById(req.params.voteid).then(vote => { + if (req.user._id.equals(vote.creator)) { + Vote.findByIdAndRemove(req.params.voteid) + .then(vote => { + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + } else { + return res.sendStatus(403) + } + }) + }) - function isCommunityMember(communityId) { - return Community.findOne({ - _id: communityId - }) - .then(community => { - return community.members.some(member => { - return req.user._id.equals(member); - }); - }) + app.post('/api/community/vote/cast/:communityid/:voteid', isLoggedIn, function (req, res) { + if (!req.user.communities.some(v => v.toString() == req.params.communityid)) { + return res.sendStatus(403) + } + Vote.findOne({ + _id: req.params.voteid + }) + .then(vote => { + if (vote.voters.some(v => v.equals(req.user._id))) { + return res.sendStatus(403) } - - Community.findOne({ - _id: req.params.communityid + vote.votes++ + vote.voters.push(req.user._id) + vote.save() + .then(vote => { + Community.findOne({ + _id: req.params.communityid }) - .then(async function(community) { - if (await isCommunityMember(community)) { - community.welcomeMessageRaw = req.body.proposedValue; - community.welcomeMessageParsed = (await helper.parseText(req.body.proposedValue)).text - community.welcomeMessageAuthor = req.user._id - community.save() - .then(result => { - res.redirect('back') + .populate('members') + .then(community => { + const mutedMemberIds = community.mutedMembers.map(a => a._id.toString()) + // Find the number of members who have been active on sweet in the last 2 weeks + // and are not muted - these are members allowed to vote - then work out the + // majority margin required for a vote to pass based on the number of those members + var currentFortnight = moment().clone().subtract(14, 'days').startOf('day') + const recentlyActiveMembers = community.members.filter((member) => { + return moment(member.lastUpdated).isBetween(currentFortnight, moment()) && + !mutedMemberIds.includes(member._id.toString()) + }) + const majorityMargin = helper.isOdd(recentlyActiveMembers.length) ? (recentlyActiveMembers.length / 2) + 0.5 : (recentlyActiveMembers.length / 2) + 1 + + if (vote.votes >= majorityMargin) { + console.log('Vote passed!') + if (vote.reference == 'visibility') { + community.settings[vote.reference] = vote.proposedValue + Image.find({ + context: 'community', + community: community._id + }).then(images => { + console.log(images) + images.forEach(function (image) { + console.log(image) + image.privacy = vote.proposedValue + image.save() + }) + }) + } else if (vote.reference == 'joinType' || vote.reference == 'voteLength') { + community.settings[vote.reference] = vote.proposedValue + } else if (vote.reference == 'description' || vote.reference == 'rules') { + community[vote.reference + 'Raw'] = vote.proposedValue + community[vote.reference + 'Parsed'] = vote.parsedProposedValue + } else if (vote.reference == 'image') { + fs.rename(global.appRoot + '/public/images/communities/staging/' + vote.proposedValue, global.appRoot + '/public/images/communities/' + vote.proposedValue, function () { + community[vote.reference] = vote.proposedValue + community.imageEnabled = true + community.save() + }) + } else if (vote.reference == 'name') { + var oldName = community.name + community.name = vote.proposedValue + CommunityPlaceholder.deleteOne({ name: vote.proposedValue }, function (err) { console.error(err) }) + community.slug = helper.slugify(vote.proposedValue) // i guess i'm assuming the "slugify" function hasn't been modified since the vote was created + } else if (vote.reference == 'userban') { + community.members.pull(vote.proposedValue) + community.bannedMembers.push(vote.proposedValue) + User.findOne({ + _id: vote.proposedValue + }) + .then(user => { + user.communities.pull(req.params.communityid) + user.bannedCommunities.push(req.params.communityid) + user.save() + }) + } else if (vote.reference == 'usermute') { + community.mutedMembers.push(vote.proposedValue) + User.findOne({ + _id: vote.proposedValue + }) + .then(user => { + user.mutedCommunities.push(req.params.communityid) + user.save() + }) + } else if (vote.reference == 'userunban') { + community.bannedMembers.pull(vote.proposedValue) + User.findOne({ + _id: vote.proposedValue + }) + .then(user => { + user.bannedCommunities.pull(req.params.communityid) + user.save() + }) + } else if (vote.reference == 'userunmute') { + community.mutedMembers.pull(vote.proposedValue) + User.findOne({ + _id: vote.proposedValue + }) + .then(user => { + user.mutedCommunities.pull(req.params.communityid) + user.save() + }) + } + community.save() + .then(community => { + Vote.findOne({ + _id: req.params.voteid + }) + .then(vote => { + vote.status = 'passed' + vote.save() + if (vote.reference == 'userban') { + User.findOne({ + _id: vote.proposedValue + }) + .then(user => { + console.log(user) + Post.deleteMany({ + community: req.params.communityid, + authorEmail: user.email + }, function (callback) { + console.log(callback) + }) + Vote.deleteMany({ + community: req.params.communityid, + creatorEmail: user.email + }, function (callback) { + console.log(callback) + }) + }) + community.members.forEach(member => { + notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'banned') + }) + notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'banned') + } else if (vote.reference == 'userunban') { + community.members.forEach(member => { + notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unbanned') + }) + notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unbanned') + } else if (vote.reference == 'usermute') { + console.log('User muted - sending notifications') + community.members.forEach(member => { + console.log('Notification sending to ' + member) + if (member.equals(vote.proposedValue)) { + notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'muted') + } else { + notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'muted') + } + }) + } else if (vote.reference == 'userunmute') { + community.members.forEach(member => { + if (member.equals(vote.proposedValue)) { + notifier.notify('community', 'managementResponse', vote.proposedValue, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unmuted') + } else { + notifier.notify('community', 'management', member, vote.proposedValue, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'unmuted') + } + }) + } else if (vote.reference == 'name') { + community.members.forEach(member => { + if (!member.equals(req.user._id)) { + notifier.notify('community', 'nameChange', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, oldName) + } + }) + } else { + community.members.forEach(member => { + if (member.equals(vote.creator)) { + notifier.notify('community', 'yourVote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'passed') + } else { + notifier.notify('community', 'vote', member, req.user._id, req.params.communityid, '/api/community/getbyid/' + req.params.communityid, 'passed') + } + }) + } + touchCommunity(req.params.communityid) + if (vote.reference == 'name') { + res.end('{"success" : "Updated Successfully", "status" : 302, "redirect": "/community/' + community.slug + '"}') + } else { + res.end('{"success" : "Updated Successfully", "status" : 200}') + } }) + }) } else { - res.redirect('back') + console.log('Vote cast.') + touchCommunity(req.params.communityid) + res.end('{"success" : "Updated Successfully", "status" : 200}') } - }) - }) + }) + }) + }) + }) - app.post('/api/community/vote/withdraw/:communityid/:voteid', isLoggedIn, function(req, res) { - Vote.findOne({ - _id: req.params.voteid - }) - .then(vote => { - if (!vote.voters.some(v => v.equals(req.user._id))) { - return res.sendStatus(403); - } - vote.votes--; - vote.voters.pull(req.user._id); - vote.save() - .then(vote => { - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - }) - }); -} + app.post('/api/community/welcomemessage/update/:communityid', isLoggedIn, function (req, res) { + function isCommunityMember (communityId) { + return Community.findOne({ + _id: communityId + }) + .then(community => { + return community.members.some(member => { + return req.user._id.equals(member) + }) + }) + } -function touchCommunity(id) { - Community.findOneAndUpdate({ - _id: id - }, { - $set: { - lastUpdated: new Date() + Community.findOne({ + _id: req.params.communityid + }) + .then(async function (community) { + if (await isCommunityMember(community)) { + community.welcomeMessageRaw = req.body.proposedValue + community.welcomeMessageParsed = (await helper.parseText(req.body.proposedValue)).text + community.welcomeMessageAuthor = req.user._id + community.save() + .then(result => { + res.redirect('back') + }) + } else { + res.redirect('back') } - }).then(community => { - console.log("Updated community!") + }) + }) + + app.post('/api/community/vote/withdraw/:communityid/:voteid', isLoggedIn, function (req, res) { + Vote.findOne({ + _id: req.params.voteid }) + .then(vote => { + if (!vote.voters.some(v => v.equals(req.user._id))) { + return res.sendStatus(403) + } + vote.votes-- + vote.voters.pull(req.user._id) + vote.save() + .then(vote => { + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + }) + }) } -function isLoggedIn(req, res, next) { - if (req.isAuthenticated()) { - return next(); +function touchCommunity (id) { + Community.findOneAndUpdate({ + _id: id + }, { + $set: { + lastUpdated: new Date() } - res.redirect('/'); -} \ No newline at end of file + }).then(community => { + console.log('Updated community!') + }) +} + +function isLoggedIn (req, res, next) { + if (req.isAuthenticated()) { + return next() + } + res.redirect('/') +} diff --git a/app/mail.js b/app/mail.js new file mode 100644 index 00000000..1273ebde --- /dev/null +++ b/app/mail.js @@ -0,0 +1,19 @@ +let sgMail + +if (process.env.NODE_ENV === 'production') { + var apiConfig = require('./apis.js') + sgMail = require('@sendgrid/mail') + sgMail.setApiKey(apiConfig.sendgrid) +} else { + sgMail = { + send: (msg) => { + console.log('====================') + console.log('== Sending email:') + console.log(msg) + console.log('====================') + return Promise.resolve() + } + } +} + +module.exports = sgMail diff --git a/app/models/community.js b/app/models/community.js index 18a4ab02..d7171daf 100644 --- a/app/models/community.js +++ b/app/models/community.js @@ -30,11 +30,10 @@ var communitySchema = new mongoose.Schema({ membershipRequests: [{ type: DBReference, ref: 'User' }], posts: [{ type: DBReference, ref: 'Post' }], votes: [{ type: DBReference, ref: 'Vote' }] -}); +}) communitySchema.pre('validate', function (next) { - if (!this.votingMembersCount) - this.votingMembersCount = this.members.length; + if (!this.votingMembersCount) { this.votingMembersCount = this.members.length } this.membersCount = this.members.length this.requestsCount = this.membershipRequests.length this.mutedMembersCount = this.mutedMembers.length @@ -42,16 +41,16 @@ communitySchema.pre('validate', function (next) { mutedMembersIds = this.mutedMembers.map(String) mutedUsersWhoAreMembers = mutedMembersIds.filter(id => membersIds.includes(id)) votingMembers = this.membersCount - mutedUsersWhoAreMembers.length - this.votingMembersCount = votingMembers; + this.votingMembersCount = votingMembers if (this.membersCount === 0) { - this.settings.joinType = "open"; - this.membershipRequests = []; - this.requestsCount = 0; + this.settings.joinType = 'open' + this.membershipRequests = [] + this.requestsCount = 0 } - next(); -}); + next() +}) -communitySchema.index({slug:1}); +communitySchema.index({ slug: 1 }) var communityPlaceholderSchema = new mongoose.Schema({ name: String, @@ -60,8 +59,8 @@ var communityPlaceholderSchema = new mongoose.Schema({ vote: { type: DBReference, ref: 'Vote' } }) -//just retrieve this with mongoose.model('Community Placeholder') in the one place in which it is needed -mongoose.model('Community Placeholder', communityPlaceholderSchema); +// just retrieve this with mongoose.model('Community Placeholder') in the one place in which it is needed +mongoose.model('Community Placeholder', communityPlaceholderSchema) // create the model for users and expose it to our app -module.exports = mongoose.model('Community', communitySchema); \ No newline at end of file +module.exports = mongoose.model('Community', communitySchema) diff --git a/app/models/image.js b/app/models/image.js index 0e377c4f..f1dd5d3c 100644 --- a/app/models/image.js +++ b/app/models/image.js @@ -1,6 +1,6 @@ var imageSchema = new mongoose.Schema({ - context: String, //either "user" or "community", corresponding to the post schema's type field's values "original" and "community" - filename: {type: String, unique: true}, + context: String, // either "user" or "community", corresponding to the post schema's type field's values "original" and "community" + filename: { type: String, unique: true }, privacy: String, accessToken: String, user: String, @@ -9,9 +9,9 @@ var imageSchema = new mongoose.Schema({ description: String, height: Number, width: Number, - quality: String, -}); + quality: String +}) -imageSchema.index({filename:1}); +imageSchema.index({ filename: 1 }) -module.exports = mongoose.model('Image', imageSchema); +module.exports = mongoose.model('Image', imageSchema) diff --git a/app/models/post.js b/app/models/post.js index 9cac6640..387688ac 100644 --- a/app/models/post.js +++ b/app/models/post.js @@ -1,149 +1,149 @@ -//this is used by older posts instead of the below inlineElementSchema +// this is used by older posts instead of the below inlineElementSchema var embedSchema = new mongoose.Schema({ - type: String, //"video" always - linkUrl: String, - embedUrl: String, - title: String, - description: String, - image: String, - domain: String, + type: String, // "video" always + linkUrl: String, + embedUrl: String, + title: String, + description: String, + image: String, + domain: String }) var inlineElementSchema = new mongoose.Schema({ - type: String, //either "link-preview" or "image(s)" - - position: Number, //what number "paragraph" this should be when it's mixed into parsedContent, where a paragraph is a

...

,
    ...
, or a
. - - //used if link-preview: - isEmbeddableVideo: Boolean, - linkUrl: String, - embedUrl: String, //only defined if isEmbeddableVideo is true - title: String, - image: String, - description: String, - domain: String, - - //used if image(s) (yeah, it's the same parallel arrays as the the old post formats): - images: [String], - imageDescriptions: [String], - //it would take a database transform for the old posts, but these fields should probably be combined into imageOrientationType or something. currently, if an image is vertical - //it has 'vertical-image' stored at the same index as it in the imageIsVertical array and a blank string otherwise, and the same for horizontality. imageOrientationType could just - //store 'vertical-image', 'horizontal-image', or a blank string. - imageIsVertical: [String], - imageIsHorizontal: [String], -}); - -//this is really similar to the embed schema but i only just realized that and i don't feel like changing this + type: String, // either "link-preview" or "image(s)" + + position: Number, // what number "paragraph" this should be when it's mixed into parsedContent, where a paragraph is a

...

,
    ...
, or a
. + + // used if link-preview: + isEmbeddableVideo: Boolean, + linkUrl: String, + embedUrl: String, // only defined if isEmbeddableVideo is true + title: String, + image: String, + description: String, + domain: String, + + // used if image(s) (yeah, it's the same parallel arrays as the the old post formats): + images: [String], + imageDescriptions: [String], + // it would take a database transform for the old posts, but these fields should probably be combined into imageOrientationType or something. currently, if an image is vertical + // it has 'vertical-image' stored at the same index as it in the imageIsVertical array and a blank string otherwise, and the same for horizontality. imageOrientationType could just + // store 'vertical-image', 'horizontal-image', or a blank string. + imageIsVertical: [String], + imageIsHorizontal: [String] +}) + +// this is really similar to the embed schema but i only just realized that and i don't feel like changing this var linkPreviewCacheSchema = new mongoose.Schema({ - isEmbeddableVideo: Boolean, - retrievalUrl: String, //the url without the protocol - this is used to retrieve the document (works if the user inputs the url with http://, https://, or neither) - linkUrl: String, //the url with the correct protocol (determined by the request package) - embedUrl: String, //only used if isEmbeddableVideo is true - title: String, - image: String, - description: String, - domain: String, + isEmbeddableVideo: Boolean, + retrievalUrl: String, // the url without the protocol - this is used to retrieve the document (works if the user inputs the url with http://, https://, or neither) + linkUrl: String, // the url with the correct protocol (determined by the request package) + embedUrl: String, // only used if isEmbeddableVideo is true + title: String, + image: String, + description: String, + domain: String }) var commentSchema = new mongoose.Schema({ - authorEmail: String, - author: { type: DBReference, ref: 'User' }, - timestamp: Date, - rawContent: String, - parsedContent: String, - mentions: [String], - tags: [String], - - //image parallel arrays - images: [String], - imageDescriptions: [String], - //it would take a database transform for the old posts, but these fields should probably be combined into imageOrientationType or something. currently, if an image is vertical - //it has 'vertical-image' stored at the same index as it in the imageIsVertical array and a blank string otherwise, and the same for horizontality. imageOrientationType could just - //store 'vertical-image', 'horizontal-image', or a blank string. - imageIsVertical: [String], - imageIsHorizontal: [String], - - inlineElements: [inlineElementSchema], //this is used instead of the image parallel arrays in newer comments. keep in mind you can't check the post's imageVersion to see if this was used, bc comments using this model can still be made on old posts - - deleted: { type: Boolean, default: false }, - cachedHTML: { //was rendered with either the versions of the templates indicated by the post's corresponding cachedHTML MTimes or the version that was available when the comment was created, whichever is newer - fullContentHTML: String - } -}); - -commentSchema.add({ replies: [commentSchema] }); + authorEmail: String, + author: { type: DBReference, ref: 'User' }, + timestamp: Date, + rawContent: String, + parsedContent: String, + mentions: [String], + tags: [String], + + // image parallel arrays + images: [String], + imageDescriptions: [String], + // it would take a database transform for the old posts, but these fields should probably be combined into imageOrientationType or something. currently, if an image is vertical + // it has 'vertical-image' stored at the same index as it in the imageIsVertical array and a blank string otherwise, and the same for horizontality. imageOrientationType could just + // store 'vertical-image', 'horizontal-image', or a blank string. + imageIsVertical: [String], + imageIsHorizontal: [String], + + inlineElements: [inlineElementSchema], // this is used instead of the image parallel arrays in newer comments. keep in mind you can't check the post's imageVersion to see if this was used, bc comments using this model can still be made on old posts + + deleted: { type: Boolean, default: false }, + cachedHTML: { // was rendered with either the versions of the templates indicated by the post's corresponding cachedHTML MTimes or the version that was available when the comment was created, whichever is newer + fullContentHTML: String + } +}) + +commentSchema.add({ replies: [commentSchema] }) var boostSchema = new mongoose.Schema({ - booster: { type: DBReference, ref: 'User', required: true }, - timestamp: { type: Date, required: true }, - boost: { type: DBReference, ref: 'Post' } + booster: { type: DBReference, ref: 'User', required: true }, + timestamp: { type: Date, required: true }, + boost: { type: DBReference, ref: 'Post' } }) var postSchema = new mongoose.Schema({ - type: String, //"original", "community", or "boost". note that the equivalent "context" field in image documents stores either "user", "community", or "user" - community: { type: DBReference, ref: 'Community' }, //hopefully undefined if type=="user" - authorEmail: { type: String, required: true }, - author: { type: DBReference, ref: 'User' }, - url: { type: String, required: true }, - privacy: { type: String, required: true }, - timestamp: { type: Date, required: true }, - lastUpdated: Date, //intially equal to timestamp, updated as comments are left - lastEdited: Date, //initially undefined (although it wouldn't be crazy to set it equal to timestamp initially) and then changed when the post content is edited through the saveedits route in postingToSweet.js - rawContent: String, //this was originally used to store plain text input i believe and then suddenly that wasn't a thing anymore and then it stored html input and now it stores a stringified object containing inlineElements objects - parsedContent: String, //this is the field that is used to store the text contents of the post in html form and is used to generate the full post html, which is stored in cachedHTML.fullContentHTML below - comments: [commentSchema], - boostTarget: { type: DBReference, ref: 'Post' }, - numberOfComments: Number, - mentions: [String], - tags: [String], - //boosts of this post will produce seperate post documents in the database that are linked to here. they link back to the original post through the boostTarget field. - boostsV2: [{ type: boostSchema, required: true }], - contentWarnings: String, - commentsDisabled: Boolean, - - //1: array of filenames stored in /public/images/uploads/[filename] (and so publicly accessible through the url /images/uploads/[filename]); - //2=array of filenames stored in /cdn/images/[filename] accessible through /api/image/display/[filename] (which checks the user's permissions and the image's privacy;) and - //3: image data stored in inlineElements instead and accessible using that second url/path scheme - //comments may have an older imageVersion if the post was edited or a newer one if the comments are more recent than the post - imageVersion: Number, - - //image parallel arrays (no positions, images were all put at the end): - images: [String], - imageDescriptions: [String], - //it would take a database transform for the old posts, but these fields should probably be combined into imageOrientationType or something. currently, if an image is vertical - //it has 'vertical-image' stored at the same index as it in the imageIsVertical array and a blank string otherwise, and the same for horizontality. imageOrientationType could just - //store 'vertical-image', 'horizontal-image', or a blank string. - imageIsVertical: [String], - imageIsHorizontal: [String], - - subscribedUsers: [String], - unsubscribedUsers: [String], - - embeds: [embedSchema], //this was only used simultanously with imageVersion 2, so you can check for that i guess. embeds are stored in inlineElements under version 3 - - inlineElements: [inlineElementSchema], //this is used instead of the image parallel arrays and the embeds in newer posts - - cachedHTML: { //the below MTimes also set a floor for the rendering date of the cached comment html (none will have older cached html, newer comments may have newer cached html, either way it'll all be brought up to date when the post is displayed) - fullContentHTML: String, - imageGalleryMTime: Date, //the last modified date of the imagegallery template when the html was rendered - embedsMTime: Date //the last modified date of the embeds template when the html was rendered - } -}); - -//used to select posts to display in feeds -postSchema.index({ author: 1 }); -postSchema.index({ community: 1 }); - -//used to sort posts in feeds -postSchema.index({ lastUpdated: -1 }); -postSchema.index({ timestamp: -1 }); - -//honestly only used by the active users graph but what the hell -postSchema.index({ 'comments.timestamp': -1 }); - -linkPreviewCacheSchema.index({ retrievalUrl: 1 }); -//just retrieve this with mongoose.model('Cached Link Metadata') in the one place in which it is needed -mongoose.model('Cached Link Metadata', linkPreviewCacheSchema); + type: String, // "original", "community", or "boost". note that the equivalent "context" field in image documents stores either "user", "community", or "user" + community: { type: DBReference, ref: 'Community' }, // hopefully undefined if type=="user" + authorEmail: { type: String, required: true }, + author: { type: DBReference, ref: 'User' }, + url: { type: String, required: true }, + privacy: { type: String, required: true }, + timestamp: { type: Date, required: true }, + lastUpdated: Date, // intially equal to timestamp, updated as comments are left + lastEdited: Date, // initially undefined (although it wouldn't be crazy to set it equal to timestamp initially) and then changed when the post content is edited through the saveedits route in postingToSweet.js + rawContent: String, // this was originally used to store plain text input i believe and then suddenly that wasn't a thing anymore and then it stored html input and now it stores a stringified object containing inlineElements objects + parsedContent: String, // this is the field that is used to store the text contents of the post in html form and is used to generate the full post html, which is stored in cachedHTML.fullContentHTML below + comments: [commentSchema], + boostTarget: { type: DBReference, ref: 'Post' }, + numberOfComments: Number, + mentions: [String], + tags: [String], + // boosts of this post will produce seperate post documents in the database that are linked to here. they link back to the original post through the boostTarget field. + boostsV2: [{ type: boostSchema, required: true }], + contentWarnings: String, + commentsDisabled: Boolean, + + // 1: array of filenames stored in /public/images/uploads/[filename] (and so publicly accessible through the url /images/uploads/[filename]); + // 2=array of filenames stored in /cdn/images/[filename] accessible through /api/image/display/[filename] (which checks the user's permissions and the image's privacy;) and + // 3: image data stored in inlineElements instead and accessible using that second url/path scheme + // comments may have an older imageVersion if the post was edited or a newer one if the comments are more recent than the post + imageVersion: Number, + + // image parallel arrays (no positions, images were all put at the end): + images: [String], + imageDescriptions: [String], + // it would take a database transform for the old posts, but these fields should probably be combined into imageOrientationType or something. currently, if an image is vertical + // it has 'vertical-image' stored at the same index as it in the imageIsVertical array and a blank string otherwise, and the same for horizontality. imageOrientationType could just + // store 'vertical-image', 'horizontal-image', or a blank string. + imageIsVertical: [String], + imageIsHorizontal: [String], + + subscribedUsers: [String], + unsubscribedUsers: [String], + + embeds: [embedSchema], // this was only used simultanously with imageVersion 2, so you can check for that i guess. embeds are stored in inlineElements under version 3 + + inlineElements: [inlineElementSchema], // this is used instead of the image parallel arrays and the embeds in newer posts + + cachedHTML: { // the below MTimes also set a floor for the rendering date of the cached comment html (none will have older cached html, newer comments may have newer cached html, either way it'll all be brought up to date when the post is displayed) + fullContentHTML: String, + imageGalleryMTime: Date, // the last modified date of the imagegallery template when the html was rendered + embedsMTime: Date // the last modified date of the embeds template when the html was rendered + } +}) + +// used to select posts to display in feeds +postSchema.index({ author: 1 }) +postSchema.index({ community: 1 }) + +// used to sort posts in feeds +postSchema.index({ lastUpdated: -1 }) +postSchema.index({ timestamp: -1 }) + +// honestly only used by the active users graph but what the hell +postSchema.index({ 'comments.timestamp': -1 }) + +linkPreviewCacheSchema.index({ retrievalUrl: 1 }) +// just retrieve this with mongoose.model('Cached Link Metadata') in the one place in which it is needed +mongoose.model('Cached Link Metadata', linkPreviewCacheSchema) // create the model for users and expose it to our app -module.exports = mongoose.model('Post', postSchema); \ No newline at end of file +module.exports = mongoose.model('Post', postSchema) diff --git a/app/models/relationship.js b/app/models/relationship.js index 220f09e1..d17fccc1 100644 --- a/app/models/relationship.js +++ b/app/models/relationship.js @@ -11,16 +11,16 @@ var relationshipSchema = new mongoose.Schema({ fromUser: { type: DBReference, ref: 'User' }, toUser: { type: DBReference, ref: 'User' }, value: { - type: String, - required: true //currently implemented possible values: follow, trust, flag, mute - }, + type: String, + required: true // currently implemented possible values: follow, trust, flag, mute + }, note: { type: String } -}); +}) -relationshipSchema.index({fromUser:1,toUser:1}) -relationshipSchema.index({value:1}); +relationshipSchema.index({ fromUser: 1, toUser: 1 }) +relationshipSchema.index({ value: 1 }) // create the model for users and expose it to our app -module.exports = mongoose.model('Relationship', relationshipSchema); +module.exports = mongoose.model('Relationship', relationshipSchema) diff --git a/app/models/tag.js b/app/models/tag.js index ac6608d7..e9979e28 100644 --- a/app/models/tag.js +++ b/app/models/tag.js @@ -2,8 +2,8 @@ var tagSchema = new mongoose.Schema({ name: String, posts: [String], lastUpdated: Date -}); +}) -tagSchema.index({name:1}); +tagSchema.index({ name: 1 }) -module.exports = mongoose.model('Tag', tagSchema); +module.exports = mongoose.model('Tag', tagSchema) diff --git a/app/models/user.js b/app/models/user.js index 5e6a3909..9ffae4bc 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -8,8 +8,7 @@ var notificationSchema = new mongoose.Schema({ url: String, seen: { type: Boolean, default: false }, clicked: { type: Boolean, default: false } -}); - +}) // define the schema for our user model var userSchema = new mongoose.Schema({ @@ -28,9 +27,9 @@ var userSchema = new mongoose.Schema({ required: true }, password: { - type: String, - required: true - }, + type: String, + required: true + }, passwordResetToken: String, passwordResetTokenExpiry: Date, image: { @@ -47,21 +46,21 @@ var userSchema = new mongoose.Schema({ websiteParsed: String, location: String, settings: { - timezone: { type: String, default: "auto" }, - autoDetectedTimeZone: {type: String, default: "" }, //this will get set to moment's guess of their timezone as soon as they next make a settings change, which they'll have to do to start getting emails which is the only thing that brings timezones into play anyway - profileVisibility: { type: String, default: "invisible" }, - newPostPrivacy: { type: String, default: "public" }, - imageQuality: { type: String, default: "standard" }, - homeTagTimelineSorting: { type: String, default: "fluid" }, - userTimelineSorting: { type: String, default: "chronological" }, - communityTimelineSorting: { type: String, default: "fluid" }, + timezone: { type: String, default: 'auto' }, + autoDetectedTimeZone: { type: String, default: '' }, // this will get set to moment's guess of their timezone as soon as they next make a settings change, which they'll have to do to start getting emails which is the only thing that brings timezones into play anyway + profileVisibility: { type: String, default: 'invisible' }, + newPostPrivacy: { type: String, default: 'public' }, + imageQuality: { type: String, default: 'standard' }, + homeTagTimelineSorting: { type: String, default: 'fluid' }, + userTimelineSorting: { type: String, default: 'chronological' }, + communityTimelineSorting: { type: String, default: 'fluid' }, flashRecentComments: { type: Boolean, default: true }, - digestEmailFrequency: { type: String, default: "off" }, - emailTime: { type: String, default: "17:00" }, - emailDay: { type: String, default: "Sunday" }, + digestEmailFrequency: { type: String, default: 'off' }, + emailTime: { type: String, default: '17:00' }, + emailDay: { type: String, default: 'Sunday' }, showRecommendations: { type: Boolean, default: true }, showHashtags: { type: Boolean, default: true }, - sendMentionEmails: { type: Boolean, default: true }, + sendMentionEmails: { type: Boolean, default: true } }, notifications: [notificationSchema], pushNotifSubscriptions: [String], @@ -70,20 +69,20 @@ var userSchema = new mongoose.Schema({ mutedCommunities: [{ type: DBReference, ref: 'Community' }], hiddenRecommendedUsers: [String], hiddenRecommendedCommunities: [String] -}); +}) // methods ====================== // generating a hash -userSchema.methods.generateHash = function(password) { - return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); -}; +userSchema.methods.generateHash = function (password) { + return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null) +} // checking if password is valid -userSchema.methods.validPassword = function(password) { - return bcrypt.compareSync(password, this.password); -}; +userSchema.methods.validPassword = function (password) { + return bcrypt.compareSync(password, this.password) +} -userSchema.index({username:1}); +userSchema.index({ username: 1 }) // create the model for users and expose it to our app -module.exports = mongoose.model('User', userSchema); +module.exports = mongoose.model('User', userSchema) diff --git a/app/models/vote.js b/app/models/vote.js index 3ea50444..b31d8069 100644 --- a/app/models/vote.js +++ b/app/models/vote.js @@ -13,14 +13,14 @@ var commentSchema = new mongoose.Schema({ parsedContent: String, mentions: [String], tags: [String] -}); +}) var voteSchema = new mongoose.Schema({ - status: String, //'active' or 'expired' - type: String, //no longer used, i believe + status: String, // 'active' or 'expired' + type: String, // no longer used, i believe community: { type: DBReference, ref: 'Community' }, - reference: String, //what is possibly being changed: description, rules, joinType, visibility, voteLength, image, or name - parsedReference: String, //a human-readble (non-camelCase) equivalent to one of the above + reference: String, // what is possibly being changed: description, rules, joinType, visibility, voteLength, image, or name + parsedReference: String, // a human-readble (non-camelCase) equivalent to one of the above proposedValue: String, parsedProposedValue: String, creatorEmail: String, @@ -38,8 +38,8 @@ var voteSchema = new mongoose.Schema({ votes: Number, voters: [{ type: DBReference, ref: 'User' }], voteThreshold: Number, - expiryTime: Date, -}); + expiryTime: Date +}) // create the model for users and expose it to our app -module.exports = mongoose.model('Vote', voteSchema); +module.exports = mongoose.model('Vote', voteSchema) diff --git a/app/notifier.js b/app/notifier.js index d6cc2a97..4118ecf2 100644 --- a/app/notifier.js +++ b/app/notifier.js @@ -1,141 +1,141 @@ -function markRead(userId, subjectId) { - User.findOne({ - _id: userId - }, 'notifications') - .then(user => { - user.notifications.forEach(notification => { - if (notification.seen == false && notification.subjectId == subjectId) { - notification.seen = true; - } - }) - user.save(); - }) +function markRead (userId, subjectId) { + User.findOne({ + _id: userId + }, 'notifications') + .then(user => { + user.notifications.forEach(notification => { + if (notification.seen == false && notification.subjectId == subjectId) { + notification.seen = true + } + }) + user.save() + }) } -function notify(type, cause, notifieeID, sourceId, subjectId, url, context) { - function buildNotification() { - switch (type) { - case 'user': - return User.findOne({ _id: sourceId }) - .then(user => { - const notifTexts = { - reply: 'replied to your post.', - boost: 'boosted your post.', - subscribedReply: 'replied to a post you have also replied to.', - mentioningPostReply: 'replied to a post you were mentioned in.', - boostedPostReply: 'replied to a post you boosted.', - commentReply: 'replied to your comment.', - mention: 'mentioned you in a ' + context + '.', - relationship: 'now ' + context + 's you.' - }; - const notifEmails = { - mention: "mentioned you on sweet 🙌" - }; - var text = notifTexts[cause]; - var image = '/images/' + (user.imageEnabled ? user.image : 'cake.svg'); - var username = '@' + user.username; - var final = '' + username + ' ' + text; - var emailText = notifEmails[cause] ? notifEmails[cause] : ""; - return { - image: image, - text: final, - emailText: emailText - }; - }) - case 'community': - return User.findOne({ _id: sourceId }) - .then(user => { - return Community.findOne({ - _id: subjectId - }) - .then(community => { - var commNotifTexts = { - request: '@' + user.username + ' has asked to join ' + community.name + '.', - requestResponse: 'Your request to join ' + community.name + ' has been ' + context + '.', - vote: 'A vote has been ' + context + ' in ' + community.name + '.', - yourVote: 'Your vote has been ' + context + ' in ' + community.name + '.', - management: '@' + user.username + ' has been ' + context + ' from ' + community.name + '.', - managementResponse: 'You have been ' + context + ' from ' + community.name + '.', - nameChange: "The name of the community " + context + " has been changed to " + community.name + '.' - } - var text = commNotifTexts[cause]; - var image = '/images/communities/' + (community.imageEnabled ? community.image : 'cake.svg'); - return { - image: image, - text: text - }; - }) - }) - } +function notify (type, cause, notifieeID, sourceId, subjectId, url, context) { + function buildNotification () { + switch (type) { + case 'user': + return User.findOne({ _id: sourceId }) + .then(user => { + const notifTexts = { + reply: 'replied to your post.', + boost: 'boosted your post.', + subscribedReply: 'replied to a post you have also replied to.', + mentioningPostReply: 'replied to a post you were mentioned in.', + boostedPostReply: 'replied to a post you boosted.', + commentReply: 'replied to your comment.', + mention: 'mentioned you in a ' + context + '.', + relationship: 'now ' + context + 's you.' + } + const notifEmails = { + mention: 'mentioned you on sweet 🙌' + } + var text = notifTexts[cause] + var image = '/images/' + (user.imageEnabled ? user.image : 'cake.svg') + var username = '@' + user.username + var final = '' + username + ' ' + text + var emailText = notifEmails[cause] ? notifEmails[cause] : '' + return { + image: image, + text: final, + emailText: emailText + } + }) + case 'community': + return User.findOne({ _id: sourceId }) + .then(user => { + return Community.findOne({ + _id: subjectId + }) + .then(community => { + var commNotifTexts = { + request: '@' + user.username + ' has asked to join ' + community.name + '.', + requestResponse: 'Your request to join ' + community.name + ' has been ' + context + '.', + vote: 'A vote has been ' + context + ' in ' + community.name + '.', + yourVote: 'Your vote has been ' + context + ' in ' + community.name + '.', + management: '@' + user.username + ' has been ' + context + ' from ' + community.name + '.', + managementResponse: 'You have been ' + context + ' from ' + community.name + '.', + nameChange: 'The name of the community ' + context + ' has been changed to ' + community.name + '.' + } + var text = commNotifTexts[cause] + var image = '/images/communities/' + (community.imageEnabled ? community.image : 'cake.svg') + return { + image: image, + text: text + } + }) + }) } - console.log("creating notification") - User.findOne({ - _id: notifieeID - }) - .then(notifiedUser => { - buildNotification() - .then(async response => { - //send the user push notifications if they have a subscribed browser - if (notifiedUser.pushNotifSubscriptions.length > 0) { - for (subbed of notifiedUser.pushNotifSubscriptions) { - const pushSubscription = JSON.parse(subbed); - const options = { - gcmAPIKey: '' - }; - const payload = JSON.stringify({ - body: response.text.replace(/<(\/)?strong>/g, ''), - imageURL: response.image.replace('.svg', '.png'), //we can't use svgs here, which cake.svg (the default profile image) is, this will use cake.png instead - link: url - }) - webpush.sendNotification(pushSubscription, payload, options).catch(async err => { - console.log("push notification subscription not working, will be removed:") - console.log(err); - notifiedUser.pushNotifSubscriptions = notifiedUser.pushNotifSubscriptions.filter(v => v != subbed); - notifiedUser = await notifiedUser.save(); - }); - } - } - // send the user an email if it's a mention and they have emails for mentions enabled - if (notifiedUser.settings.sendMentionEmails == true && response.emailText) { - emailer.sendSingleNotificationEmail(notifiedUser, response, url); - } - //if the most recent notification is a trust or follow, and the current is also a trust or follow from the same user, combine the two - var lastNotif = notifiedUser.notifications[notifiedUser.notifications.length - 1] - if (lastNotif && cause == "relationship" && lastNotif.category == 'relationship' && lastNotif.url == url && + } + console.log('creating notification') + User.findOne({ + _id: notifieeID + }) + .then(notifiedUser => { + buildNotification() + .then(async response => { + // send the user push notifications if they have a subscribed browser + if (notifiedUser.pushNotifSubscriptions.length > 0) { + for (subbed of notifiedUser.pushNotifSubscriptions) { + const pushSubscription = JSON.parse(subbed) + const options = { + gcmAPIKey: '' + } + const payload = JSON.stringify({ + body: response.text.replace(/<(\/)?strong>/g, ''), + imageURL: response.image.replace('.svg', '.png'), // we can't use svgs here, which cake.svg (the default profile image) is, this will use cake.png instead + link: url + }) + webpush.sendNotification(pushSubscription, payload, options).catch(async err => { + console.log('push notification subscription not working, will be removed:') + console.log(err) + notifiedUser.pushNotifSubscriptions = notifiedUser.pushNotifSubscriptions.filter(v => v != subbed) + notifiedUser = await notifiedUser.save() + }) + } + } + // send the user an email if it's a mention and they have emails for mentions enabled + if (notifiedUser.settings.sendMentionEmails == true && response.emailText) { + emailer.sendSingleNotificationEmail(notifiedUser, response, url) + } + // if the most recent notification is a trust or follow, and the current is also a trust or follow from the same user, combine the two + var lastNotif = notifiedUser.notifications[notifiedUser.notifications.length - 1] + if (lastNotif && cause == 'relationship' && lastNotif.category == 'relationship' && lastNotif.url == url && ((lastNotif.text.includes('follows you') && context == 'trust') || (lastNotif.text.includes('trusts you') && context == 'follow'))) { - // It's too late at night to work out a better way to get the username of the new notification that isn't regexing the old notification, so... this will break at some point - var username = lastNotif.text.match(/@[A-Za-z0-9-_]*/); - console.log(username) - var notification = { - category: cause, - sourceId: sourceId, - subjectId: subjectId, - text: '' + username + ' ' + 'now follows and trusts you.', - image: response.image, - url: url - } - notifiedUser.notifications[notifiedUser.notifications.length - 1] = notification; - notifiedUser.save().then(() => { console.log('notification sent to ' + notifiedUser.username) }) - } else { - var notification = { - category: cause, - sourceId: sourceId, - subjectId: subjectId, - text: response.text, - image: response.image, - url: url - } - notifiedUser.notifications.push(notification); - notifiedUser.notifications = notifiedUser.notifications.slice(Math.max(0, notifiedUser.notifications.length - 60)); - notifiedUser.save().then(() => { console.log("notification sent to " + notifiedUser.username); }); - } - }) - }) - .catch(error => { - console.error("could not send notification! type: "+type+" cause: "+cause+" context: "+context); - console.error(error); + // It's too late at night to work out a better way to get the username of the new notification that isn't regexing the old notification, so... this will break at some point + var username = lastNotif.text.match(/@[A-Za-z0-9-_]*/) + console.log(username) + var notification = { + category: cause, + sourceId: sourceId, + subjectId: subjectId, + text: '' + username + ' ' + 'now follows and trusts you.', + image: response.image, + url: url + } + notifiedUser.notifications[notifiedUser.notifications.length - 1] = notification + notifiedUser.save().then(() => { console.log('notification sent to ' + notifiedUser.username) }) + } else { + var notification = { + category: cause, + sourceId: sourceId, + subjectId: subjectId, + text: response.text, + image: response.image, + url: url + } + notifiedUser.notifications.push(notification) + notifiedUser.notifications = notifiedUser.notifications.slice(Math.max(0, notifiedUser.notifications.length - 60)) + notifiedUser.save().then(() => { console.log('notification sent to ' + notifiedUser.username) }) + } }) + }) + .catch(error => { + console.error('could not send notification! type: ' + type + ' cause: ' + cause + ' context: ' + context) + console.error(error) + }) } -module.exports.markRead = markRead; -module.exports.notify = notify; +module.exports.markRead = markRead +module.exports.notify = notify diff --git a/app/personalAccountActions.js b/app/personalAccountActions.js index 48586d0f..dd85955b 100644 --- a/app/personalAccountActions.js +++ b/app/personalAccountActions.js @@ -2,770 +2,760 @@ const reservedUsernames = require('../config/reserved-usernames.js') // APIs -var apiConfig = require('../config/apis.js'); - -const sgMail = require('@sendgrid/mail'); -sgMail.setApiKey(apiConfig.sendgrid); +const sgMail = require('./mail') module.exports = function (app, passport) { - - //Responds to a post request containing signup information. - //Inputs: the request body, containing an email, a username, and a password. - //Outputs: either a redirect back to the signup html page with an error message in the sessionflash if some of the info is invalid; - //or a redirect to login if with a message telling you you've been sent a confirmation email otherwise. also passport is called to send the email for some reason - app.post('/signup', function (req, res) { - - if (reservedUsernames.includes(req.body.username)) { - req.session.sessionFlash = { - type: 'alert', - message: "This username is unavailable.", - username: req.body.username, - email: req.body.email - } - res.redirect(301, '/signup'); + // Responds to a post request containing signup information. + // Inputs: the request body, containing an email, a username, and a password. + // Outputs: either a redirect back to the signup html page with an error message in the sessionflash if some of the info is invalid; + // or a redirect to login if with a message telling you you've been sent a confirmation email otherwise. also passport is called to send the email for some reason + app.post('/signup', function (req, res) { + if (reservedUsernames.includes(req.body.username)) { + req.session.sessionFlash = { + type: 'alert', + message: 'This username is unavailable.', + username: req.body.username, + email: req.body.email + } + res.redirect(301, '/signup') + } else { + req.checkBody('email', 'Please enter a valid email.').isEmail().isLength({ + max: 80 + }) + req.checkBody('username', 'Username can contain only lowercase letters, numbers, - (dash) and _ (underscore).').matches(/^[a-z0-9-_]+$/) + req.checkBody('username', 'Username must be between 2 and 20 characters long.').isLength({ + min: 2, + max: 20 + }) + req.checkBody('password', 'Password must be at least 8 characters long.').isLength({ + min: 8 + }) + req.checkBody('password', 'Passwords do not match.').equals(req.body.passwordrepeat) + + req.getValidationResult().then(function (result) { + if (!result.isEmpty()) { + var errors = result.array().map(function (elem) { + return elem.msg + }).join('
') + req.session.sessionFlash = { + type: 'alert', + message: errors, + username: req.body.username, + email: req.body.email + } + res.redirect(301, '/signup') } else { - req.checkBody('email', 'Please enter a valid email.').isEmail().isLength({ - max: 80 - }); - req.checkBody('username', 'Username can contain only lowercase letters, numbers, - (dash) and _ (underscore).').matches(/^[a-z0-9-_]+$/); - req.checkBody('username', 'Username must be between 2 and 20 characters long.').isLength({ - min: 2, - max: 20 - }) - req.checkBody('password', 'Password must be at least 8 characters long.').isLength({ - min: 8 - }); - req.checkBody('password', 'Passwords do not match.').equals(req.body.passwordrepeat) - - req.getValidationResult().then(function (result) { - if (!result.isEmpty()) { - var errors = result.array().map(function (elem) { - return elem.msg - }).join("
"); - req.session.sessionFlash = { - type: 'alert', - message: errors, - username: req.body.username, - email: req.body.email - } - res.redirect(301, '/signup'); - } else { - req.session.sessionFlash = { - message: 'An email has been sent to ' + req.body.email + ' with further instructions. Please check your spam or junk folder if it does not arrive in the next few minutes.' - } - passport.authenticate('signup', { - successRedirect: '/login', - failureRedirect: '/signup', - failureFlash: true - })(req, res); - } - }); + req.session.sessionFlash = { + message: 'An email has been sent to ' + req.body.email + ' with further instructions. Please check your spam or junk folder if it does not arrive in the next few minutes.' + } + passport.authenticate('signup', { + successRedirect: '/login', + failureRedirect: '/signup', + failureFlash: true + })(req, res) } - }); - - - //Responds to post requests containing information theoretically used to log a user in. - //Input: info from the request body: the email field and the password field. the email field can actually also contain the user's username and - //work just as well. - //Output: either a redirect back to the login html page with a sessionflash message "Please check you email and password" (which shows up twice in fact if - //you didn't enter an email) or you are successfully logged in by passport. - app.post('/login', function (req, res) { - req.checkBody('email', 'Please enter a valid email.').isEmail(); - req.checkBody('email', 'Please fill in your email.').notEmpty(); - req.checkBody('password', 'Please fill in your password.').notEmpty(); - - req.getValidationResult().then(function (result) { - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1. - res.setHeader("Pragma", "no-cache"); // HTTP 1.0. - res.setHeader("Expires", "0"); // Proxies. - if (!result.isEmpty()) { - var errors = result.array().map(function (elem) { - return elem.msg - }).join("
"); - req.session.sessionFlash = { - type: 'alert', - message: errors, - email: req.body.email - } - res.redirect(301, '/login'); - } else { - passport.authenticate('login', { - successRedirect: '/home', - failureRedirect: '/login', - failureFlash: true, - })(req, res); - } - }); - }); - - - //Responds to get requests for /logout. - //Input: none - //Output: user is logged out and redirected to the referring page or / if no referrer. - app.get('/logout', function (req, res) { - req.logout(); - req.session.destroy(); - res.redirect('back'); - }); - - //Responds to post requests that create relationships between users. - //Input: the parameters in the url. type can be either follow, flag, or trust; action can either be add (follow/flag/trust) or delete (unfollow/unflag/untrust); - //from and fromid are both the id of the account taking the action (?), same with to and toid (??) and from username is the username of the account - //taking the action. - //Output: a relationship document in the database or the removal of one, depending on type, and a notification if someone has followed someone. Or - //isLoggedInOrRedirect redirects you. - app.post("/useraction/:type/:action/:from/:to/:fromid/:toid/:fromusername", isLoggedInOrRedirect, function (req, res) { - if (req.params.from != req.user._id.toString()) { - res.status(400).send("action not permitted: following/unfollowing/flagging/unflagging/trusting/untrusting a user from an account you're not logged in to"); - return; + }) + } + }) + + // Responds to post requests containing information theoretically used to log a user in. + // Input: info from the request body: the email field and the password field. the email field can actually also contain the user's username and + // work just as well. + // Output: either a redirect back to the login html page with a sessionflash message "Please check you email and password" (which shows up twice in fact if + // you didn't enter an email) or you are successfully logged in by passport. + app.post('/login', function (req, res) { + req.checkBody('email', 'Please enter a valid email.').isEmail() + req.checkBody('email', 'Please fill in your email.').notEmpty() + req.checkBody('password', 'Please fill in your password.').notEmpty() + + req.getValidationResult().then(function (result) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') // HTTP 1.1. + res.setHeader('Pragma', 'no-cache') // HTTP 1.0. + res.setHeader('Expires', '0') // Proxies. + if (!result.isEmpty()) { + var errors = result.array().map(function (elem) { + return elem.msg + }).join('
') + req.session.sessionFlash = { + type: 'alert', + message: errors, + email: req.body.email } + res.redirect(301, '/login') + } else { + passport.authenticate('login', { + successRedirect: '/home', + failureRedirect: '/login', + failureFlash: true + })(req, res) + } + }) + }) + + // Responds to get requests for /logout. + // Input: none + // Output: user is logged out and redirected to the referring page or / if no referrer. + app.get('/logout', function (req, res) { + req.logout() + req.session.destroy() + res.redirect('back') + }) + + // Responds to post requests that create relationships between users. + // Input: the parameters in the url. type can be either follow, flag, or trust; action can either be add (follow/flag/trust) or delete (unfollow/unflag/untrust); + // from and fromid are both the id of the account taking the action (?), same with to and toid (??) and from username is the username of the account + // taking the action. + // Output: a relationship document in the database or the removal of one, depending on type, and a notification if someone has followed someone. Or + // isLoggedInOrRedirect redirects you. + app.post('/useraction/:type/:action/:from/:to/:fromid/:toid/:fromusername', isLoggedInOrRedirect, function (req, res) { + if (req.params.from != req.user._id.toString()) { + res.status(400).send("action not permitted: following/unfollowing/flagging/unflagging/trusting/untrusting a user from an account you're not logged in to") + return + } + User.findOne({ + _id: req.params.from + }) + .then(fromUser => { + from = fromUser + console.log(from) + }) + .then(() => { User.findOne({ - _id: req.params.from - }) - .then(fromUser => { - from = fromUser; - console.log(from) - }) - .then(() => { - User.findOne({ - _id: req.params.to - }) - .then(toUser => { - to = toUser; - console.log(to) - }) - .then(() => { - if (req.params.action == "add") { - const flag = new Relationship({ - from: from.email, - to: to.email, - fromUser: req.params.from, - toUser: req.params.to, - value: req.params.type - }); - flag.save() - .then(() => { - // Do not notify when users are flagged, muted, or blocked (blocking and muting not currently implemented) - if (req.params.type != 'block' && req.params.type != 'flag' && req.params.type != 'mute') { - notifier.notify('user', 'relationship', to._id, from._id, from._id, '/' + from.username, req.params.type) - } - }) - .then(() => { - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - .catch((err) => { - console.log("Database error.") - console.log(err) - }); - } else if (req.params.action = "delete") { - Relationship.findOneAndRemove({ - from: from.email, - to: to.email, - fromUser: from._id, - toUser: to._id, - value: req.params.type - }).then(() => { - res.end('{"success" : "Updated Successfully", "status" : 200}'); - }) - .catch((err) => { - console.log("Database error.") - }); - } - }) - }) - }); - - //Respond to post requests that update the logged in user's display name, bio, location, website, and profile picture. - //Inputs: all the fields listed above, in req.params. new profile pictures arrive as raw image data, not a url (like in createpost.) - //Outputs: if the user has uploaded a new image, that image is saved; all the fields in the user's document are updated. - app.post("/updateprofile", isLoggedInOrRedirect, function (req, res) { - let imageEnabled = req.user.imageEnabled; - let imageFilename = req.user.image; - if (Object.keys(req.files).length != 0) { - console.log(req.files.imageUpload.data.length) - if (req.files.imageUpload.data.length > 3145728) { - req.session.sessionFlash = { - type: 'alert', - message: 'File too large. The file size limit is 3MB.' - } - return res.redirect('back'); - } else { - imageEnabled = true; - let eventImageBuffer = req.files.imageUpload.data; - sharp(eventImageBuffer) - .resize({ - width: 600, - height: 600 - }) - .jpeg({ - quality: 85 - }) - .toFile('./public/images/' + req.user._id + '.jpg') - .catch(err => { - console.error(err); - }); - imageFilename = req.user._id + '.jpg'; + _id: req.params.to + }) + .then(toUser => { + to = toUser + console.log(to) + }) + .then(() => { + if (req.params.action == 'add') { + const flag = new Relationship({ + from: from.email, + to: to.email, + fromUser: req.params.from, + toUser: req.params.to, + value: req.params.type + }) + flag.save() + .then(() => { + // Do not notify when users are flagged, muted, or blocked (blocking and muting not currently implemented) + if (req.params.type != 'block' && req.params.type != 'flag' && req.params.type != 'mute') { + notifier.notify('user', 'relationship', to._id, from._id, from._id, '/' + from.username, req.params.type) + } + }) + .then(() => { + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + .catch((err) => { + console.log('Database error.') + console.log(err) + }) + } else if (req.params.action = 'delete') { + Relationship.findOneAndRemove({ + from: from.email, + to: to.email, + fromUser: from._id, + toUser: to._id, + value: req.params.type + }).then(() => { + res.end('{"success" : "Updated Successfully", "status" : 200}') + }) + .catch((err) => { + console.log('Database error.') + }) } + }) + }) + }) + + // Respond to post requests that update the logged in user's display name, bio, location, website, and profile picture. + // Inputs: all the fields listed above, in req.params. new profile pictures arrive as raw image data, not a url (like in createpost.) + // Outputs: if the user has uploaded a new image, that image is saved; all the fields in the user's document are updated. + app.post('/updateprofile', isLoggedInOrRedirect, function (req, res) { + let imageEnabled = req.user.imageEnabled + let imageFilename = req.user.image + if (Object.keys(req.files).length != 0) { + console.log(req.files.imageUpload.data.length) + if (req.files.imageUpload.data.length > 3145728) { + req.session.sessionFlash = { + type: 'alert', + message: 'File too large. The file size limit is 3MB.' } - User.findOne({ - _id: req.user._id - }) - .then((user) => { - let parsedAbout = req.user.aboutParsed; - if (req.body.about != req.user.aboutRaw) { - // Parse about section - let splitAbout = helper.escapeHTMLChars(req.body.about).substring(0, 500).split(/\r\n|\r|\n/gi); - let parsedAboutArray = []; - splitAbout.forEach(function (line) { - if (line != "") { - line = "

" + line + "

"; - line = Autolinker.link(line); - var mentionRegex = /(^|[^@\w])@([\w-]{1,30})[\b-]*/g - var mentionReplace = '$1@$2'; - var hashtagRegex = /(^|>|\n|\ |\t)#(\w{1,60})\b/g - var hashtagReplace = '$1#$2'; - line = line.replace(mentionRegex, mentionReplace).replace(hashtagRegex, hashtagReplace); - parsedAboutArray.push(line); - } - }) - parsedAbout = parsedAboutArray.join(''); - } - //the assumption is that when handlebars displays these, it'll automatically escape the html so that the browser doesn't render it, so there's no - //need to do that here to the input except with the parsed stuff above and the website link, which are going to be displayed with html intact at some point - user.displayName = req.body.displayName.substring(0, 50); - user.pronouns = req.body.pronouns.substring(0, 50); - user.aboutRaw = req.body.about.substring(0, 500); - user.aboutParsed = parsedAbout; - user.location = req.body.location.substring(0, 50); - user.websiteRaw = req.body.website.substring(0, 50); - user.websiteParsed = Autolinker.link(helper.escapeHTMLChars(req.body.website.substring(0, 50))); - user.image = imageFilename; - user.imageEnabled = imageEnabled; - user.save().then(() => { - res.redirect('back'); - }) - .catch((err) => { - console.log("Database error: " + err) - }); - }) - }); - - - //Responds to post requests from users who do not want notifications from activity on some post anymore. - //Inputs: the id of the post - //Outputs: removes the logged in user from the post's subscribedusers field, adds them to unsubscribedUsers - app.post('/api/post/unsubscribe/:postid', isLoggedInOrRedirect, async function (req, res) { - Post.findOne({ - _id: req.params.postid - }) - .then(async post => { - if (post.type == "boost") { - post = await Post.findById(post.boostTarget); - } - post.subscribedUsers.pull(req.user._id) - post.unsubscribedUsers.push(req.user._id) - post.save() - .then(response => { - res.sendStatus(200); - }) - .catch(error => { - console.error(error); - }) - }) + return res.redirect('back') + } else { + imageEnabled = true + const eventImageBuffer = req.files.imageUpload.data + sharp(eventImageBuffer) + .resize({ + width: 600, + height: 600 + }) + .jpeg({ + quality: 85 + }) + .toFile('./public/images/' + req.user._id + '.jpg') + .catch(err => { + console.error(err) + }) + imageFilename = req.user._id + '.jpg' + } + } + User.findOne({ + _id: req.user._id }) - - //Well, it's a bit like the last one but in reverse - app.post('/api/post/subscribe/:postid', isLoggedInOrRedirect, async function (req, res) { - Post.findOne({ - _id: req.params.postid - }) - .then(async post => { - if (post.type == "boost") { - post = await Post.findById(post.boostTarget); - } - post.unsubscribedUsers.pull(req.user._id) - post.subscribedUsers.push(req.user._id) - post.save() - .then(response => { - res.sendStatus(200); - }) - .catch(error => { - console.error(error); - }) - }) + .then((user) => { + let parsedAbout = req.user.aboutParsed + if (req.body.about != req.user.aboutRaw) { + // Parse about section + const splitAbout = helper.escapeHTMLChars(req.body.about).substring(0, 500).split(/\r\n|\r|\n/gi) + const parsedAboutArray = [] + splitAbout.forEach(function (line) { + if (line != '') { + line = '

' + line + '

' + line = Autolinker.link(line) + var mentionRegex = /(^|[^@\w])@([\w-]{1,30})[\b-]*/g + var mentionReplace = '$1@$2' + var hashtagRegex = /(^|>|\n|\ |\t)#(\w{1,60})\b/g + var hashtagReplace = '$1#$2' + line = line.replace(mentionRegex, mentionReplace).replace(hashtagRegex, hashtagReplace) + parsedAboutArray.push(line) + } + }) + parsedAbout = parsedAboutArray.join('') + } + // the assumption is that when handlebars displays these, it'll automatically escape the html so that the browser doesn't render it, so there's no + // need to do that here to the input except with the parsed stuff above and the website link, which are going to be displayed with html intact at some point + user.displayName = req.body.displayName.substring(0, 50) + user.pronouns = req.body.pronouns.substring(0, 50) + user.aboutRaw = req.body.about.substring(0, 500) + user.aboutParsed = parsedAbout + user.location = req.body.location.substring(0, 50) + user.websiteRaw = req.body.website.substring(0, 50) + user.websiteParsed = Autolinker.link(helper.escapeHTMLChars(req.body.website.substring(0, 50))) + user.image = imageFilename + user.imageEnabled = imageEnabled + user.save().then(() => { + res.redirect('back') + }) + .catch((err) => { + console.log('Database error: ' + err) + }) + }) + }) + + // Responds to post requests from users who do not want notifications from activity on some post anymore. + // Inputs: the id of the post + // Outputs: removes the logged in user from the post's subscribedusers field, adds them to unsubscribedUsers + app.post('/api/post/unsubscribe/:postid', isLoggedInOrRedirect, async function (req, res) { + Post.findOne({ + _id: req.params.postid }) - - //Responds to post request that's updating settings - //Input: the settings - //Output: settings are saved for the user in the database, we're redirected back to the user's page. - //database error will do... something? again, all unless isLoggedInOrRedirect redirects you first. - app.post('/updatesettings', isLoggedInOrRedirect, async function (req, res) { - let newSets = req.body; - console.log(newSets) - let oldSets = req.user.settings; - - var emailSetsChanged = false; - if (newSets.digestEmailFrequency != oldSets.digestEmailFrequency || newSets.timezone != oldSets.timezone || newSets.autoDetectedTimeZone != oldSets.autoDetectedTimeZone || newSets.emailTime != oldSets.emailTime || newSets.emailDay != oldSets.emailDay) { - emailSetsChanged = true; + .then(async post => { + if (post.type == 'boost') { + post = await Post.findById(post.boostTarget) } - - User.update({ - _id: req.user._id - }, { - $set: { - 'settings.timezone': newSets.timezone, - 'settings.autoDetectedTimeZone': newSets.autoDetectedTimeZone ? newSets.autoDetectedTimeZone : oldSets.autoDetectedTimeZone, - 'settings.profileVisibility': newSets.profileVisibility, - 'settings.newPostPrivacy': newSets.newPostPrivacy, - 'settings.digestEmailFrequency': newSets.digestEmailFrequency, - 'settings.imageQuality': newSets.imageQuality, - 'settings.homeTagTimelineSorting': newSets.homeTagTimelineSorting, - 'settings.userTimelineSorting': newSets.userTimelineSorting, - 'settings.communityTimelineSorting': newSets.communityTimelineSorting, - 'settings.flashRecentComments': (newSets.flashRecentComments == 'on' ? true : false), - 'settings.showRecommendations': (newSets.showRecommendations == 'on' ? true : false), - 'settings.showHashtags': (newSets.showHashtags == 'on' ? true : false), - 'settings.sendMentionEmails': (newSets.sendMentionEmails == 'on' ? true : false), - 'settings.emailTime': newSets.emailTime, - 'settings.emailDay': newSets.emailDay - } - }) - .then(async (updateStatus) => { - if (emailSetsChanged) { - emailer.emailRescheduler((await User.findById(req.user._id))); //can't use req.user bc that will still store the old settings - } - res.redirect('/' + req.user.username) - }) - .catch(error => { - console.log("Error updating settings!") - console.log(error) - }) + post.subscribedUsers.pull(req.user._id) + post.unsubscribedUsers.push(req.user._id) + post.save() + .then(response => { + res.sendStatus(200) + }) + .catch(error => { + console.error(error) + }) + }) + }) + + // Well, it's a bit like the last one but in reverse + app.post('/api/post/subscribe/:postid', isLoggedInOrRedirect, async function (req, res) { + Post.findOne({ + _id: req.params.postid }) + .then(async post => { + if (post.type == 'boost') { + post = await Post.findById(post.boostTarget) + } + post.unsubscribedUsers.pull(req.user._id) + post.subscribedUsers.push(req.user._id) + post.save() + .then(response => { + res.sendStatus(200) + }) + .catch(error => { + console.error(error) + }) + }) + }) + + // Responds to post request that's updating settings + // Input: the settings + // Output: settings are saved for the user in the database, we're redirected back to the user's page. + // database error will do... something? again, all unless isLoggedInOrRedirect redirects you first. + app.post('/updatesettings', isLoggedInOrRedirect, async function (req, res) { + const newSets = req.body + console.log(newSets) + const oldSets = req.user.settings + + var emailSetsChanged = false + if (newSets.digestEmailFrequency != oldSets.digestEmailFrequency || newSets.timezone != oldSets.timezone || newSets.autoDetectedTimeZone != oldSets.autoDetectedTimeZone || newSets.emailTime != oldSets.emailTime || newSets.emailDay != oldSets.emailDay) { + emailSetsChanged = true + } - app.post('/api/notifications/clearall', isLoggedInOrRedirect, function (req, res) { - User.findOne({ - _id: req.user._id - }, 'notifications') - .then(user => { - user.notifications.forEach(notification => { - notification.seen = true; - }) - user.save() - .then(result => { - if (result) { - res.sendStatus(200); - } - }) - }) + User.update({ + _id: req.user._id + }, { + $set: { + 'settings.timezone': newSets.timezone, + 'settings.autoDetectedTimeZone': newSets.autoDetectedTimeZone ? newSets.autoDetectedTimeZone : oldSets.autoDetectedTimeZone, + 'settings.profileVisibility': newSets.profileVisibility, + 'settings.newPostPrivacy': newSets.newPostPrivacy, + 'settings.digestEmailFrequency': newSets.digestEmailFrequency, + 'settings.imageQuality': newSets.imageQuality, + 'settings.homeTagTimelineSorting': newSets.homeTagTimelineSorting, + 'settings.userTimelineSorting': newSets.userTimelineSorting, + 'settings.communityTimelineSorting': newSets.communityTimelineSorting, + 'settings.flashRecentComments': (newSets.flashRecentComments == 'on'), + 'settings.showRecommendations': (newSets.showRecommendations == 'on'), + 'settings.showHashtags': (newSets.showHashtags == 'on'), + 'settings.sendMentionEmails': (newSets.sendMentionEmails == 'on'), + 'settings.emailTime': newSets.emailTime, + 'settings.emailDay': newSets.emailDay + } }) - - //Responds to get requests for email verification that don't have the verification token included. Deprecated? When would this happen - //Input: none - //Output: redirect to /login with a 301 code and "No token provided" in the flash message. - app.get('/verify-email', function (req, res) { - req.session.sessionFlash = { - type: 'alert', - message: "No token provided." + .then(async (updateStatus) => { + if (emailSetsChanged) { + emailer.emailRescheduler((await User.findById(req.user._id))) // can't use req.user bc that will still store the old settings } - res.redirect(301, '/login'); - }); - - //Responds to get requests for email verification that include the verification token. - //Input: the token - //Output: rendering of verify-email with the token as a hidden input on the page and the email autofilled from sessionFlash. - app.get('/verify-email/:verificationtoken', function (req, res) { - res.render('verify-email', { - layout: 'logged-out', - sessionFlash: res.locals.sessionFlash, - token: req.params.verificationtoken - }); + res.redirect('/' + req.user.username) + }) + .catch(error => { + console.log('Error updating settings!') + console.log(error) + }) + }) + + app.post('/api/notifications/clearall', isLoggedInOrRedirect, function (req, res) { + User.findOne({ + _id: req.user._id + }, 'notifications') + .then(user => { + user.notifications.forEach(notification => { + notification.seen = true + }) + user.save() + .then(result => { + if (result) { + res.sendStatus(200) + } + }) + }) + }) + + // Responds to get requests for email verification that don't have the verification token included. Deprecated? When would this happen + // Input: none + // Output: redirect to /login with a 301 code and "No token provided" in the flash message. + app.get('/verify-email', function (req, res) { + req.session.sessionFlash = { + type: 'alert', + message: 'No token provided.' + } + res.redirect(301, '/login') + }) + + // Responds to get requests for email verification that include the verification token. + // Input: the token + // Output: rendering of verify-email with the token as a hidden input on the page and the email autofilled from sessionFlash. + app.get('/verify-email/:verificationtoken', function (req, res) { + res.render('verify-email', { + layout: 'logged-out', + sessionFlash: res.locals.sessionFlash, + token: req.params.verificationtoken }) - - //Responds to post requests for email verification. - //Input: the email address and the verification token - //Output: A redirect to /verify-email/... if the email is wrong or there's an error saving the user, - //redirect to /resend-token if the token is wrong or if there is a database error finding the user, or a - //redirect to /login and the user's isVerified property being set to true if everything's right. - app.post('/verify-email', function (req, res) { - req.checkBody('email', 'Please enter a valid email.').isEmail().isLength({ - max: 80 - }); - req.getValidationResult().then(function (result) { - if (!result.isEmpty()) { - console.log("Not an email") - var errors = result.array().map(function (elem) { - return elem.msg - }).join("
"); - req.session.sessionFlash = { - type: 'alert', - message: errors, - email: req.body.email - } - res.redirect(301, '/verify-email/' + req.body.verificationToken); + }) + + // Responds to post requests for email verification. + // Input: the email address and the verification token + // Output: A redirect to /verify-email/... if the email is wrong or there's an error saving the user, + // redirect to /resend-token if the token is wrong or if there is a database error finding the user, or a + // redirect to /login and the user's isVerified property being set to true if everything's right. + app.post('/verify-email', function (req, res) { + req.checkBody('email', 'Please enter a valid email.').isEmail().isLength({ + max: 80 + }) + req.getValidationResult().then(function (result) { + if (!result.isEmpty()) { + console.log('Not an email') + var errors = result.array().map(function (elem) { + return elem.msg + }).join('
') + req.session.sessionFlash = { + type: 'alert', + message: errors, + email: req.body.email + } + res.redirect(301, '/verify-email/' + req.body.verificationToken) + } else { + User.findOne({ + email: req.body.email, + verificationToken: req.body.verificationToken, + verificationTokenExpiry: { + $gt: Date.now() + } + }) + .then(user => { + if (!user) { + console.log('No such user') + req.session.sessionFlash = { + type: 'alert', + message: 'Email verification token invalid or has expired. Enter your email below to resend the token.' + } + res.redirect(301, '/resend-token') } else { - User.findOne({ - email: req.body.email, - verificationToken: req.body.verificationToken, - verificationTokenExpiry: { - $gt: Date.now() - } - }) - .then(user => { - if (!user) { - console.log("No such user") - req.session.sessionFlash = { - type: 'alert', - message: "Email verification token invalid or has expired. Enter your email below to resend the token." - } - res.redirect(301, '/resend-token'); - } else { - user.isVerified = true; - user.save() - .then(user => { - req.session.sessionFlash = { - message: "Your email has been verified successfully. Please log in below." - } - res.redirect(301, '/login'); - }) - .catch(err => { - console.log("Cannot save user") - console.error(err) - req.session.sessionFlash = { - type: 'alert', - message: "An error occured while verifying your token. Please try again." - } - res.redirect(301, '/verify-email/' + req.body.verificationToken); - }) - } - }) - .catch(err => { - console.log("Cannot access database") - console.error(err) - req.session.sessionFlash = { - type: 'alert', - message: "Email verification token invalid or has expired. Enter your email below to resend the token." - } - res.redirect(301, '/resend-token'); - }) + user.isVerified = true + user.save() + .then(user => { + req.session.sessionFlash = { + message: 'Your email has been verified successfully. Please log in below.' + } + res.redirect(301, '/login') + }) + .catch(err => { + console.log('Cannot save user') + console.error(err) + req.session.sessionFlash = { + type: 'alert', + message: 'An error occured while verifying your token. Please try again.' + } + res.redirect(301, '/verify-email/' + req.body.verificationToken) + }) } - }); - }); - - //Responds to get requests for /resend-token - //Input: flash message - //Output: renders resend-token with flash message - app.get('/resend-token', function (req, res) { - res.render('resend-token', { - layout: 'logged-out', - sessionFlash: res.locals.sessionFlash - }); - }); - - //Responds to post requests from /resend-token - //Input: email address - //Output: a redirect to /resend-token with a new flash message. - app.post('/resend-token', function (req, res) { - User.findOne({ - email: req.body.email - }) + }) + .catch(err => { + console.log('Cannot access database') + console.error(err) + req.session.sessionFlash = { + type: 'alert', + message: 'Email verification token invalid or has expired. Enter your email below to resend the token.' + } + res.redirect(301, '/resend-token') + }) + } + }) + }) + + // Responds to get requests for /resend-token + // Input: flash message + // Output: renders resend-token with flash message + app.get('/resend-token', function (req, res) { + res.render('resend-token', { + layout: 'logged-out', + sessionFlash: res.locals.sessionFlash + }) + }) + + // Responds to post requests from /resend-token + // Input: email address + // Output: a redirect to /resend-token with a new flash message. + app.post('/resend-token', function (req, res) { + User.findOne({ + email: req.body.email + }) + .then(user => { + if (!user) { // There is no user registered with this email. + req.session.sessionFlash = { + message: 'A new token has been sent to ' + req.body.email + '. Please check your spam or junk folder if it does not arrive in the next few minutes. You may now close this page.', + email: req.body.email + } + res.redirect(301, '/resend-token') + } else if (user.isVerified) { // This email address is aleady verified. + req.session.sessionFlash = { + message: 'A new token has been sent to ' + req.body.email + '. Please check your spam or junk folder if it does not arrive in the next few minutes. You may now close this page.', + email: req.body.email + } + res.redirect(301, '/resend-token') + } else { // Actual success + require('crypto').randomBytes(20, function (err, buffer) { + token = buffer.toString('hex') + }) + user.verificationToken = token + user.verificationTokenExpiry = Date.now() + 3600000 // 1 hour + user.save() .then(user => { - if (!user) { // There is no user registered with this email. - req.session.sessionFlash = { - message: "A new token has been sent to " + req.body.email + ". Please check your spam or junk folder if it does not arrive in the next few minutes. You may now close this page.", - email: req.body.email - } - res.redirect(301, '/resend-token'); - } else if (user.isVerified) { // This email address is aleady verified. - req.session.sessionFlash = { - message: "A new token has been sent to " + req.body.email + ". Please check your spam or junk folder if it does not arrive in the next few minutes. You may now close this page.", - email: req.body.email - } - res.redirect(301, '/resend-token'); - } else { // Actual success - require('crypto').randomBytes(20, function (err, buffer) { - token = buffer.toString('hex'); - }) - user.verificationToken = token; - user.verificationTokenExpiry = Date.now() + 3600000; // 1 hour - user.save() - .then(user => { - const msg = { - to: email, - from: 'support@sweet.sh', - subject: 'sweet new user verification', - text: 'Hi! You are receiving this because you have created a new account on sweet with this email.\n\n' + + const msg = { + to: email, + from: 'support@sweet.sh', + subject: 'sweet new user verification', + text: 'Hi! You are receiving this because you have created a new account on sweet with this email.\n\n' + 'Please click on the following link, or paste it into your browser, to verify your email:\n\n' + 'https://sweet.sh/verify-email/' + token + '\n\n' + 'If you did not create an account on sweet, please ignore and delete this email. The token will expire in an hour.\n' - }; - sgMail.send(msg) - .then(user => { - req.session.sessionFlash = { - message: "A new token has been sent to " + req.body.email + ". Please check your spam or junk folder if it does not arrive in the next few minutes. You may now close this page." - } - res.redirect(301, '/resend-token'); - }) - }) - .catch(err => { - req.session.sessionFlash = { - type: 'alert', - message: "There has been a problem sending your token. Please try again in a few minutes.", - email: req.body.email - } - res.redirect(301, '/resend-token'); - }) - } + } + sgMail.send(msg) + .then(() => { + req.session.sessionFlash = { + message: 'A new token has been sent to ' + req.body.email + '. Please check your spam or junk folder if it does not arrive in the next few minutes. You may now close this page.' + } + res.redirect(301, '/resend-token') + }) }) .catch(err => { - req.session.sessionFlash = { - type: 'alert', - message: "There has been a problem sending your token. Please try again in a few minutes.", - email: req.body.email - } - res.redirect(301, '/resend-token'); - }) - }); - - //Responds to get requests for /forgot-password without a password reset token - //Input: flash message - //Output: renders forgot-password with flash message - app.get('/forgot-password', function (req, res) { - res.render('forgot-password', { - layout: 'logged-out', - sessionFlash: res.locals.sessionFlash - }); - }); - - //Responds to get requests for /forgot-password with a password reset token - //Input: password reset token - //Output: a redirect to /forgot-password with flash message if the token is wrong, - //a redirect to reset-password if the token is right - app.get('/reset-password/:token', function (req, res) { - User.findOne({ - passwordResetToken: req.params.token, - passwordResetTokenExpiry: { - $gt: Date.now() - } - }) - .then(user => { - if (!user) { - console.log("Wrong token on GET") - req.session.sessionFlash = { - type: 'alert', - message: "Password reset token is invalid or has expired." - } - res.redirect(301, '/forgot-password'); - } else { - console.log("Correct token") - res.render('reset-password', { - layout: 'logged-out', - sessionFlash: res.locals.sessionFlash, - resetToken: req.params.token - }); - } - }) - }); - - //Responds to post requests from /forgot-password by sending a password reset email with a token - //Input: the user's email - //Ouput: just a redirect to /forgot-password with an incorrect flash message if the email is wrong, - //an email with a link to /reset-password with a token if the email is right and the token saved in - //the database, or a redirect to /forgot-password with a warning message if the email or database apis return errors. - app.post('/forgot-password', function (req, res) { - require('crypto').randomBytes(20, function (err, buffer) { - token = buffer.toString('hex'); - }) - User.findOne({ + req.session.sessionFlash = { + type: 'alert', + message: 'There has been a problem sending your token. Please try again in a few minutes.', email: req.body.email + } + res.redirect(301, '/resend-token') }) - .then(user => { - if (!user) { // No account with this email address exists. - req.session.sessionFlash = { - type: 'alert', - message: 'An email has been sent to ' + req.body.email + ' with further instructions. Please check your spam or junk folder if it does not arrive in the next few minutes.', - email: req.body.email - } - res.redirect(301, '/forgot-password'); - } - user.passwordResetToken = token; - user.passwordResetTokenExpiry = Date.now() + 3600000; // 1 hour - user.save() - .then(user => { - const msg = { - to: req.body.email, - from: 'support@sweet.sh', - subject: 'sweet password reset request', - text: 'Hi! You are receiving this because someone has requested a reset of the password for your sweet account.\n\n' + + } + }) + .catch(err => { + req.session.sessionFlash = { + type: 'alert', + message: 'There has been a problem sending your token. Please try again in a few minutes.', + email: req.body.email + } + res.redirect(301, '/resend-token') + }) + }) + + // Responds to get requests for /forgot-password without a password reset token + // Input: flash message + // Output: renders forgot-password with flash message + app.get('/forgot-password', function (req, res) { + res.render('forgot-password', { + layout: 'logged-out', + sessionFlash: res.locals.sessionFlash + }) + }) + + // Responds to get requests for /forgot-password with a password reset token + // Input: password reset token + // Output: a redirect to /forgot-password with flash message if the token is wrong, + // a redirect to reset-password if the token is right + app.get('/reset-password/:token', function (req, res) { + User.findOne({ + passwordResetToken: req.params.token, + passwordResetTokenExpiry: { + $gt: Date.now() + } + }) + .then(user => { + if (!user) { + console.log('Wrong token on GET') + req.session.sessionFlash = { + type: 'alert', + message: 'Password reset token is invalid or has expired.' + } + res.redirect(301, '/forgot-password') + } else { + console.log('Correct token') + res.render('reset-password', { + layout: 'logged-out', + sessionFlash: res.locals.sessionFlash, + resetToken: req.params.token + }) + } + }) + }) + + // Responds to post requests from /forgot-password by sending a password reset email with a token + // Input: the user's email + // Ouput: just a redirect to /forgot-password with an incorrect flash message if the email is wrong, + // an email with a link to /reset-password with a token if the email is right and the token saved in + // the database, or a redirect to /forgot-password with a warning message if the email or database apis return errors. + app.post('/forgot-password', function (req, res) { + require('crypto').randomBytes(20, function (err, buffer) { + token = buffer.toString('hex') + }) + User.findOne({ + email: req.body.email + }) + .then(user => { + if (!user) { // No account with this email address exists. + req.session.sessionFlash = { + type: 'alert', + message: 'An email has been sent to ' + req.body.email + ' with further instructions. Please check your spam or junk folder if it does not arrive in the next few minutes.', + email: req.body.email + } + res.redirect(301, '/forgot-password') + } + user.passwordResetToken = token + user.passwordResetTokenExpiry = Date.now() + 3600000 // 1 hour + user.save() + .then(user => { + const msg = { + to: req.body.email, + from: 'support@sweet.sh', + subject: 'sweet password reset request', + text: 'Hi! You are receiving this because someone has requested a reset of the password for your sweet account.\n\n' + 'Please click on the following link, or paste it into your browser, to reset your password:\n\n' + 'https://sweet.sh/reset-password/' + token + '\n\n' + 'If you did not request this email, please ignore this email and your password will remain unchanged. The password reset will expire in 1 hour.\n' - }; - sgMail.send(msg) - .then(user => { - req.session.sessionFlash = { - message: 'An email has been sent to ' + req.body.email + ' with further instructions. Please check your spam or junk folder if it does not arrive in the next few minutes.' - } - res.redirect(301, '/forgot-password'); - }) - .catch(error => { - req.session.sessionFlash = { - type: 'alert', - message: "There has been a problem sending your recovery email. Please try again in a few minutes.", - email: req.body.email - } - res.redirect(301, '/forgot-password'); - console.error(error.toString()); - }) - }) - .catch(error => { - req.session.sessionFlash = { - type: 'alert', - message: "There has been a problem sending your recovery email. Please try again in a few minutes.", - email: req.body.email - } - res.redirect(301, '/forgot-password'); - console.error(error.toString()); - }) - }); - }); - - //Responds to post requests with new passwords from the passport reset page. - //Input: the new password, the reset token, the username, and the email - //Output: a redirect to /forgot-password with a flash message if the token is wrong, a redirect to /reset-password - //with the token and a warning message if the new password is invalid, a new password for the user and an email - //telling the user they've reset the passport and a redirect to /login if everything is right, or a redirect to /reset-password - //with the token if there was a database error saving the user's new password. - app.post('/reset-password/:token', function (req, res) { - console.log(req.params.token) - User.findOne({ - passwordResetToken: req.params.token, - passwordResetTokenExpiry: { - $gt: Date.now() + } + sgMail.send(msg) + .then(() => { + req.session.sessionFlash = { + message: 'An email has been sent to ' + req.body.email + ' with further instructions. Please check your spam or junk folder if it does not arrive in the next few minutes.' } - }) - .then(user => { - console.log(user) - if (!user) { - console.log("Wrong token (apparently)") - req.session.sessionFlash = { - type: 'alert', - message: "Password reset token is invalid or has expired." - } - res.redirect(301, '/forgot-password'); - } else { - console.log("Checking body") - req.checkBody('password', 'Password must be at least 8 characters long.').isLength({ - min: 8 - }); - req.checkBody('password', 'Passwords do not match.').equals(req.body.passwordrepeat) - req.getValidationResult().then(function (result) { - if (!result.isEmpty()) { - var errors = result.array().map(function (elem) { - return elem.msg - }).join("
"); - req.session.sessionFlash = { - type: 'alert', - message: errors, - username: req.body.username, - email: req.body.email - } - res.redirect(301, '/reset-password/' + req.params.token); - } else { - console.log("Body is fine, changing password") - user.password = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(8), null); - user.passwordResetToken = ""; - user.passwordResetTokenExpiry = ""; - user.save() - .then(user => { - const msg = { - to: user.email, - from: 'support@sweet.sh', - subject: 'sweet password sucessfully reset', - text: 'Hi! The password on your sweet account ' + user.email + ' has just been changed.\n\n' + - 'If you did not do this, please get in touch with sweet support at support@sweet.sh immediately.\n' - }; - sgMail.send(msg) - .then(user => { - req.session.sessionFlash = { - message: 'Your password has been successfully changed. You can now log in with your email and new password.' - } - res.redirect(301, '/login'); - }) - }) - .catch(error => { - req.session.sessionFlash = { - type: 'alert', - message: "There has been a problem updating your password. Please try again in a few minutes." - } - res.redirect(301, '/reset-password/' + req.body.token); - console.error(error.toString()); - }) - } - }); + res.redirect(301, '/forgot-password') + }) + .catch(error => { + req.session.sessionFlash = { + type: 'alert', + message: 'There has been a problem sending your recovery email. Please try again in a few minutes.', + email: req.body.email } - }); - }); - - app.post('/pushnotifs/subscribe', async function (req, res) { - if (req.isAuthenticated()) { - var user = await User.findById(req.user._id); - if (!user.pushNotifSubscriptions.some(v => { //check to make sure there isn't already a subscription set up with this endpoint - return JSON.parse(v).endpoint == JSON.parse(req.body.subscription).endpoint; - })) { - user.pushNotifSubscriptions.push(req.body.subscription); - user.save(); + res.redirect(301, '/forgot-password') + console.error(error.toString()) + }) + }) + .catch(error => { + req.session.sessionFlash = { + type: 'alert', + message: 'There has been a problem sending your recovery email. Please try again in a few minutes.', + email: req.body.email } - } - res.sendStatus(200); - }) - - app.post('/pushnotifs/unsubscribe', async function (req, res) { - if (req.isAuthenticated()) { - var user = await User.findById(req.user._id); - user.pushNotifSubscriptions = user.pushNotifSubscriptions.filter(v => { //check to make sure there isn't already a subscription set up with this endpoint - return !(JSON.parse(v).endpoint == JSON.parse(req.body.subscription).endpoint); - }); - user.save(); - } - res.sendStatus(200); + res.redirect(301, '/forgot-password') + console.error(error.toString()) + }) + }) + }) + + // Responds to post requests with new passwords from the passport reset page. + // Input: the new password, the reset token, the username, and the email + // Output: a redirect to /forgot-password with a flash message if the token is wrong, a redirect to /reset-password + // with the token and a warning message if the new password is invalid, a new password for the user and an email + // telling the user they've reset the passport and a redirect to /login if everything is right, or a redirect to /reset-password + // with the token if there was a database error saving the user's new password. + app.post('/reset-password/:token', function (req, res) { + console.log(req.params.token) + User.findOne({ + passwordResetToken: req.params.token, + passwordResetTokenExpiry: { + $gt: Date.now() + } }) - - app.post('/api/recommendations/remove/:type/:id', isLoggedInOrRedirect, function (req, res) { - User.findOne({ - _id: req.user._id - }) - .then(user => { - if (req.params.type == "user") { - user.hiddenRecommendedUsers.push(req.params.id) - } - else if (req.params.type == "community") { - user.hiddenRecommendedCommunities.push(req.params.id) + .then(user => { + console.log(user) + if (!user) { + console.log('Wrong token (apparently)') + req.session.sessionFlash = { + type: 'alert', + message: 'Password reset token is invalid or has expired.' + } + res.redirect(301, '/forgot-password') + } else { + console.log('Checking body') + req.checkBody('password', 'Password must be at least 8 characters long.').isLength({ + min: 8 + }) + req.checkBody('password', 'Passwords do not match.').equals(req.body.passwordrepeat) + req.getValidationResult().then(function (result) { + if (!result.isEmpty()) { + var errors = result.array().map(function (elem) { + return elem.msg + }).join('
') + req.session.sessionFlash = { + type: 'alert', + message: errors, + username: req.body.username, + email: req.body.email + } + res.redirect(301, '/reset-password/' + req.params.token) + } else { + console.log('Body is fine, changing password') + user.password = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(8), null) + user.passwordResetToken = '' + user.passwordResetTokenExpiry = '' + user.save() + .then(user => { + const msg = { + to: user.email, + from: 'support@sweet.sh', + subject: 'sweet password sucessfully reset', + text: 'Hi! The password on your sweet account ' + user.email + ' has just been changed.\n\n' + + 'If you did not do this, please get in touch with sweet support at support@sweet.sh immediately.\n' + } + sgMail.send(msg) + .then(() => { + req.session.sessionFlash = { + message: 'Your password has been successfully changed. You can now log in with your email and new password.' + } + res.redirect(301, '/login') + }) + }) + .catch(error => { + req.session.sessionFlash = { + type: 'alert', + message: 'There has been a problem updating your password. Please try again in a few minutes.' + } + res.redirect(301, '/reset-password/' + req.body.token) + console.error(error.toString()) + }) } - user.save() - .then(result => { - res.sendStatus(200) - }) - }) - }) + }) + } + }) + }) -}; + app.post('/pushnotifs/subscribe', async function (req, res) { + if (req.isAuthenticated()) { + var user = await User.findById(req.user._id) + if (!user.pushNotifSubscriptions.some(v => { // check to make sure there isn't already a subscription set up with this endpoint + return JSON.parse(v).endpoint == JSON.parse(req.body.subscription).endpoint + })) { + user.pushNotifSubscriptions.push(req.body.subscription) + user.save() + } + } + res.sendStatus(200) + }) -//For post and get requests where the browser will handle the response automatically and so redirects will work -function isLoggedInOrRedirect(req, res, next) { + app.post('/pushnotifs/unsubscribe', async function (req, res) { if (req.isAuthenticated()) { - // A potentially expensive way to update a user's last logged in timestamp (currently only relevant to sorting search results) - currentTime = new Date(); - if ((currentTime - req.user.lastUpdated) > 3600000) { // If the timestamp is older than an hour - User.findOne({ - _id: req.user._id - }) - .then(user => { - user.lastUpdated = currentTime; - user.save() - }) + var user = await User.findById(req.user._id) + user.pushNotifSubscriptions = user.pushNotifSubscriptions.filter(v => { // check to make sure there isn't already a subscription set up with this endpoint + return !(JSON.parse(v).endpoint == JSON.parse(req.body.subscription).endpoint) + }) + user.save() + } + res.sendStatus(200) + }) + + app.post('/api/recommendations/remove/:type/:id', isLoggedInOrRedirect, function (req, res) { + User.findOne({ + _id: req.user._id + }) + .then(user => { + if (req.params.type == 'user') { + user.hiddenRecommendedUsers.push(req.params.id) + } else if (req.params.type == 'community') { + user.hiddenRecommendedCommunities.push(req.params.id) } - return next(); + user.save() + .then(result => { + res.sendStatus(200) + }) + }) + }) +} + +// For post and get requests where the browser will handle the response automatically and so redirects will work +function isLoggedInOrRedirect (req, res, next) { + if (req.isAuthenticated()) { + // A potentially expensive way to update a user's last logged in timestamp (currently only relevant to sorting search results) + currentTime = new Date() + if ((currentTime - req.user.lastUpdated) > 3600000) { // If the timestamp is older than an hour + User.findOne({ + _id: req.user._id + }) + .then(user => { + user.lastUpdated = currentTime + user.save() + }) } - res.redirect('/'); - //next('route'); don't want this! the request has been handled by the redirect, we don't need to do anything else with it in another route + return next() + } + res.redirect('/') + // next('route'); don't want this! the request has been handled by the redirect, we don't need to do anything else with it in another route } diff --git a/app/postingToSweet.js b/app/postingToSweet.js index 6b343d2c..df347ae7 100644 --- a/app/postingToSweet.js +++ b/app/postingToSweet.js @@ -1,1095 +1,1082 @@ // APIs -var apiConfig = require('../config/apis.js'); - -const sgMail = require('@sendgrid/mail'); -sgMail.setApiKey(apiConfig.sendgrid); - -module.exports = function(app) { - - //New image upload reciever. - //Inputs: image data. - //Outputs: if the image is under the max size for its file type (currently 5 MB for .gifs and 10 MB for .jpgs) it is saved (if it's a .gif), - //or resized, compressed, maybe flattened, and saved according to the user's image upload settings. Saves to the temp folder; when a post or comment is actually completed, - //it's moved to the image folder that post images are loaded from upon being displayed. A thumbnail version of the uploaded image, complete with final flattening and rotation, - //is sent in data url form in the response, along with the filename of the image as it is stored on the server - app.post("/api/image/v2", isLoggedInOrErrorResponse, async function(req, res) { - var imageQualitySettingsArray = { - 'standard': { - resize: 1200, - filetype: 'jpg', - jpegQuality: 85 - }, - 'high': { - resize: 2048, - filetype: 'png', - jpegQuality: 95 - }, - 'ridiculous': { - resize: 4096, - filetype: 'png', - jpegQuality: 95 - } - }; - var imageQualitySettings = imageQualitySettingsArray[req.user.settings.imageQuality]; - if (req.files.image) { - if (req.files.image.size <= 10485760) { - var sharpImage; - var imageMeta; - try { - sharpImage = sharp(req.files.image.data); - imageMeta = await sharpImage.metadata(); - } catch (err) { - console.log("image failed to be loaded by sharp for format determination"); - res.setHeader('content-type', 'text/plain'); - res.end(JSON.stringify({ error: "filetype" })); - return; - } - var imageFormat = imageMeta.format; - let imageUrl = shortid.generate(); - if (imageFormat == "gif") { - if (req.files.image.size <= 5242880) { - var imageData = req.files.image.data; - fs.writeFile('./cdn/images/temp/' + imageUrl + '.gif', imageData, 'base64', function(err) { //to temp - if (err) { - return console.log(err); - } - res.setHeader('content-type', 'text/plain'); - res.end(JSON.stringify({ url: imageUrl + '.gif' })); - }) - } else { - res.setHeader('content-type', 'text/plain'); - res.end(JSON.stringify({ error: "filesize" })); - } - } else if (imageFormat == "jpeg" || imageFormat == "png") { - sharpImage = sharpImage.resize({ - width: imageQualitySettings.resize, - withoutEnlargement: true - }).rotate(); - if (imageFormat == "png" && req.user.settings.imageQuality == "standard") { - sharpImage = sharpImage.flatten({ background: { r: 255, g: 255, b: 255 } }); - } - if (imageFormat == "jpeg" || req.user.settings.imageQuality == "standard") { - sharpImage = sharpImage.jpeg({ quality: imageQualitySettings.jpegQuality }) - var finalFormat = "jpeg"; - } else { - sharpImage = sharpImage.png(); - var finalFormat = "png"; - } +module.exports = function (app) { + // New image upload reciever. + // Inputs: image data. + // Outputs: if the image is under the max size for its file type (currently 5 MB for .gifs and 10 MB for .jpgs) it is saved (if it's a .gif), + // or resized, compressed, maybe flattened, and saved according to the user's image upload settings. Saves to the temp folder; when a post or comment is actually completed, + // it's moved to the image folder that post images are loaded from upon being displayed. A thumbnail version of the uploaded image, complete with final flattening and rotation, + // is sent in data url form in the response, along with the filename of the image as it is stored on the server + app.post('/api/image/v2', isLoggedInOrErrorResponse, async function (req, res) { + var imageQualitySettingsArray = { + standard: { + resize: 1200, + filetype: 'jpg', + jpegQuality: 85 + }, + high: { + resize: 2048, + filetype: 'png', + jpegQuality: 95 + }, + ridiculous: { + resize: 4096, + filetype: 'png', + jpegQuality: 95 + } + } + var imageQualitySettings = imageQualitySettingsArray[req.user.settings.imageQuality] + if (req.files.image) { + if (req.files.image.size <= 10485760) { + var sharpImage + var imageMeta + try { + sharpImage = sharp(req.files.image.data) + imageMeta = await sharpImage.metadata() + } catch (err) { + console.log('image failed to be loaded by sharp for format determination') + res.setHeader('content-type', 'text/plain') + res.end(JSON.stringify({ error: 'filetype' })) + return + } + var imageFormat = imageMeta.format + const imageUrl = shortid.generate() + if (imageFormat == 'gif') { + if (req.files.image.size <= 5242880) { + var imageData = req.files.image.data + fs.writeFile('./cdn/images/temp/' + imageUrl + '.gif', imageData, 'base64', function (err) { // to temp + if (err) { + return console.log(err) + } + res.setHeader('content-type', 'text/plain') + res.end(JSON.stringify({ url: imageUrl + '.gif' })) + }) + } else { + res.setHeader('content-type', 'text/plain') + res.end(JSON.stringify({ error: 'filesize' })) + } + } else if (imageFormat == 'jpeg' || imageFormat == 'png') { + sharpImage = sharpImage.resize({ + width: imageQualitySettings.resize, + withoutEnlargement: true + }).rotate() + if (imageFormat == 'png' && req.user.settings.imageQuality == 'standard') { + sharpImage = sharpImage.flatten({ background: { r: 255, g: 255, b: 255 } }) + } + if (imageFormat == 'jpeg' || req.user.settings.imageQuality == 'standard') { + sharpImage = sharpImage.jpeg({ quality: imageQualitySettings.jpegQuality }) + var finalFormat = 'jpeg' + } else { + sharpImage = sharpImage.png() + var finalFormat = 'png' + } + + // send the client a thumbnail bc a) maybe the image is being rotated according to exif data or is a png with transparency being removed, and b) bc using a really small thumbnail in the browser speeds subsequent front-end interactions way up, at least on my phone + // IN THEORY we should just be able to .clone() sharpImage and operate on the result of that instead of making this new object for the thumbnail, but i'll be damned if i can get that to behave, i get cropped images somehow + var thumbnail = sharp(req.files.image.data).resize({ height: 200, withoutEnlargement: true }) + thumbnail = await (finalFormat == 'jpeg' ? thumbnail.rotate().flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg() : thumbnail.rotate().png()).toBuffer() + var response = { url: imageUrl + '.' + finalFormat, thumbnail: 'data:image/' + finalFormat + ';base64,' + thumbnail.toString('base64') } + + await sharpImage.toFile('./cdn/images/temp/' + imageUrl + '.' + finalFormat) // to temp + .catch(err => { + console.error('could not temp save uploaded image:') + console.error(err) + }) - //send the client a thumbnail bc a) maybe the image is being rotated according to exif data or is a png with transparency being removed, and b) bc using a really small thumbnail in the browser speeds subsequent front-end interactions way up, at least on my phone - //IN THEORY we should just be able to .clone() sharpImage and operate on the result of that instead of making this new object for the thumbnail, but i'll be damned if i can get that to behave, i get cropped images somehow - var thumbnail = sharp(req.files.image.data).resize({ height: 200, withoutEnlargement: true }); - thumbnail = await (finalFormat == "jpeg" ? thumbnail.rotate().flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg() : thumbnail.rotate().png()).toBuffer(); - var response = { url: imageUrl + '.' + finalFormat, thumbnail: "data:image/" + finalFormat + ";base64," + thumbnail.toString('base64')}; + res.setHeader('content-type', 'text/plain') + res.end(JSON.stringify(response)) + } else { + console.log('image not a gif or a png or a jpg according to sharp!') + res.setHeader('content-type', 'text/plain') + res.end(JSON.stringify({ error: 'filetype' })) + } + } else { + res.setHeader('content-type', 'text/plain') + res.end(JSON.stringify({ error: 'filesize' })) + } + } + }) + + // Responds to post requests that inform the server that a post that images were uploaded for will not be posted by deleting those images. + // Inputs: image file name + // Outputs: the image presumably in the temp folder with that filename is deleted + app.post('/cleartempimage', isLoggedInOrErrorResponse, function (req, res) { + if (req.body.imageURL.match(/^(\w|-){7,14}.(jpeg|jpg|png|gif)$/)) { // makes sure the incoming imageURL matches the shortid format and then a . and then an image extension + fs.unlink('./cdn/images/temp/' + req.body.imageURL, function (e) { + if (e) { + console.log('could not delete image ' + './cdn/images/temp/' + req.body.imageURL) + console.log(e) + } + }) + } + res.sendStatus(200) + }) + + // Responds to post requests that create a new post. + // Input: if the post contains no inlineElements, a simple string with its html contents. otherwise, an array of paragraphs and inline element objects ready to be parsed by the function + // parseText calls to parse it. + // Outputs: all that stuff is saved as a new post document (with the body of the post parsed to turn urls and tags and @s into links). Or, error response if not logged in. + app.post('/createpost', isLoggedInOrErrorResponse, async function (req, res) { + var parsedResult = await helper.parseText(JSON.parse(req.body.postContent)) + + // don't save mentions or tags for draft posts, this means that notifications and tag adding will be deferred until the post is published and at that point + // all of the mentions and tags will register as "new" and so the right actions will occur then + if (req.body.isDraft) { + parsedResult.mentions = [] + parsedResult.tags = [] + } - await sharpImage.toFile('./cdn/images/temp/' + imageUrl + '.' + finalFormat) //to temp - .catch(err => { - console.error("could not temp save uploaded image:") - console.error(err); - }); + if (req.body.communityId) { + var imagePrivacy = (await Community.findById(req.body.communityId)).settings.visibility + } else if (req.body.isDraft) { + var imagePrivacy = 'private' // this should already be stored in req.body.postPrivacy but just in case + } else { + var imagePrivacy = req.body.postPrivacy + } - res.setHeader('content-type', 'text/plain'); - res.end(JSON.stringify(response)); - } else { - console.log("image not a gif or a png or a jpg according to sharp!"); - res.setHeader('content-type', 'text/plain'); - res.end(JSON.stringify({ error: "filetype" })); - return; - } - } else { - res.setHeader('content-type', 'text/plain'); - res.end(JSON.stringify({ error: "filesize" })); - } - } - }) + for (var inline of parsedResult.inlineElements) { + if (inline.type == 'image(s)') { + // calling this function also moves the images out of temp storage and saves documents for them in the images collection in the database + var horizOrVertics = await helper.finalizeImages(inline.images, (req.body.communityId ? 'community' : req.body.isDraft ? 'draft' : 'original'), req.body.communityId, req.user._id, imagePrivacy, req.user.settings.imageQuality) + inline.imageIsHorizontal = horizOrVertics.imageIsHorizontal + inline.imageIsVertical = horizOrVertics.imageIsVertical + } + } - //Responds to post requests that inform the server that a post that images were uploaded for will not be posted by deleting those images. - //Inputs: image file name - //Outputs: the image presumably in the temp folder with that filename is deleted - app.post("/cleartempimage", isLoggedInOrErrorResponse, function(req, res) { - if (req.body.imageURL.match(/^(\w|-){7,14}.(jpeg|jpg|png|gif)$/)) { //makes sure the incoming imageURL matches the shortid format and then a . and then an image extension - fs.unlink("./cdn/images/temp/" + req.body.imageURL, function(e) { - if (e) { - console.log("could not delete image " + "./cdn/images/temp/" + req.body.imageURL); - console.log(e); - } - }); - } - res.sendStatus(200); + var newPostUrl = shortid.generate() + const postCreationTime = new Date() + + if (!(parsedResult.inlineElements.length || parsedResult.text.trim())) { // in case someone tries to make a blank post with a custom ajax post request. storing blank posts = not to spec + res.status(400).send('bad post op') + return + } + var isCommunityPost = !!req.body.communityId + + var post = new Post({ + type: isCommunityPost ? 'community' : req.body.isDraft ? 'draft' : 'original', + community: isCommunityPost ? req.body.communityId : undefined, + authorEmail: req.user.email, + author: req.user._id, + url: newPostUrl, + privacy: isCommunityPost ? 'public' : req.body.isDraft ? 'private' : req.body.postPrivacy, + timestamp: postCreationTime, + lastUpdated: postCreationTime, + rawContent: req.body.postContent, + parsedContent: parsedResult.text, + numberOfComments: 0, + mentions: parsedResult.mentions, + tags: parsedResult.tags, + contentWarnings: req.body.postContentWarnings, + imageVersion: 3, + inlineElements: parsedResult.inlineElements, + subscribedUsers: [req.user._id] }) - //Responds to post requests that create a new post. - //Input: if the post contains no inlineElements, a simple string with its html contents. otherwise, an array of paragraphs and inline element objects ready to be parsed by the function - //parseText calls to parse it. - //Outputs: all that stuff is saved as a new post document (with the body of the post parsed to turn urls and tags and @s into links). Or, error response if not logged in. - app.post("/createpost", isLoggedInOrErrorResponse, async function(req, res) { + var newPostId = post._id - var parsedResult = await helper.parseText(JSON.parse(req.body.postContent)); + for (mention of parsedResult.mentions) { + if (mention != req.user.username) { + User.findOne({ username: mention }).then(async mentioned => { + if (isCommunityPost) { + if (mentioned.communities.some(v => v.equals(post.community))) { + notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + newPostUrl, 'post') + } + } else if (req.body.postPrivacy == 'private') { + if (await Relationship.findOne({ value: 'trust', fromUser: req.user._id, toUser: mentioned._id })) { + notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + newPostUrl, 'post') + } + } else { + notifier.notify('user', 'mention', mentioned._id, req.user._id, post._id, '/' + req.user.username + '/' + newPostUrl, 'post') + } + }) + } + } - //don't save mentions or tags for draft posts, this means that notifications and tag adding will be deferred until the post is published and at that point - //all of the mentions and tags will register as "new" and so the right actions will occur then - if(req.body.isDraft){ - parsedResult.mentions = []; - parsedResult.tags = []; - } + for (tag of parsedResult.tags) { + Tag.findOneAndUpdate({ name: tag }, { $push: { posts: newPostId.toString() }, $set: { lastUpdated: postCreationTime } }, { upsert: true, new: true }, function (error, result) { if (error) return }) + } - if (req.body.communityId) { - var imagePrivacy = (await Community.findById(req.body.communityId)).settings.visibility; - } else if(req.body.isDraft){ - var imagePrivacy = "private"; //this should already be stored in req.body.postPrivacy but just in case - }else{ - var imagePrivacy = req.body.postPrivacy; - } + if (isCommunityPost) { + Community.findOneAndUpdate({ _id: req.body.communityId }, { $set: { lastUpdated: new Date() } }) + } - for (var inline of parsedResult.inlineElements) { - if (inline.type == "image(s)") { - //calling this function also moves the images out of temp storage and saves documents for them in the images collection in the database - var horizOrVertics = await helper.finalizeImages(inline.images, (req.body.communityId ? "community" : req.body.isDraft ? "draft" : "original"), req.body.communityId, req.user._id, imagePrivacy, req.user.settings.imageQuality); - inline.imageIsHorizontal = horizOrVertics.imageIsHorizontal; - inline.imageIsVertical = horizOrVertics.imageIsVertical; - } + await post.save() + res.status(200).send('' + (postCreationTime.getTime() + 1)) + }) + + // Responds to requests that delete posts. + // Inputs: id of post to delete (in req.params) + // Outputs: delete each image, delete each tag, delete the boosted versions, delete each comment image, delete notifications it caused, delete the post document. + app.post('/deletepost/:postid', isLoggedInOrRedirect, function (req, res) { + Post.findOne({ _id: req.params.postid }) + .then((post) => { + if (!post.author._id.equals(req.user._id)) { + res.status(400).send('you are not the owner of this post which you are attempting to delete. i know how you feel, but this is not allowed') + return } - var newPostUrl = shortid.generate(); - let postCreationTime = new Date(); + function deleteImagesRecursive (postOrComment) { + if (postOrComment.inlineElements && postOrComment.inlineElements.length) { + for (const il of postOrComment.inlineElements) { + if (il.type == 'image(s)') { + for (const image of il.images) { + fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { + if (err) console.log('Image deletion error ' + err) + }) + Image.deleteOne({ filename: image }) + } + } + } + } else if (postOrComment.images && postOrComment.images.length) { + for (const image of postOrComment.images) { + fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { + if (err) console.log('Image deletion error ' + err) + }) + Image.deleteOne({ filename: image }) + } + } - if (!(parsedResult.inlineElements.length || parsedResult.text.trim())) { //in case someone tries to make a blank post with a custom ajax post request. storing blank posts = not to spec - res.status(400).send('bad post op'); - return; - } - var isCommunityPost = !!req.body.communityId; - - var post = new Post({ - type: isCommunityPost ? 'community' : req.body.isDraft ? 'draft' : 'original', - community: isCommunityPost ? req.body.communityId : undefined, - authorEmail: req.user.email, - author: req.user._id, - url: newPostUrl, - privacy: isCommunityPost ? 'public' : req.body.isDraft ? "private" : req.body.postPrivacy, - timestamp: postCreationTime, - lastUpdated: postCreationTime, - rawContent: req.body.postContent, - parsedContent: parsedResult.text, - numberOfComments: 0, - mentions: parsedResult.mentions, - tags: parsedResult.tags, - contentWarnings: req.body.postContentWarnings, - imageVersion: 3, - inlineElements: parsedResult.inlineElements, - subscribedUsers: [req.user._id] - }); - - var newPostId = post._id; - - for (mention of parsedResult.mentions) { - if (mention != req.user.username) { - User.findOne({ username: mention }).then(async mentioned => { - if (isCommunityPost) { - if (mentioned.communities.some(v=>v.equals(post.community))) { - notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + newPostUrl, 'post'); - } - } else if (req.body.postPrivacy == "private") { - if (await Relationship.findOne({ value: 'trust', fromUser: req.user._id, toUser: mentioned._id })) { - notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + newPostUrl, 'post'); - } - } else { - notifier.notify('user', 'mention', mentioned._id, req.user._id, post._id, '/' + req.user.username + '/' + newPostUrl, 'post') - } - }) + if (postOrComment.comments && postOrComment.comments.length) { + for (var comment of postOrComment.comments) { + deleteImagesRecursive(comment) } + } + if (postOrComment.replies && postOrComment.replies.length) { + for (var reply of postOrComment.replies) { + deleteImagesRecursive(reply) + } + } } - for (tag of parsedResult.tags) { - Tag.findOneAndUpdate({ name: tag }, { "$push": { "posts": newPostId.toString() }, "$set": { "lastUpdated": postCreationTime } }, { upsert: true, new: true }, function(error, result) { if (error) return }); + deleteImagesRecursive(post) + + // Delete tags (does not currently fix tag last updated time) + if (post.tags) { + post.tags.forEach((tag) => { + Tag.findOneAndUpdate({ name: tag }, { $pull: { posts: req.params.postid } }) + .then((tag) => { + console.log('Deleted post from tag: ' + tag) + }) + .catch((err) => { + console.log('Database error while attempting to delete post from tag: ' + err) + }) + }) } - if (isCommunityPost) { - Community.findOneAndUpdate({ _id: req.body.communityId }, { $set: { lastUpdated: new Date() } }) + // Delete boosts + if (post.type == 'original' && post.boosts) { + post.boosts.forEach((boost) => { + Post.deleteOne({ _id: boost }) + .then((boost) => { + console.log('Deleted boost: ' + boost) + }) + .catch((err) => { + console.log('Database error while attempting to delete boost while deleting post: ' + err) + }) + }) } - await post.save(); - res.status(200).send("" + (postCreationTime.getTime() + 1)); - }); + // Delete notifications + User.update({}, { $pull: { notifications: { subjectId: post._id } } }, { multi: true }).then(response => { console.log(response) }) + }) + .then(() => { + Post.deleteOne({ _id: req.params.postid }) + .then(() => { + res.sendStatus(200) + }) + .catch((err) => { + console.log('Error while attempting to delete post: ' + err) + }) + }) + }) + + // Responds to post requests which create a comment. + // Inputs: comment body, filenames of comment images, descriptions of comment images + // Outputs: makes the comment document (with the body parsed for urls, tags, and @mentions), embeds a comment document in its post document, + // moves comment images out of temp. Also, notify the owner of the post, people subscribed to the post, and everyone who was mentioned. + app.post('/createcomment/:postid/:commentid', isLoggedInOrErrorResponse, async function (req, res) { + commentTimestamp = new Date() + var commentId = mongoose.Types.ObjectId() + + var rawContent = req.body.commentContent + var parsedResult = await helper.parseText(JSON.parse(rawContent)) + + if (!(parsedResult.inlineElements.length || parsedResult.text.trim())) { + res.status(400).send('bad post op') + return + } - //Responds to requests that delete posts. - //Inputs: id of post to delete (in req.params) - //Outputs: delete each image, delete each tag, delete the boosted versions, delete each comment image, delete notifications it caused, delete the post document. - app.post("/deletepost/:postid", isLoggedInOrRedirect, function(req, res) { - Post.findOne({ "_id": req.params.postid }) - .then((post) => { + var comment = { + _id: commentId, + authorEmail: req.user.email, + author: req.user._id, + timestamp: commentTimestamp, + rawContent: rawContent, + parsedContent: parsedResult.text, + mentions: parsedResult.mentions, + tags: parsedResult.tags + } - if (!post.author._id.equals(req.user._id)) { - res.status(400).send("you are not the owner of this post which you are attempting to delete. i know how you feel, but this is not allowed"); - return; - } + Post.findOne({ _id: req.params.postid }) + .populate('author') + .then(async (post) => { + if (post.communityId) { + var postType = 'community' + var postPrivacy = (await Community.findById(post.communityId)).settings.visibility + } else { + var postType = 'original' + var postPrivacy = post.privacy + } - function deleteImagesRecursive(postOrComment) { - if (postOrComment.inlineElements && postOrComment.inlineElements.length) { - for (const il of postOrComment.inlineElements) { - if (il.type == "image(s)") { - for (const image of il.images) { - fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { - if (err) console.log("Image deletion error " + err) - }); - Image.deleteOne({ "filename": image }); - } - } - } - } else if (postOrComment.images && postOrComment.images.length) { - for (const image of postOrComment.images) { - fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { - if (err) console.log("Image deletion error " + err) - }); - Image.deleteOne({ "filename": image }); - } - } + for (var inline of parsedResult.inlineElements) { + if (inline.type == 'image(s)') { + // calling this function also moves the images out of temp storage and saves documents for them in the images collection in the database + var horizOrVertics = await helper.finalizeImages(inline.images, postType, post.communityId, req.user._id, postPrivacy, req.user.settings.imageQuality) + inline.imageIsHorizontal = horizOrVertics.imageIsHorizontal + inline.imageIsVertical = horizOrVertics.imageIsVertical + } + } - if (postOrComment.comments && postOrComment.comments.length) { - for (var comment of postOrComment.comments) { - deleteImagesRecursive(comment); - } - } - if (postOrComment.replies && postOrComment.replies.length) { - for (var reply of postOrComment.replies) { - deleteImagesRecursive(reply); - } - } + comment.inlineElements = parsedResult.inlineElements + var contentHTML = await helper.renderHTMLContent(comment) + comment.cachedHTML = { fullContentHTML: contentHTML } + + numberOfComments = 0 + var depth = undefined + commentParent = false + if (req.params.commentid == 'undefined') { + depth = 1 + // This is a top level comment with no parent (identified by commentid) + post.comments.push(comment) + + // Count total comments + function countComments (array) { + array.forEach((element) => { + if (!element.deleted) { + numberOfComments++ + } + if (element.replies) { + var replies = countComments(element.replies) + if (replies) { + return replies } - - deleteImagesRecursive(post); - - // Delete tags (does not currently fix tag last updated time) - if (post.tags) { - post.tags.forEach((tag) => { - Tag.findOneAndUpdate({ name: tag }, { $pull: { posts: req.params.postid } }) - .then((tag) => { - console.log("Deleted post from tag: " + tag) - }) - .catch((err) => { - console.log("Database error while attempting to delete post from tag: " + err) - }) - }) + } + }) + return numberOfComments + } + post.numberOfComments = countComments(post.comments) + } else { + // This is a child level comment so we have to drill through the comments + // until we find it + commentParent = undefined + + function findNested (array, id, depthSoFar = 2) { + var foundElement = false + array.forEach((element) => { + if (element._id && element._id.equals(id)) { + if (depthSoFar > 5) { + res.status(403).send('>:^(') + return undefined + } else { + depth = depthSoFar + element.replies.push(comment) + foundElement = element + commentParent = element } - - // Delete boosts - if (post.type == "original" && post.boosts) { - post.boosts.forEach((boost) => { - Post.deleteOne({ "_id": boost }) - .then((boost) => { - console.log("Deleted boost: " + boost) - }) - .catch((err) => { - console.log("Database error while attempting to delete boost while deleting post: " + err) - }) - }) + } + if (!element.deleted) { + numberOfComments++ + } + if (element.replies) { + var found = findNested(element.replies, id, depthSoFar + 1) + if (found) { + foundElement = element + commentParent = element + return found } - - // Delete notifications - User.update({}, { $pull: { notifications: { subjectId: post._id } } }, { multi: true }).then(response => { console.log(response) }) + } }) - .then(() => { - Post.deleteOne({ "_id": req.params.postid }) - .then(() => { - res.sendStatus(200); - }) - .catch((err) => { - console.log("Error while attempting to delete post: " + err) - }); - }); - }); - - //Responds to post requests which create a comment. - //Inputs: comment body, filenames of comment images, descriptions of comment images - //Outputs: makes the comment document (with the body parsed for urls, tags, and @mentions), embeds a comment document in its post document, - //moves comment images out of temp. Also, notify the owner of the post, people subscribed to the post, and everyone who was mentioned. - app.post("/createcomment/:postid/:commentid", isLoggedInOrErrorResponse, async function(req, res) { - commentTimestamp = new Date(); - var commentId = mongoose.Types.ObjectId(); - - var rawContent = req.body.commentContent; - var parsedResult = await helper.parseText(JSON.parse(rawContent)); - - if (!(parsedResult.inlineElements.length || parsedResult.text.trim())) { - res.status(400).send('bad post op'); - return; + return foundElement + } + var target = findNested(post.comments, req.params.commentid) + if (target) { + post.numberOfComments = numberOfComments + } } - - var comment = { - _id: commentId, - authorEmail: req.user.email, - author: req.user._id, - timestamp: commentTimestamp, - rawContent: rawContent, - parsedContent: parsedResult.text, - mentions: parsedResult.mentions, - tags: parsedResult.tags, - }; - - Post.findOne({ "_id": req.params.postid }) - .populate('author') - .then(async (post) => { - - if (post.communityId) { - var postType = "community"; - var postPrivacy = (await Community.findById(post.communityId)).settings.visibility; + if (!depth) { + // if depth was left undefined then it was found to be invalid (i.e. > 5), let's get out of here + return + } + var postPrivacy = post.privacy + post.lastUpdated = new Date() + // Add user to subscribed users for post + if ((!post.author._id.equals(req.user._id) && post.subscribedUsers.includes(req.user._id.toString()) === false)) { // Don't subscribe to your own post, or to a post you're already subscribed to + post.subscribedUsers.push(req.user._id.toString()) + } + post.save() + .then(async () => { + // Notify any and all interested parties + User.findOne({ _id: post.author }) + .then((originalPoster) => { + // remove duplicates from subscribed/unsubscribed users + subscribedUsers = post.subscribedUsers.filter((v, i, a) => a.indexOf(v) === i) + unsubscribedUsers = post.unsubscribedUsers.filter((v, i, a) => a.indexOf(v) === i) + + // NOTIFY EVERYONE WHO IS MENTIONED + + // we're never going to notify the author of the comment about them mentioning themself + workingMentions = parsedResult.mentions.filter(m => m != req.user.username) + + if (post.type == 'community') { + workingMentions.forEach(function (mentionedUsername) { + User.findOne({ + username: mentionedUsername + }).then((mentionedUser) => { + // within communities: notify the mentioned user if this post's community is one they belong to + if (mentionedUser.communities.some(c => c.toString() == post.community.toString())) { + notifier.notify('user', 'mention', mentionedUser._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') + } + }).catch(err => { + console.log('could not find document for mentioned user ' + mentionedUsername + ', error:') + console.log(err) + }) + }) } else { - var postType = "original"; - var postPrivacy = post.privacy; - } - - for (var inline of parsedResult.inlineElements) { - if (inline.type == "image(s)") { - //calling this function also moves the images out of temp storage and saves documents for them in the images collection in the database - var horizOrVertics = await helper.finalizeImages(inline.images, postType, post.communityId, req.user._id, postPrivacy, req.user.settings.imageQuality); - inline.imageIsHorizontal = horizOrVertics.imageIsHorizontal; - inline.imageIsVertical = horizOrVertics.imageIsVertical; - } - } - - comment.inlineElements = parsedResult.inlineElements; - var contentHTML = await helper.renderHTMLContent(comment) - comment.cachedHTML = { fullContentHTML: contentHTML }; - - numberOfComments = 0; - var depth = undefined; - commentParent = false; - if (req.params.commentid == 'undefined') { - depth = 1; - // This is a top level comment with no parent (identified by commentid) - post.comments.push(comment); - - // Count total comments - function countComments(array) { - array.forEach((element) => { - if (!element.deleted) { - numberOfComments++; - } - if (element.replies) { - var replies = countComments(element.replies) - if (replies) { - return replies; - } - } + if (postPrivacy == 'private') { + workingMentions.forEach(mentionedUsername => { + User.findOne({ + username: mentionedUsername + }).then(mentionedUser => { + // Make sure to only notify mentioned people if they are trusted by the post's author (and can therefore see the post). + // The post's author is implicitly trusted by the post's author + if (mentionedUser._id.equals(originalPoster._id)) { + notifier.notify('user', 'mention', mentionedUser._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') + return // no need to go down there and check for relationships and stuff + } + Relationship.findOne({ + fromUser: originalPoster._id, + toUser: mentionedUser._id, + value: 'trust' + }, { + _id: 1 + }).then(theRelationshipExists => { + if (theRelationshipExists) { + notifier.notify('user', 'mention', mentionedUser._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') + } }) - return numberOfComments; - } - post.numberOfComments = countComments(post.comments); - } else { - // This is a child level comment so we have to drill through the comments - // until we find it - commentParent = undefined; - - function findNested(array, id, depthSoFar = 2) { - var foundElement = false; - array.forEach((element) => { - if (element._id && element._id.equals(id)) { - if (depthSoFar > 5) { - res.status(403).send(">:^("); - return undefined; - } else { - depth = depthSoFar; - element.replies.push(comment); - foundElement = element; - commentParent = element; - } - } - if (!element.deleted) { - numberOfComments++; - } - if (element.replies) { - var found = findNested(element.replies, id, depthSoFar + 1) - if (found) { - foundElement = element; - commentParent = element; - return found; - } - } + }).catch(err => { + console.log('could not find document for mentioned user ' + mention + ', error:') + console.log(err) + }) + }) + } else if (postPrivacy == 'public') { + workingMentions.forEach(function (mention) { + User.findOne({ + username: mention + }) + .then((mentionedGuy) => { + // notify everyone + notifier.notify('user', 'mention', mentionedGuy._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') + }).catch(err => { + console.log('could not find document for mentioned user ' + mention + ', error:') + console.log(err) }) - return foundElement; - } - var target = findNested(post.comments, req.params.commentid); - if (target) { - post.numberOfComments = numberOfComments; - } - } - if (!depth) { - //if depth was left undefined then it was found to be invalid (i.e. > 5), let's get out of here - return; + }) + } } - var postPrivacy = post.privacy; - post.lastUpdated = new Date(); - // Add user to subscribed users for post - if ((!post.author._id.equals(req.user._id) && post.subscribedUsers.includes(req.user._id.toString()) === false)) { // Don't subscribe to your own post, or to a post you're already subscribed to - post.subscribedUsers.push(req.user._id.toString()); + + // NOTIFY THE POST'S AUTHOR + // Author doesn't need to know about their own comments, and about replies on your posts they're not subscribed to, and if they're @ed they already got a notification above + if (!originalPoster._id.equals(req.user._id) && (post.unsubscribedUsers.includes(originalPoster._id.toString()) === false) && (!parsedResult.mentions.includes(originalPoster.username))) { + console.log('Notifying post author of a reply') + notifier.notify('user', 'reply', originalPoster._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url + '#comment-' + comment._id, 'post') } - post.save() - .then(async () => { - - //Notify any and all interested parties - User.findOne({ _id: post.author }) - .then((originalPoster) => { - //remove duplicates from subscribed/unsubscribed users - subscribedUsers = post.subscribedUsers.filter((v, i, a) => a.indexOf(v) === i); - unsubscribedUsers = post.unsubscribedUsers.filter((v, i, a) => a.indexOf(v) === i); - - //NOTIFY EVERYONE WHO IS MENTIONED - - //we're never going to notify the author of the comment about them mentioning themself - workingMentions = parsedResult.mentions.filter(m => m != req.user.username); - - if (post.type == "community") { - workingMentions.forEach(function(mentionedUsername) { - User.findOne({ - username: mentionedUsername - }).then((mentionedUser) => { - //within communities: notify the mentioned user if this post's community is one they belong to - if (mentionedUser.communities.some(c => c.toString() == post.community.toString())) { - notifier.notify('user', 'mention', mentionedUser._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') - } - }).catch(err => { - console.log("could not find document for mentioned user " + mentionedUsername + ", error:"); - console.log(err); - }) - }) - } else { - if (postPrivacy == "private") { - workingMentions.forEach(mentionedUsername => { - User.findOne({ - username: mentionedUsername - }).then(mentionedUser => { - // Make sure to only notify mentioned people if they are trusted by the post's author (and can therefore see the post). - // The post's author is implicitly trusted by the post's author - if (mentionedUser._id.equals(originalPoster._id)) { - notifier.notify('user', 'mention', mentionedUser._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') - return; //no need to go down there and check for relationships and stuff - } - Relationship.findOne({ - fromUser: originalPoster._id, - toUser: mentionedUser._id, - value: "trust" - }, { - _id: 1 - }).then(theRelationshipExists => { - if (theRelationshipExists) { - notifier.notify('user', 'mention', mentionedUser._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') - } - }) - }).catch(err => { - console.log("could not find document for mentioned user " + mention + ", error:"); - console.log(err); - }) - }) - } else if (postPrivacy == "public") { - workingMentions.forEach(function(mention) { - User.findOne({ - username: mention - }) - .then((mentionedGuy) => { - //notify everyone - notifier.notify('user', 'mention', mentionedGuy._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'reply') - }).catch(err => { - console.log("could not find document for mentioned user " + mention + ", error:"); - console.log(err); - }) - }); - } - } - - // NOTIFY THE POST'S AUTHOR - // Author doesn't need to know about their own comments, and about replies on your posts they're not subscribed to, and if they're @ed they already got a notification above - if (!originalPoster._id.equals(req.user._id) && (post.unsubscribedUsers.includes(originalPoster._id.toString()) === false) && (!parsedResult.mentions.includes(originalPoster.username))) { - console.log("Notifying post author of a reply") - notifier.notify('user', 'reply', originalPoster._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url + '#comment-' + comment._id, 'post') - } - - // NOTIFY THE PARENT COMMENT'S AUTHOR - // Author doesn't need to know about their own child comments, and about replies on your posts they're not subscribed to, and if they're @ed they already got a notification above, and if they're the post's author as well as the parent comment's author (they got a notification above for that too) - // First check if this comment even HAS a parent - if (commentParent) { - parentCommentAuthor = commentParent.author; - if (!parentCommentAuthor._id.equals(req.user._id) && + + // NOTIFY THE PARENT COMMENT'S AUTHOR + // Author doesn't need to know about their own child comments, and about replies on your posts they're not subscribed to, and if they're @ed they already got a notification above, and if they're the post's author as well as the parent comment's author (they got a notification above for that too) + // First check if this comment even HAS a parent + if (commentParent) { + parentCommentAuthor = commentParent.author + if (!parentCommentAuthor._id.equals(req.user._id) && (post.unsubscribedUsers.includes(parentCommentAuthor._id.toString()) === false) && (!parsedResult.mentions.includes(parentCommentAuthor.username)) && (!originalPoster._id.equals(parentCommentAuthor._id))) { - console.log("Notifying parent comment author of a reply") - notifier.notify('user', 'commentReply', parentCommentAuthor._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url + '#comment-' + commentParent._id, 'post') - } - } - - //NOTIFY PEOPLE WHO BOOSTED THE POST - if (post.boostsV2.length > 0) { - var boosterIDs = []; - post.populate('boostV2.booster', (err, populatedPost) => { - if (err) { - console.log('could not notify people who boosted post ' + post._id.toString() + " of a recent reply:"); - console.log(err); - } else { - populatedPost.boostsV2.forEach(boost => { - boosterIDs.push(boost.booster._id.toString()); - //make sure we're not notifying the person who left the comment (this will be necessary if they left it on their own boosted post) - //and make sure we're not notifying the post's author (necessary if they boosted their own post) (they'll have gotten a notification above) - //and make sure we're not notifying anyone who was @ed (they'll have gotten a notification above), - //or anyone who unsubscribed from the post - if (!boost.booster._id.equals(req.user._id) && + console.log('Notifying parent comment author of a reply') + notifier.notify('user', 'commentReply', parentCommentAuthor._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url + '#comment-' + commentParent._id, 'post') + } + } + + // NOTIFY PEOPLE WHO BOOSTED THE POST + if (post.boostsV2.length > 0) { + var boosterIDs = [] + post.populate('boostV2.booster', (err, populatedPost) => { + if (err) { + console.log('could not notify people who boosted post ' + post._id.toString() + ' of a recent reply:') + console.log(err) + } else { + populatedPost.boostsV2.forEach(boost => { + boosterIDs.push(boost.booster._id.toString()) + // make sure we're not notifying the person who left the comment (this will be necessary if they left it on their own boosted post) + // and make sure we're not notifying the post's author (necessary if they boosted their own post) (they'll have gotten a notification above) + // and make sure we're not notifying anyone who was @ed (they'll have gotten a notification above), + // or anyone who unsubscribed from the post + if (!boost.booster._id.equals(req.user._id) && !boost.booster._id.equals(originalPoster._id) && !parsedResult.mentions.includes(boost.booster.username) && !post.unsubscribedUsers.includes(boost.booster._id.toString())) { - notifier.notify('user', 'boostedPostReply', boost.booster._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'post') - } - }) - } - //if there are boosters, we notify the other "subscribers" here, because here we have the full list of - //boosters and can check the subscribers against it before notifying them - var workingSubscribers = post.subscribedUsers.filter(u => !boosterIDs.includes(u)); - notifySubscribers(workingSubscribers); - }) - } - - //NOTIFY THE OTHER SUBSCRIBERS (PEOPLE WHO WERE MENTIONED IN THE ORGINAL POST AND THOSE WHO COMMENTED ON IT) - - //if there are boosts for this post, this was called a few lines up from here. otherwise, we do it now - if (post.boostsV2.length === 0) { - notifySubscribers(post.subscribedUsers) - } - - //checks each subscriber for trustedness if this is a private post, notifies all of 'em otherwise - function notifySubscribers(subscriberList) { - if (postPrivacy == "private") { - subscriberList.forEach(subscriberID => { - Relationship.findOne({ - fromUser: originalPoster._id, - toUser: subscriberID, - value: "trust" - }, { - _id: 1 - }).then(theRelationshipExists => { - if (theRelationshipExists) { - notifySubscriber(subscriberID); - } - }) - }) - } else { - subscriberList.forEach(subscriberID => { - notifySubscriber(subscriberID); - }) - } - } - - function notifySubscriber(subscriberID) { - if ((subscriberID != req.user._id.toString()) // Do not notify the comment's author about the comment - && - (subscriberID != originalPoster._id.toString()) //don't notify the post's author (because they get a different notification, above) - && - (post.unsubscribedUsers.includes(subscriberID) === false) //don't notify unsubscribed users - && - (commentParent ? subscriberID != parentCommentAuthor._id.toString() : true) // don't notify parent comment author, if it's a child comment (because they get a different notification, above) - ) { - console.log("Notifying subscribed user"); - User.findById(subscriberID).then((subscriber) => { - if (!parsedResult.mentions.includes(subscriber.username)) { //don't notify people who are going to be notified anyway bc they're mentioned in the new comment - if (post.mentions.includes(subscriber.username)) { - notifier.notify('user', 'mentioningPostReply', subscriberID, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'post') - } else { - notifier.notify('user', 'subscribedReply', subscriberID, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'post') - } - } - }).catch(err => { - console.log("could not find subscribed user " + subscriberID + ", error:") - console.log(err); - }) - } - } - - }).catch(err => { - console.log("can't find author of commented-upon post, error:"); - console.log(err); - }) - - if (req.user.imageEnabled) { - var image = req.user.image - } else { - var image = 'cake.svg' - } - if (req.user.displayName) { - var name = '' + req.user.displayName + '@' + req.user.username + ''; - } else { - var name = '@' + req.user.username + ''; - } - - hbs.render('./views/partials/comment_dynamic.handlebars', { - image: image, - name: name, - username: req.user.username, - timestamp: moment(commentTimestamp).fromNow(), - content: contentHTML, - comment_id: commentId.toString(), - post_id: post._id.toString(), - depth: depth - }) - .then(async html => { - var result = { - comment: html - } - res.contentType('json'); - res.send(JSON.stringify(result)); - }) - }) - .catch((err) => { - console.log("Database error: " + err) - }); - }) - }); - - //Responds to post requests that delete comments. - //Input: postid and commentid. - //Output: deletes each of the comment's images and removes the comment's document from the post. Then, updates the post's lastUpdated field to be - //that of the new most recent comment's (or the time of the post's creation if there are no comments left) with the relocatePost function. Also - //updates numberOfComments. - app.post("/deletecomment/:postid/:commentid", isLoggedInOrRedirect, function(req, res) { - Post.findOne({ "_id": req.params.postid }) - .then((post) => { - var commentsByUser = 0; - var latestTimestamp = 0; - var numberOfComments = 0; - var target = undefined; - - function findNested(array, id, parent) { - array.forEach((element) => { - if (!element.deleted) { - numberOfComments++; - } - if ((element.author.toString() == req.user._id.toString()) && !element.deleted) { - commentsByUser++; - } - if (element.timestamp > latestTimestamp) { - latestTimestamp = element.timestamp; - } - element.numberOfSiblings = (parent.replies ? parent.replies.length - 1 : post.comments.length - 1); - element.parent = parent; - if (!target && element._id && element._id.equals(id)) { - target = element; - commentsByUser--; - numberOfComments--; - console.log('numberOfComments', numberOfComments) - } - if (element.replies) { - findNested(element.replies, id, element) - } - }) - } - - findNested(post.comments, req.params.commentid, post); - if (target) { - post.numberOfComments = numberOfComments; - } - - //i'll be impressed if someone trips this one, comment ids aren't displayed for comments that the logged in user didn't make - if (!target.author.equals(req.user._id) && post.author.toString() != req.user._id.toString()) { - res.status(400).send("you do not appear to be who you would like us to think that you are! this comment ain't got your brand on it"); - return; - } - - if (target.images && target.images.length) { - for (const image of target.images) { - fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { - if (err) console.log("Image deletion error " + err) - }) - Image.deleteOne({ "filename": image }); - } - } else if (target.inlineElements && target.inlineElements.length) { - for (const ie of target.inlineElements) { - if (ie.type == "image(s)") { - for (const image of ie.images) { - fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { - if (err) console.log("Image deletion error " + err) - }) - Image.deleteOne({ "filename": image }); - } + notifier.notify('user', 'boostedPostReply', boost.booster._id, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'post') } + }) } + // if there are boosters, we notify the other "subscribers" here, because here we have the full list of + // boosters and can check the subscribers against it before notifying them + var workingSubscribers = post.subscribedUsers.filter(u => !boosterIDs.includes(u)) + notifySubscribers(workingSubscribers) + }) } - // Check if target has children - if (target.replies && target.replies.length) { - // We feel sorry for the children - just wipe the target's memory - target.parsedContent = ""; - target.rawContent = ""; - target.deleted = true; - } else { - // There are no children, the target can be destroyed - target.remove(); - if (target.numberOfSiblings == 0 && target.parent.deleted) { - // There are also no siblings, and the element's parent - // has been deleted, so we can even destroy that! - target.parent.remove(); - } + // NOTIFY THE OTHER SUBSCRIBERS (PEOPLE WHO WERE MENTIONED IN THE ORGINAL POST AND THOSE WHO COMMENTED ON IT) + + // if there are boosts for this post, this was called a few lines up from here. otherwise, we do it now + if (post.boostsV2.length === 0) { + notifySubscribers(post.subscribedUsers) } - post.save() - .then((comment) => { - post.lastUpdated = latestTimestamp; - //unsubscribe the author of the deleted comment from the post if they have no other comments on it - if (commentsByUser == 0) { - post.subscribedUsers = post.subscribedUsers.filter((v, i, a) => { - return v != req.user._id.toString(); - }) - post.save().catch(err => { - console.error(err) - }) - } - // if (!target.some((v, i, a) => { - // return v.author.toString() == req.user._id.toString(); - // })) { - // post.subscribedUsers = post.subscribedUsers.filter((v, i, a) => { - // return v != req.user._id.toString(); - // }) - // post.save().catch(err => { - // console.error(err) - // }) - // } - result = { - numberOfComments: numberOfComments + // checks each subscriber for trustedness if this is a private post, notifies all of 'em otherwise + function notifySubscribers (subscriberList) { + if (postPrivacy == 'private') { + subscriberList.forEach(subscriberID => { + Relationship.findOne({ + fromUser: originalPoster._id, + toUser: subscriberID, + value: 'trust' + }, { + _id: 1 + }).then(theRelationshipExists => { + if (theRelationshipExists) { + notifySubscriber(subscriberID) } - res.contentType('json').send(JSON.stringify(result)); + }) }) - .catch((error) => { - console.error(error) + } else { + subscriberList.forEach(subscriberID => { + notifySubscriber(subscriberID) }) - }) - }); - - //Responds to a post request that boosts a post. - //Inputs: id of the post to be boosted - //Outputs: a new post of type boost, adds the id of that new post into the boosts field of the old post, sends a notification to the - //user whose post was boosted. - app.post('/createboost/:postid', isLoggedInOrRedirect, function(req, res) { - boostedTimestamp = new Date(); - Post.findOne({ - '_id': req.params.postid - }, { - boostsV2: 1, - lastUpdated: 1, - privacy: 1, - unsubscribedUsers: 1, - author: 1, - url: 1 - }).populate('author') - .then((boostedPost) => { - if (boostedPost.privacy != "public" || boostedPost.type == 'community') { - res.status(400).send("post is not public and therefore may not be boosted"); - return; + } } - var boost = new Post({ - type: 'boost', - authorEmail: req.user.email, - author: req.user._id, - url: shortid.generate(), - privacy: 'public', - timestamp: boostedTimestamp, - lastUpdated: boostedTimestamp, - boostTarget: boostedPost._id - }) - boost.save().then(savedBoost => { - const boost = { - booster: req.user._id, - timestamp: boostedTimestamp, - boost: savedBoost._id - } - boostedPost.boostsV2 = boostedPost.boostsV2.filter(boost => { - return !boost.booster.equals(req.user._id) - }) - boostedPost.boostsV2.push(boost); - boostedPost.save().then(() => { - //don't notify the original post's author if they're creating the boost or are unsubscribed from this post - if (!boostedPost.unsubscribedUsers.includes(boostedPost.author._id.toString()) && !boostedPost.author._id.equals(req.user._id)) { - notifier.notify('user', 'boost', boostedPost.author._id, req.user._id, null, '/' + boostedPost.author.username + '/' + boostedPost.url, 'post') + function notifySubscriber (subscriberID) { + if ((subscriberID != req.user._id.toString()) // Do not notify the comment's author about the comment + && + (subscriberID != originalPoster._id.toString()) // don't notify the post's author (because they get a different notification, above) + && + (post.unsubscribedUsers.includes(subscriberID) === false) // don't notify unsubscribed users + && + (commentParent ? subscriberID != parentCommentAuthor._id.toString() : true) // don't notify parent comment author, if it's a child comment (because they get a different notification, above) + ) { + console.log('Notifying subscribed user') + User.findById(subscriberID).then((subscriber) => { + if (!parsedResult.mentions.includes(subscriber.username)) { // don't notify people who are going to be notified anyway bc they're mentioned in the new comment + if (post.mentions.includes(subscriber.username)) { + notifier.notify('user', 'mentioningPostReply', subscriberID, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'post') + } else { + notifier.notify('user', 'subscribedReply', subscriberID, req.user._id, post._id, '/' + originalPoster.username + '/' + post.url, 'post') } - res.redirect("back"); + } + }).catch(err => { + console.log('could not find subscribed user ' + subscriberID + ', error:') + console.log(err) }) - }) - }) - }) + } + } + }).catch(err => { + console.log("can't find author of commented-upon post, error:") + console.log(err) + }) - //Responds to a post request that boosts a post. - //Inputs: id of the post to be boosted - //Outputs: a new post of type boost, adds the id of that new post into the boosts field of the old post, sends a notification to the - //user whose post was boosted. - app.post('/removeboost/:postid', isLoggedInOrRedirect, function(req, res) { - Post.findOne({ '_id': req.params.postid }, { boostsV2: 1, privacy: 1, author: 1, url: 1, timestamp: 1 }) - .then((boostedPost) => { - var boost = boostedPost.boostsV2.find(b => { - return b.booster.equals(req.user._id) - }); - boostedPost.boostsV2 = boostedPost.boostsV2.filter(boost => { - return !boost.booster.equals(req.user._id) - }) - Post.deleteOne({ - _id: boost.boost - }, function() { - console.log('delete') - }); - boostedPost.save().then(() => { - res.redirect("back"); - }) - }) - }) + if (req.user.imageEnabled) { + var image = req.user.image + } else { + var image = 'cake.svg' + } + if (req.user.displayName) { + var name = '' + req.user.displayName + '@' + req.user.username + '' + } else { + var name = '@' + req.user.username + '' + } - app.post('/createposteditor/:postid', isLoggedInOrRedirect, function(req, res) { - Post.findOne({ - _id: req.params.postid + hbs.render('./views/partials/comment_dynamic.handlebars', { + image: image, + name: name, + username: req.user.username, + timestamp: moment(commentTimestamp).fromNow(), + content: contentHTML, + comment_id: commentId.toString(), + post_id: post._id.toString(), + depth: depth }) - .then(async post => { - if (post.author.equals(req.user._id)) { - // This post has been written by the logged in user - we good - var isCommunityPost = (post.type == 'community'); - var content = await helper.renderHTMLContent(post, true); - hbs.render('./views/partials/posteditormodal.handlebars', { - contentWarnings: post.contentWarnings, - privacy: post.privacy, - isCommunityPost: isCommunityPost, - isDraft: post.type=="draft", - postID: post._id.toString() - }) - .then(async html => { - var result = { - editor: html, - content: content - } - res.contentType('json'); - res.send(JSON.stringify(result)); - }) - } else { - res.send('Hold up there scout') + .then(async html => { + var result = { + comment: html } - }) - }) - - app.post('/saveedits/:postid', isLoggedInOrErrorResponse, async function(req, res) { - var post = await Post.findById(req.params.postid); - if (!post.author._id.equals(req.user._id)) { - return res.sendStatus(403); - } - var parsedPost = await helper.parseText(JSON.parse(req.body.postContent)); - if (!(parsedPost.inlineElements.length || parsedPost.text.trim())) { - //ignore the edit if it results in an empty post; it's an invalid request, the client side code also works to prevent this - return res.sendStatus(403); + res.contentType('json') + res.send(JSON.stringify(result)) + }) + }) + .catch((err) => { + console.log('Database error: ' + err) + }) + }) + }) + + // Responds to post requests that delete comments. + // Input: postid and commentid. + // Output: deletes each of the comment's images and removes the comment's document from the post. Then, updates the post's lastUpdated field to be + // that of the new most recent comment's (or the time of the post's creation if there are no comments left) with the relocatePost function. Also + // updates numberOfComments. + app.post('/deletecomment/:postid/:commentid', isLoggedInOrRedirect, function (req, res) { + Post.findOne({ _id: req.params.postid }) + .then((post) => { + var commentsByUser = 0 + var latestTimestamp = 0 + var numberOfComments = 0 + var target = undefined + + function findNested (array, id, parent) { + array.forEach((element) => { + if (!element.deleted) { + numberOfComments++ + } + if ((element.author.toString() == req.user._id.toString()) && !element.deleted) { + commentsByUser++ + } + if (element.timestamp > latestTimestamp) { + latestTimestamp = element.timestamp + } + element.numberOfSiblings = (parent.replies ? parent.replies.length - 1 : post.comments.length - 1) + element.parent = parent + if (!target && element._id && element._id.equals(id)) { + target = element + commentsByUser-- + numberOfComments-- + console.log('numberOfComments', numberOfComments) + } + if (element.replies) { + findNested(element.replies, id, element) + } + }) } - post.lastEdited = new Date(); - post.rawContent = req.body.postContent; - post.parsedContent = parsedPost.text; + findNested(post.comments, req.params.commentid, post) + if (target) { + post.numberOfComments = numberOfComments + } - //don't save mentions or tags for draft posts, this means that notifications and tag adding will be deferred until the post is published and at that point - //all of the mentions and tags will register as "new" and so the right actions will occur then - if(req.body.isDraft){ - parsedPost.mentions = []; - parsedPost.tags = []; + // i'll be impressed if someone trips this one, comment ids aren't displayed for comments that the logged in user didn't make + if (!target.author.equals(req.user._id) && post.author.toString() != req.user._id.toString()) { + res.status(400).send("you do not appear to be who you would like us to think that you are! this comment ain't got your brand on it") + return } - //process images added to/deleted from the post, retrieve/find orientations. its spaghetti, i know. the imageIsVertical and imageIsHorizontal database fields should - //really be combined into a single imageOrientationType field that just stores either 'vertical-image', 'horizontal-image', or a blank string for each image. - //it would be a lot simpler, if computationally wasteful, to just re-compute the orientation of every image for every image instead of doing all of this to keep track - //of how it was stored for images already in the post in potentially a completely different format and image order; if this code gives too much trouble probably just switch to that. - - //create lookup tables: oldHorizontalImages[imageFileName] will contain the value of imageIsHorizontal corresponding to that filename, and the same for vertical ones - var horizontalityLookup = {}; - var verticalityLookup = {}; - var oldPostImages = []; - if (post.images && post.images.length) { - oldPostImages = post.images; - if (post.imageIsHorizontal && post.imageIsVertical) { - oldPostImages.map((v, i) => { - horizontalityLookup[v] = post.imageIsHorizontal[i]; - verticalityLookup[v] = post.imageIsVertical[i]; + if (target.images && target.images.length) { + for (const image of target.images) { + fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { + if (err) console.log('Image deletion error ' + err) + }) + Image.deleteOne({ filename: image }) + } + } else if (target.inlineElements && target.inlineElements.length) { + for (const ie of target.inlineElements) { + if (ie.type == 'image(s)') { + for (const image of ie.images) { + fs.unlink(global.appRoot + '/cdn/images/' + image, (err) => { + if (err) console.log('Image deletion error ' + err) }) + Image.deleteOne({ filename: image }) + } } - } else if (post.inlineElements && post.inlineElements.length) { - post.inlineElements.filter(element => element.type == "image(s)").map(imagesElement => imagesElement.images.map( - (imageFilename, i) => { - oldPostImages.push(imageFilename); - if (imagesElement.imageIsHorizontal && imagesElement.imageIsVertical) { - horizontalityLookup[imageFilename] = imagesElement.imageIsHorizontal[i]; - verticalityLookup[imageFilename] = imagesElement.imageIsVertical[i]; - } - })) + } } - if (post.community) { - var imagePrivacy = (await Community.findById(post.community)).settings.visibility; - var imagePrivacyChanged = false; - } else if(req.body.isDraft){ - var imagePrivacy = "private"; - var imagePrivacyChanged = false; + + // Check if target has children + if (target.replies && target.replies.length) { + // We feel sorry for the children - just wipe the target's memory + target.parsedContent = '' + target.rawContent = '' + target.deleted = true } else { - var imagePrivacy = req.body.postPrivacy; - var imagePrivacyChanged = !(imagePrivacy == post.privacy); + // There are no children, the target can be destroyed + target.remove() + if (target.numberOfSiblings == 0 && target.parent.deleted) { + // There are also no siblings, and the element's parent + // has been deleted, so we can even destroy that! + target.parent.remove() + } } - //finalize each new image with the helper function; retrieve the orientation of the already existing ones from the lookup table by their filename. - //change the privacy of old image documents if the post's privacy changed. - var currentPostImages = []; - for (e of parsedPost.inlineElements) { - if (e.type == "image(s)") { - e.imageIsVertical = []; - e.imageIsHorizontal = []; - for (var i = 0; i < e.images.length; i++) { - currentPostImages.push(e.images[i]); - if (!oldPostImages.includes(e.images[i])) { - var horizOrVertic = await helper.finalizeImages([e.images[i]], post.type, post.community, req.user._id.toString(), imagePrivacy, req.user.settings.imageQuality); - e.imageIsVertical.push(horizOrVertic.imageIsVertical[0]); - e.imageIsHorizontal.push(horizOrVertic.imageIsHorizontal[0]); - } else if (!post.imageVersion || post.imageVersion < 2) { - //finalize images that were previously stored in /public/images/uploads so that there's only one url scheme that needs to be used with inlineElements. - var horizOrVertic = await helper.finalizeImages([e.images[i]], post.type, post.community, req.user._id.toString(), imagePrivacy, req.user.settings.imageQuality, global.appRoot + '/public/images/uploads/'); - e.imageIsVertical.push(horizOrVertic.imageIsVertical[0]); - e.imageIsHorizontal.push(horizOrVertic.imageIsHorizontal[0]); - } else { - e.imageIsVertical.push(verticalityLookup[e.images[i]]); - e.imageIsHorizontal.push(horizontalityLookup[e.images[i]]); - if (imagePrivacyChanged) { - var imageDoc = await Image.findOne({ filename: e.images[i] }); - imageDoc.privacy = imagePrivacy; - await imageDoc.save(); - } - } - } + + post.save() + .then((comment) => { + post.lastUpdated = latestTimestamp + // unsubscribe the author of the deleted comment from the post if they have no other comments on it + if (commentsByUser == 0) { + post.subscribedUsers = post.subscribedUsers.filter((v, i, a) => { + return v != req.user._id.toString() + }) + post.save().catch(err => { + console.error(err) + }) + } + // if (!target.some((v, i, a) => { + // return v.author.toString() == req.user._id.toString(); + // })) { + // post.subscribedUsers = post.subscribedUsers.filter((v, i, a) => { + // return v != req.user._id.toString(); + // }) + // post.save().catch(err => { + // console.error(err) + // }) + // } + result = { + numberOfComments: numberOfComments } + res.contentType('json').send(JSON.stringify(result)) + }) + .catch((error) => { + console.error(error) + }) + }) + }) + + // Responds to a post request that boosts a post. + // Inputs: id of the post to be boosted + // Outputs: a new post of type boost, adds the id of that new post into the boosts field of the old post, sends a notification to the + // user whose post was boosted. + app.post('/createboost/:postid', isLoggedInOrRedirect, function (req, res) { + boostedTimestamp = new Date() + Post.findOne({ + _id: req.params.postid + }, { + boostsV2: 1, + lastUpdated: 1, + privacy: 1, + unsubscribedUsers: 1, + author: 1, + url: 1 + }).populate('author') + .then((boostedPost) => { + if (boostedPost.privacy != 'public' || boostedPost.type == 'community') { + res.status(400).send('post is not public and therefore may not be boosted') + return } + var boost = new Post({ + type: 'boost', + authorEmail: req.user.email, + author: req.user._id, + url: shortid.generate(), + privacy: 'public', + timestamp: boostedTimestamp, + lastUpdated: boostedTimestamp, + boostTarget: boostedPost._id + }) + boost.save().then(savedBoost => { + const boost = { + booster: req.user._id, + timestamp: boostedTimestamp, + boost: savedBoost._id + } + boostedPost.boostsV2 = boostedPost.boostsV2.filter(boost => { + return !boost.booster.equals(req.user._id) + }) + boostedPost.boostsV2.push(boost) + + boostedPost.save().then(() => { + // don't notify the original post's author if they're creating the boost or are unsubscribed from this post + if (!boostedPost.unsubscribedUsers.includes(boostedPost.author._id.toString()) && !boostedPost.author._id.equals(req.user._id)) { + notifier.notify('user', 'boost', boostedPost.author._id, req.user._id, null, '/' + boostedPost.author.username + '/' + boostedPost.url, 'post') + } + res.redirect('back') + }) + }) + }) + }) + + // Responds to a post request that boosts a post. + // Inputs: id of the post to be boosted + // Outputs: a new post of type boost, adds the id of that new post into the boosts field of the old post, sends a notification to the + // user whose post was boosted. + app.post('/removeboost/:postid', isLoggedInOrRedirect, function (req, res) { + Post.findOne({ _id: req.params.postid }, { boostsV2: 1, privacy: 1, author: 1, url: 1, timestamp: 1 }) + .then((boostedPost) => { + var boost = boostedPost.boostsV2.find(b => { + return b.booster.equals(req.user._id) + }) + boostedPost.boostsV2 = boostedPost.boostsV2.filter(boost => { + return !boost.booster.equals(req.user._id) + }) + Post.deleteOne({ + _id: boost.boost + }, function () { + console.log('delete') + }) + boostedPost.save().then(() => { + res.redirect('back') + }) + }) + }) - var deletedImages = oldPostImages.filter(v => !currentPostImages.includes(v)); - for (image of deletedImages) { - Image.deleteOne({ filename: image }); - fs.unlink(global.appRoot + ((!post.imageVersion || post.imageVersion < 2) ? '/public/images/uploads/' : '/cdn/images/') + image, (err) => { if(err){console.error('could not delete unused image from edited post:\n' + err) }}); + app.post('/createposteditor/:postid', isLoggedInOrRedirect, function (req, res) { + Post.findOne({ + _id: req.params.postid + }) + .then(async post => { + if (post.author.equals(req.user._id)) { + // This post has been written by the logged in user - we good + var isCommunityPost = (post.type == 'community') + var content = await helper.renderHTMLContent(post, true) + hbs.render('./views/partials/posteditormodal.handlebars', { + contentWarnings: post.contentWarnings, + privacy: post.privacy, + isCommunityPost: isCommunityPost, + isDraft: post.type == 'draft', + postID: post._id.toString() + }) + .then(async html => { + var result = { + editor: html, + content: content + } + res.contentType('json') + res.send(JSON.stringify(result)) + }) + } else { + res.send('Hold up there scout') } + }) + }) - post.inlineElements = parsedPost.inlineElements; - post.imageVersion = 3; - post.images = undefined; - post.imageDescriptions = undefined; - post.imageIsHorizontal = undefined; - post.imageIsVertical = undefined; - post.embeds = undefined; - - var newMentions = parsedPost.mentions.filter(v => !post.mentions.includes(v)); - for (mention of newMentions) { - if (mention != req.user.username) { - User.findOne({ username: mention }).then(async mentioned => { - if (post.community) { - if (mentioned.communities.some(v=>v.equals(post.community))) { - notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + post.url, 'post'); - } - } else if (req.body.postPrivacy == "private") { - if (await Relationship.findOne({ value: 'trust', fromUser: req.user._id, toUser: mentioned._id })) { - notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + post.url, 'post'); - } - } else { - notifier.notify('user', 'mention', mentioned._id, req.user._id, post._id, '/' + req.user.username + '/' + post.url, 'post') - } - }) + app.post('/saveedits/:postid', isLoggedInOrErrorResponse, async function (req, res) { + var post = await Post.findById(req.params.postid) + if (!post.author._id.equals(req.user._id)) { + return res.sendStatus(403) + } + var parsedPost = await helper.parseText(JSON.parse(req.body.postContent)) + if (!(parsedPost.inlineElements.length || parsedPost.text.trim())) { + // ignore the edit if it results in an empty post; it's an invalid request, the client side code also works to prevent this + return res.sendStatus(403) + } + + post.lastEdited = new Date() + post.rawContent = req.body.postContent + post.parsedContent = parsedPost.text + + // don't save mentions or tags for draft posts, this means that notifications and tag adding will be deferred until the post is published and at that point + // all of the mentions and tags will register as "new" and so the right actions will occur then + if (req.body.isDraft) { + parsedPost.mentions = [] + parsedPost.tags = [] + } + + // process images added to/deleted from the post, retrieve/find orientations. its spaghetti, i know. the imageIsVertical and imageIsHorizontal database fields should + // really be combined into a single imageOrientationType field that just stores either 'vertical-image', 'horizontal-image', or a blank string for each image. + // it would be a lot simpler, if computationally wasteful, to just re-compute the orientation of every image for every image instead of doing all of this to keep track + // of how it was stored for images already in the post in potentially a completely different format and image order; if this code gives too much trouble probably just switch to that. + + // create lookup tables: oldHorizontalImages[imageFileName] will contain the value of imageIsHorizontal corresponding to that filename, and the same for vertical ones + var horizontalityLookup = {} + var verticalityLookup = {} + var oldPostImages = [] + if (post.images && post.images.length) { + oldPostImages = post.images + if (post.imageIsHorizontal && post.imageIsVertical) { + oldPostImages.map((v, i) => { + horizontalityLookup[v] = post.imageIsHorizontal[i] + verticalityLookup[v] = post.imageIsVertical[i] + }) + } + } else if (post.inlineElements && post.inlineElements.length) { + post.inlineElements.filter(element => element.type == 'image(s)').map(imagesElement => imagesElement.images.map( + (imageFilename, i) => { + oldPostImages.push(imageFilename) + if (imagesElement.imageIsHorizontal && imagesElement.imageIsVertical) { + horizontalityLookup[imageFilename] = imagesElement.imageIsHorizontal[i] + verticalityLookup[imageFilename] = imagesElement.imageIsVertical[i] + } + })) + } + if (post.community) { + var imagePrivacy = (await Community.findById(post.community)).settings.visibility + var imagePrivacyChanged = false + } else if (req.body.isDraft) { + var imagePrivacy = 'private' + var imagePrivacyChanged = false + } else { + var imagePrivacy = req.body.postPrivacy + var imagePrivacyChanged = !(imagePrivacy == post.privacy) + } + // finalize each new image with the helper function; retrieve the orientation of the already existing ones from the lookup table by their filename. + // change the privacy of old image documents if the post's privacy changed. + var currentPostImages = [] + for (e of parsedPost.inlineElements) { + if (e.type == 'image(s)') { + e.imageIsVertical = [] + e.imageIsHorizontal = [] + for (var i = 0; i < e.images.length; i++) { + currentPostImages.push(e.images[i]) + if (!oldPostImages.includes(e.images[i])) { + var horizOrVertic = await helper.finalizeImages([e.images[i]], post.type, post.community, req.user._id.toString(), imagePrivacy, req.user.settings.imageQuality) + e.imageIsVertical.push(horizOrVertic.imageIsVertical[0]) + e.imageIsHorizontal.push(horizOrVertic.imageIsHorizontal[0]) + } else if (!post.imageVersion || post.imageVersion < 2) { + // finalize images that were previously stored in /public/images/uploads so that there's only one url scheme that needs to be used with inlineElements. + var horizOrVertic = await helper.finalizeImages([e.images[i]], post.type, post.community, req.user._id.toString(), imagePrivacy, req.user.settings.imageQuality, global.appRoot + '/public/images/uploads/') + e.imageIsVertical.push(horizOrVertic.imageIsVertical[0]) + e.imageIsHorizontal.push(horizOrVertic.imageIsHorizontal[0]) + } else { + e.imageIsVertical.push(verticalityLookup[e.images[i]]) + e.imageIsHorizontal.push(horizontalityLookup[e.images[i]]) + if (imagePrivacyChanged) { + var imageDoc = await Image.findOne({ filename: e.images[i] }) + imageDoc.privacy = imagePrivacy + await imageDoc.save() } + } } - post.mentions = parsedPost.mentions; + } + } - var newTags = parsedPost.tags.filter(v => !post.tags.includes(v)); - for (const tag of newTags) { - Tag.findOneAndUpdate({ name: tag }, { "$push": { "posts": post._id.toString() }, "$set": { "lastUpdated": new Date() } }, { upsert: true, new: true }, function(error, result) { if (error) console.error('could not update tag upon post editing\n' + error) }); - } - var deletedTags = post.tags.filter(v => !parsedPost.tags.includes(v)) - for (const tag of deletedTags) { - Tag.findOneAndUpdate({ name: tag }, { "$pull": { "posts": post._id.toString() } }).catch(err => console.error('could not remove edited post ' + post._id.toString() + ' from tag ' + tag + '\n' + err)); + var deletedImages = oldPostImages.filter(v => !currentPostImages.includes(v)) + for (image of deletedImages) { + Image.deleteOne({ filename: image }) + fs.unlink(global.appRoot + ((!post.imageVersion || post.imageVersion < 2) ? '/public/images/uploads/' : '/cdn/images/') + image, (err) => { if (err) { console.error('could not delete unused image from edited post:\n' + err) } }) + } - } - post.tags = parsedPost.tags; + post.inlineElements = parsedPost.inlineElements + post.imageVersion = 3 + post.images = undefined + post.imageDescriptions = undefined + post.imageIsHorizontal = undefined + post.imageIsVertical = undefined + post.embeds = undefined + + var newMentions = parsedPost.mentions.filter(v => !post.mentions.includes(v)) + for (mention of newMentions) { + if (mention != req.user.username) { + User.findOne({ username: mention }).then(async mentioned => { + if (post.community) { + if (mentioned.communities.some(v => v.equals(post.community))) { + notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + post.url, 'post') + } + } else if (req.body.postPrivacy == 'private') { + if (await Relationship.findOne({ value: 'trust', fromUser: req.user._id, toUser: mentioned._id })) { + notifier.notify('user', 'mention', mentioned._id, req.user._id, newPostId, '/' + req.user.username + '/' + post.url, 'post') + } + } else { + notifier.notify('user', 'mention', mentioned._id, req.user._id, post._id, '/' + req.user.username + '/' + post.url, 'post') + } + }) + } + } + post.mentions = parsedPost.mentions + + var newTags = parsedPost.tags.filter(v => !post.tags.includes(v)) + for (const tag of newTags) { + Tag.findOneAndUpdate({ name: tag }, { $push: { posts: post._id.toString() }, $set: { lastUpdated: new Date() } }, { upsert: true, new: true }, function (error, result) { if (error) console.error('could not update tag upon post editing\n' + error) }) + } + var deletedTags = post.tags.filter(v => !parsedPost.tags.includes(v)) + for (const tag of deletedTags) { + Tag.findOneAndUpdate({ name: tag }, { $pull: { posts: post._id.toString() } }).catch(err => console.error('could not remove edited post ' + post._id.toString() + ' from tag ' + tag + '\n' + err)) + } + post.tags = parsedPost.tags - //if the post is in a community, the community's last activity timestamp could be updated here, but maybe not, idk + // if the post is in a community, the community's last activity timestamp could be updated here, but maybe not, idk - var newHTML = await helper.renderHTMLContent(post); - post.cachedHTML.fullContentHTML = newHTML; + var newHTML = await helper.renderHTMLContent(post) + post.cachedHTML.fullContentHTML = newHTML - post.contentWarnings = req.body.postContentWarnings; + post.contentWarnings = req.body.postContentWarnings - if (req.body.postContentWarnings) { - //this bit does not need to be stored in the database, it's rendered in the feed by the posts_v2 handlebars file and that's fine - newHTML = '' + + if (req.body.postContentWarnings) { + // this bit does not need to be stored in the database, it's rendered in the feed by the posts_v2 handlebars file and that's fine + newHTML = '' + '
' + newHTML + '
' + - ''; - } + '' + } - if(post.type == "draft" && !req.body.isDraft){ - var timePublished = new Date(); - post.timestamp = timePublished; - post.lastUpdated = timePublished; - post.lastEdited = undefined; - post.type = "original"; - newHTML = '

This post was published!

'; //the color is $sweet-red from _colors.scss - } + if (post.type == 'draft' && !req.body.isDraft) { + var timePublished = new Date() + post.timestamp = timePublished + post.lastUpdated = timePublished + post.lastEdited = undefined + post.type = 'original' + newHTML = '

This post was published!

' // the color is $sweet-red from _colors.scss + } - if (post.type == "original") { - post.privacy = req.body.postPrivacy; - }else if(post.type == "draft"){ - post.privacy = "private"; //the client should send is this in req.body.postPrivacy but, just to be sure - }else if(post.type == "community"){ - post.privacy = "public"; - } + if (post.type == 'original') { + post.privacy = req.body.postPrivacy + } else if (post.type == 'draft') { + post.privacy = 'private' // the client should send is this in req.body.postPrivacy but, just to be sure + } else if (post.type == 'community') { + post.privacy = 'public' + } + + post.save().then(() => { + res.contentType('text/html; charset=utf-8') + res.status(200) + res.send(newHTML) + }) + }) +} - post.save().then(() => { - res.contentType("text/html; charset=utf-8"); - res.status(200); - res.send(newHTML); +// scan the temp folder every day and delete images that are more than a week old. just in case someone leaves their post editor open for that long. this generous +// time limit is enabled by the fact that the vast, vast majority of temp images are going to be specifically cleared by the /cleartempimage route above; the only +// images that are going to be picked up by this function are those uploaded by users whose device loses power, whose device loses internet connection and doesn't +// regain it before they close the tab, and maybe those that are using a really weird browser or extensions. +function cleanTempFolder () { + fs.readdir('./cdn/images/temp', function (err, files) { + files.forEach(file => { + if (file != '.gitkeep' && file != '') { + fs.stat('./cdn/images/temp/' + file, function (err, s) { + if (Date.now() - s.mtimeMs > 7 * 24 * 60 * 60 * 1000) { + fs.unlink('./cdn/images/temp/' + file, function (e) { + if (e) { + console.log("couldn't clean temp file " + file) + console.log(e) + } + }) + } }) + } }) -}; - -//scan the temp folder every day and delete images that are more than a week old. just in case someone leaves their post editor open for that long. this generous -//time limit is enabled by the fact that the vast, vast majority of temp images are going to be specifically cleared by the /cleartempimage route above; the only -//images that are going to be picked up by this function are those uploaded by users whose device loses power, whose device loses internet connection and doesn't -//regain it before they close the tab, and maybe those that are using a really weird browser or extensions. -function cleanTempFolder() { - fs.readdir("./cdn/images/temp", function(err, files) { - files.forEach(file => { - if (file != ".gitkeep" && file != "") { - fs.stat("./cdn/images/temp/" + file, function(err, s) { - if (Date.now() - s.mtimeMs > 7*24*60*60*1000) { - fs.unlink("./cdn/images/temp/" + file, function(e) { - if (e) { - console.log("couldn't clean temp file " + file); - console.log(e); - } - }) - } - }) - } - }); - }); + }) } -cleanTempFolder(); -setInterval(cleanTempFolder, 24*60*60*1000); - -//For post and get requests where the browser will handle the response automatically and so redirects will work -function isLoggedInOrRedirect(req, res, next) { - if (req.isAuthenticated()) { - // A potentially expensive way to update a user's last logged in timestamp (currently only relevant to sorting search results) - currentTime = new Date(); - if ((currentTime - req.user.lastUpdated) > 3600000) { // If the timestamp is older than an hour - User.findOne({ - _id: req.user._id - }) - .then(user => { - user.lastUpdated = currentTime; - user.save() - }) - } - return next(); +cleanTempFolder() +setInterval(cleanTempFolder, 24 * 60 * 60 * 1000) + +// For post and get requests where the browser will handle the response automatically and so redirects will work +function isLoggedInOrRedirect (req, res, next) { + if (req.isAuthenticated()) { + // A potentially expensive way to update a user's last logged in timestamp (currently only relevant to sorting search results) + currentTime = new Date() + if ((currentTime - req.user.lastUpdated) > 3600000) { // If the timestamp is older than an hour + User.findOne({ + _id: req.user._id + }) + .then(user => { + user.lastUpdated = currentTime + user.save() + }) } - res.redirect('/'); - //next('route'); don't want this! the request has been handled by the redirect, we don't need to do anything else with it in another route + return next() + } + res.redirect('/') + // next('route'); don't want this! the request has been handled by the redirect, we don't need to do anything else with it in another route } -//For post requests where the jQuery code making the request will handle the response -function isLoggedInOrErrorResponse(req, res, next) { - if (req.isAuthenticated()) { - return next(); - } - res.send('nope'); - //next('route'); don't want this! the request has been handled by the error response, we don't need to do anything else with it in another route +// For post requests where the jQuery code making the request will handle the response +function isLoggedInOrErrorResponse (req, res, next) { + if (req.isAuthenticated()) { + return next() + } + res.send('nope') + // next('route'); don't want this! the request has been handled by the error response, we don't need to do anything else with it in another route } diff --git a/app/statisticsTracker.js b/app/statisticsTracker.js index 35047fd8..6994f8d3 100644 --- a/app/statisticsTracker.js +++ b/app/statisticsTracker.js @@ -1,621 +1,617 @@ module.exports = function (app, mongoose) { - - //Fun stats tracker. Non-interactive. - var cameOnlineAt = new Date(); - app.get('/admin/juststats', function (req, res) { - var currentTime = new Date(); - var uptime = new Date(currentTime - cameOnlineAt).toISOString().slice(11, -1); - Post.countDocuments({}).then(numberOfPosts => { - Image.countDocuments({}).then(numberOfImages => { - mongoose.connection.db.collection('sessions', (err, collection) => { - collection.find().toArray(function (err, activeSessions) { - var numberOfActiveSessions = activeSessions.length; - var activeusers = []; - for (sesh of activeSessions) { - var sessiondata = JSON.parse(sesh.session); - if (sessiondata.passport) { - if (!activeusers.includes(sessiondata.passport.user)) { - activeusers.push(sessiondata.passport.user); - } - } - } - var uniqueActiveSessions = activeusers.length; - Post.find({ - timestamp: { - $gte: new Date(new Date().setDate(new Date().getDate() - 1)) - } - }).then(posts => { - var funstats = - "Uptime " + uptime + "
" + - "Active sessions " + numberOfActiveSessions + "
" + - "Unique active users " + uniqueActiveSessions + - "
" + - "
Total
" + - "Posts " + numberOfPosts + "
" + - "Images " + numberOfImages + "
" + - "
" + - "
Last 24 hours
" + - "Posts " + posts.length + "
" + - "
"; - res.status(200).send(funstats); - }) - }) - }) - }) - }) - }) - - app.get('/admin/stats', function (req, res) { - if (req.isAuthenticated()) { - res.render('asyncPage', { - getUrl: ["/admin/juststats"], - loggedIn: true, - loggedInUserData: req.user - }); - } else { - res.render('asyncPage', { - getUrl: ["/admin/juststats"], - loggedIn: false - }); - } - }) - - //the graph may take a hot second to render, so when you navigate to the page that displays it - //you get a little thing that tells you to wait for a second and then the page makes a request to - //the getUrl path which leads to the route below. - app.get("/admin/postgraph", function (req, res) { - if (req.isAuthenticated()) { - res.render('asyncPage', { - getUrl: ["/admin/justpostgraph"], - loggedIn: true, - loggedInUserData: req.user - }); - } else { - res.render('asyncPage', { - getUrl: ["/admin/justpostgraph"], - loggedIn: false - }); - } - }); - - app.get("/admin/usergraph", function (req, res) { - if (req.isAuthenticated()) { - res.render('asyncPage', { - getUrl: ["/admin/justusergraph"], - loggedIn: true, - loggedInUserData: req.user - }); - } else { - res.render('asyncPage', { - getUrl: ["/admin/justusergraph"], - loggedIn: false - }); - } - }) - - app.get("/admin/allstats", function (req, res) { - if (req.isAuthenticated()) { - res.render('asyncPage', { - getUrl: ["/admin/juststats", "/admin/justpostgraph", "/admin/justusergraph"], - loggedIn: true, - loggedInUserData: req.user - }); - } else { - res.render('asyncPage', { - getUrl: ["/admin/juststats", "/admin/justpostgraph", "/admin/justusergraph"], - loggedIn: false - }); - } - }) - - //storing a promise for the post table building function out here lets us check if the function is currently running so we don't call it again if it is - var postTablePromise = null; - //this function just checks if the file with the post totals by day exists and is up to date and then builds the graph from it if it does and is - //and calls the function that creates the csv and waits for it to finish if it doesn't or isn't. - app.get("/admin/justpostgraph", async function (req, res) { - if (userTablePromise) { - await userTablePromise; - } - var mostRecentDate; - if (!fs.existsSync(postTableFileName)) { - if (!postTablePromise) { - postTablePromise = rebuildPostTable(); - } - //note that this is an assignment statement, not a comparison. tableNotUpToDate will give us false if it is up to date or the last line - //of the file (in split/array form) otherwise, which we save in mostRecentDate to pass to rebuildPostTable so it knows where to start building from - } else if (mostRecentDate = tableNotUpToDate(postTableFileName)) { - if (!postTablePromise) { - postTablePromise = rebuildPostTable(mostRecentDate); - } - } - await postTablePromise; - postTablePromise = null; - var datapoints = await parseTableForGraph(postTableFileName, Post); - datapoints.label = "cumulative sweet posts"; - datapoints.color = "rgb(75, 192, 192)"; - var dxdatapoints = getPerDayRate(datapoints); - dxdatapoints.label = "sweet posts added per day"; - dxdatapoints.color = "rgb(192,75,192)"; - res.render('partials/timeGraph', { - layout: false, - chartName: "postGraph", - datapoint: [datapoints, dxdatapoints] - }) - }) - - //storing a promise for the user table building function out here lets us check if the function is currently running so we don't call it again if it is - var userTablePromise = null; - //this function just checks if the file with the user totals by day exists and is up to date and then builds the graph from it if it does and is - //and calls the function that creates the csv and waits for it to finish if it doesn't or isn't. - app.get("/admin/justusergraph", async function (req, res) { - if (postTablePromise) { - await postTablePromise; - } - var mostRecentDate; - if (!fs.existsSync(userTableFileName)) { - if (!userTablePromise) { - userTablePromise = rebuildUserTable(); - } - //note that this is an assignment statement, not a comparison. tableNotUpToDate will give us false if it is up to date or the last line - //of the file (in split/array form) otherwise, which we save in mostRecentDate to pass to rebuildUserTable so it knows where to start building from - } else if (mostRecentDate = tableNotUpToDate(userTableFileName)) { - if (!userTablePromise) { - userTablePromise = rebuildUserTable(mostRecentDate); - } - } - await userTablePromise; - userTablePromise = null; - var datapoints = await parseTableForGraph(userTableFileName, User); - datapoints.label = "cumulative sweet users"; - datapoints.color = "rgb(75, 192, 192)"; - var dxdatapoints = getPerDayRate(datapoints); - dxdatapoints.label = "sweet users added per day"; - dxdatapoints.color = "rgb(192,75,192)"; - res.render('partials/timeGraph', { - layout: false, - chartName: "userGraph", - datapoint: [datapoints, dxdatapoints] - }) - }) - - var activeUsersTablePromise = undefined; - app.get("/admin/justactiveusersgraph", async function (req, res) { - var mostRecentDate; - if (!fs.existsSync(activeUserTableFileName)) { - if (!activeUsersTablePromise) { - activeUsersTablePromise = rebuildActiveUsersTable(); - } - //note that this is an assignment statement, not a comparison. tableNotUpToDate will give us false if it is up to date or the last line - //of the file (in split/array form) otherwise, which we save in mostRecentDate to pass to rebuildUserTable so it knows where to start building from - } else if (mostRecentDate = tableNotUpToDate(activeUserTableFileName, 3)) { - if (!activeUsersTablePromise) { - activeUsersTablePromise = rebuildActiveUsersTable(mostRecentDate); + // Fun stats tracker. Non-interactive. + var cameOnlineAt = new Date() + app.get('/admin/juststats', function (req, res) { + var currentTime = new Date() + var uptime = new Date(currentTime - cameOnlineAt).toISOString().slice(11, -1) + Post.countDocuments({}).then(numberOfPosts => { + Image.countDocuments({}).then(numberOfImages => { + mongoose.connection.db.collection('sessions', (err, collection) => { + collection.find().toArray(function (err, activeSessions) { + var numberOfActiveSessions = activeSessions.length + var activeusers = [] + for (sesh of activeSessions) { + var sessiondata = JSON.parse(sesh.session) + if (sessiondata.passport) { + if (!activeusers.includes(sessiondata.passport.user)) { + activeusers.push(sessiondata.passport.user) + } + } } - } - await activeUsersTablePromise; - activeUsersTablePromise = undefined; - var datapoints = await parseTableForGraph(activeUserTableFileName, null, getActiveUsersSinceLastSave); - datapoints.label = "active sweet users during 3-day intervals"; - datapoints.color = "rgb(75, 192, 192)"; - res.render('partials/timeGraph', { - layout: false, - chartName: "activeUsersGraph", - datapoint: [datapoints] + var uniqueActiveSessions = activeusers.length + Post.find({ + timestamp: { + $gte: new Date(new Date().setDate(new Date().getDate() - 1)) + } + }).then(posts => { + var funstats = + 'Uptime ' + uptime + '
' + + 'Active sessions ' + numberOfActiveSessions + '
' + + 'Unique active users ' + uniqueActiveSessions + + '
' + + '
Total
' + + 'Posts ' + numberOfPosts + '
' + + 'Images ' + numberOfImages + '
' + + '
' + + '
Last 24 hours
' + + 'Posts ' + posts.length + '
' + + '
' + res.status(200).send(funstats) + }) + }) }) + }) }) - - app.get("/admin/activeusersgraph", function (req, res) { - if (req.isAuthenticated()) { - res.render('asyncPage', { - getUrl: ["/admin/justactiveusersgraph"], - loggedIn: true, - loggedInUserData: req.user - }); - } else { - res.render('asyncPage', { - getUrl: ["/admin/justactiveusersgraph"], - loggedIn: false - }); - } - }) - - app.get("/admin/resetgraphs/:password", function (req, res) { - var passwordHash = "$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq" - if (req.isAuthenticated() && bcrypt.compareSync(req.params.password, passwordHash)) { - if (!postTablePromise && fs.existsSync(postTableFileName)) { - fs.unlinkSync(path.resolve(global.appRoot, postTableFileName)); - } - if (!userTablePromise && fs.existsSync(userTableFileName)) { - fs.unlinkSync(path.resolve(global.appRoot, userTableFileName)); - } - if (!activeUsersTablePromise && fs.existsSync(activeUserTableFileName)) { - fs.unlinkSync(path.resolve(global.appRoot, activeUserTableFileName)); - } - - res.status(200).send("thy will be done"); - } else { - res.status(200).send("no dice") - } - }) - - app.get("/admin/secretstuff/:password/lyds.txt", function (req, res) { - var passwordHash = "$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq"; - if (bcrypt.compareSync(req.params.password, passwordHash) && fs.existsSync(path.resolve(global.appRoot, "lyds.txt"))) { - res.status(200).sendFile(path.resolve(global.appRoot, "lyds.txt")); - } - }) -}; - -var postTableFileName = "postTimeline.csv"; -var userTableFileName = "userTimeline.csv"; -var activeUserTableFileName = "activeUsersTimeline.csv"; - -//Checks if the last line of the table file is for a day more recent than dateInterval days ago. -function tableNotUpToDate(tableFilename, dateInterval = 1) { - var lastLine = ""; - fs.readFileSync(tableFilename, 'utf-8').split('\n').forEach(function (line) { - if (line && line != "\n") { - lastLine = line; - } - }); - var shouldBeInThere = new Date() - shouldBeInThere.setDate(shouldBeInThere.getDate() - dateInterval); - if (new Date(lastLine.split(",")[0]).getTime() > shouldBeInThere.getTime()) { - return false; + }) + + app.get('/admin/stats', function (req, res) { + if (req.isAuthenticated()) { + res.render('asyncPage', { + getUrl: ['/admin/juststats'], + loggedIn: true, + loggedInUserData: req.user + }) } else { - //return the last line so the calling function knows what the most recent date saved is and thus what dates to update with - return lastLine.split(","); + res.render('asyncPage', { + getUrl: ['/admin/juststats'], + loggedIn: false + }) } -} - -//Creates a file that contains dates and the number of posts that were stored by that date. Starts from the earliest post or the -//last line in the existing file (startDate). This function figures out the date range that our posts are in and then saves the number of posts that -//were created as of that day into the postCountByDay array. Then we write all the dates into our file. Then we're done. -async function rebuildPostTable(startDate) { - //if we're rebuilding (which means we're starting from the earliest post and don't have a startDate), we throw out any existing old version of the file. - if (fs.existsSync(postTableFileName) && !startDate) { - fs.unlinkSync(path.resolve(global.appRoot, postTableFileName)); + }) + + // the graph may take a hot second to render, so when you navigate to the page that displays it + // you get a little thing that tells you to wait for a second and then the page makes a request to + // the getUrl path which leads to the route below. + app.get('/admin/postgraph', function (req, res) { + if (req.isAuthenticated()) { + res.render('asyncPage', { + getUrl: ['/admin/justpostgraph'], + loggedIn: true, + loggedInUserData: req.user + }) + } else { + res.render('asyncPage', { + getUrl: ['/admin/justpostgraph'], + loggedIn: false + }) } - - var today = new Date(new Date().setDate(new Date().getDate() - 1)); - today.setHours(23) - today.setMinutes(59); - today.setSeconds(59); - today.setMilliseconds(999); //this actually sets today to the last millisecond that counts as yesterday. that's the most recent data we're looking at for end-of-day totals. - - //before will store the end of day time upon which we'll base our first end-of-day total. - var before; - if (!startDate) { - await Post.find({ - timestamp: { - $exists: true - } - }).sort('timestamp').then(async posts => { - before = new Date(posts[0].timestamp.getFullYear(), posts[0].timestamp.getMonth(), posts[0].timestamp.getDate(), 23, 59, 59, 999); - }) + }) + + app.get('/admin/usergraph', function (req, res) { + if (req.isAuthenticated()) { + res.render('asyncPage', { + getUrl: ['/admin/justusergraph'], + loggedIn: true, + loggedInUserData: req.user + }) } else { - //if we have it, startDate is passed in as the components of the last line (describing the most recent saved date) from the existing file. - before = new Date(startDate[1], startDate[2], parseInt(startDate[3]) + 1, 23, 59, 59, 999); //start with the day after the most recently saved one (hence the plus one) + res.render('asyncPage', { + getUrl: ['/admin/justusergraph'], + loggedIn: false + }) } - - var totalDays = (today.getTime() - before.getTime()) / (24 * 60 * 60 * 1000) + 1; //it's plus one to account for the day before the date stored by before - - var postCountByDay = []; - - //populate postCountByDay with date objects that also have a property indicating what the post count was at the end of that day - for (var i = 0; i < totalDays; i++) { - var sequentialDate = new Date(before); - await Post.find({ - $or: [{ - timestamp: { - $lte: sequentialDate - } - }, { - timestamp: undefined - }] - }).then(posts => { - sequentialDate.postCount = posts.length; - postCountByDay.push(sequentialDate); - }) - before.setDate(before.getDate() + 1); + }) + + app.get('/admin/allstats', function (req, res) { + if (req.isAuthenticated()) { + res.render('asyncPage', { + getUrl: ['/admin/juststats', '/admin/justpostgraph', '/admin/justusergraph'], + loggedIn: true, + loggedInUserData: req.user + }) + } else { + res.render('asyncPage', { + getUrl: ['/admin/juststats', '/admin/justpostgraph', '/admin/justusergraph'], + loggedIn: false + }) } - - //Write each line in CSV format (so it can be opened in excel or openoffice calc or gnumeric.) - //Note that the file always ends with a \n, and this needs to be true for this code to work when appending new lines to the file - postCountByDay.forEach((date) => { - fs.appendFileSync(postTableFileName, date.toDateString() + "," + date.getFullYear() + "," + date.getMonth() + "," + date.getDate()); - fs.appendFileSync(postTableFileName, "," + date.postCount); - fs.appendFileSync(postTableFileName, "\n"); + }) + + // storing a promise for the post table building function out here lets us check if the function is currently running so we don't call it again if it is + var postTablePromise = null + // this function just checks if the file with the post totals by day exists and is up to date and then builds the graph from it if it does and is + // and calls the function that creates the csv and waits for it to finish if it doesn't or isn't. + app.get('/admin/justpostgraph', async function (req, res) { + if (userTablePromise) { + await userTablePromise + } + var mostRecentDate + if (!fs.existsSync(postTableFileName)) { + if (!postTablePromise) { + postTablePromise = rebuildPostTable() + } + // note that this is an assignment statement, not a comparison. tableNotUpToDate will give us false if it is up to date or the last line + // of the file (in split/array form) otherwise, which we save in mostRecentDate to pass to rebuildPostTable so it knows where to start building from + } else if (mostRecentDate = tableNotUpToDate(postTableFileName)) { + if (!postTablePromise) { + postTablePromise = rebuildPostTable(mostRecentDate) + } + } + await postTablePromise + postTablePromise = null + var datapoints = await parseTableForGraph(postTableFileName, Post) + datapoints.label = 'cumulative sweet posts' + datapoints.color = 'rgb(75, 192, 192)' + var dxdatapoints = getPerDayRate(datapoints) + dxdatapoints.label = 'sweet posts added per day' + dxdatapoints.color = 'rgb(192,75,192)' + res.render('partials/timeGraph', { + layout: false, + chartName: 'postGraph', + datapoint: [datapoints, dxdatapoints] }) - - //make way for next update/rebuild - postCountByDay = []; -} - -//Creates a file that contains dates and the number of users that were stored by that date. Starts from the earliest user or the -//last line in the existing file (startDate). This function figures out the date range that our users are in and then saves the number of users that -//were created as of that day into the userCountByDay array. Then we write all the dates into our file. Then we're done. -async function rebuildUserTable(startDate) { - //if we're rebuilding (which means we're starting from the earliest user and don't have a startDate), we throw out any existing old version of the file. - if (fs.existsSync(userTableFileName) && !startDate) { - fs.unlinkSync(path.resolve(global.appRoot, userTableFileName)); + }) + + // storing a promise for the user table building function out here lets us check if the function is currently running so we don't call it again if it is + var userTablePromise = null + // this function just checks if the file with the user totals by day exists and is up to date and then builds the graph from it if it does and is + // and calls the function that creates the csv and waits for it to finish if it doesn't or isn't. + app.get('/admin/justusergraph', async function (req, res) { + if (postTablePromise) { + await postTablePromise } - - var today = new Date(new Date().setDate(new Date().getDate() - 1)); - today.setHours(23) - today.setMinutes(59); - today.setSeconds(59); - today.setMilliseconds(999); //this actually sets today to the last millisecond that counts as yesterday. that's the most recent data we're looking at for end-of-day totals. - - //before will store the end of day time upon which we'll base our first end-of-day total. - var before; - if (!startDate) { - await User.find({ - joined: { - $exists: true - } - }).sort('joined').then(async users => { - before = new Date(users[0].joined.getFullYear(), users[0].joined.getMonth(), users[0].joined.getDate(), 23, 59, 59, 999); - }) - } else { - //if we have it, startDate is passed in as the components of the last line (describing the most recent saved date) from the existing file. - before = new Date(startDate[1], startDate[2], parseInt(startDate[3]) + 1, 23, 59, 59, 999); //start with the day after the most recently saved one (hence the plus one) + var mostRecentDate + if (!fs.existsSync(userTableFileName)) { + if (!userTablePromise) { + userTablePromise = rebuildUserTable() + } + // note that this is an assignment statement, not a comparison. tableNotUpToDate will give us false if it is up to date or the last line + // of the file (in split/array form) otherwise, which we save in mostRecentDate to pass to rebuildUserTable so it knows where to start building from + } else if (mostRecentDate = tableNotUpToDate(userTableFileName)) { + if (!userTablePromise) { + userTablePromise = rebuildUserTable(mostRecentDate) + } } - - var totalDays = (today.getTime() - before.getTime()) / (24 * 60 * 60 * 1000) + 1; //it's plus one to account for the day before the date stored by before - - var userCountByDay = []; - - //populate userCountByDay with date objects that also have a property indicating what the user count was at the end of that day - for (var i = 0; i < totalDays; i++) { - var sequentialDate = new Date(before); - await User.find({ - $or: [{ - joined: { - $lte: sequentialDate - } - }, { - joined: undefined - }] - }).then(users => { - sequentialDate.userCount = users.length; - userCountByDay.push(sequentialDate); - }) - before.setDate(before.getDate() + 1); + await userTablePromise + userTablePromise = null + var datapoints = await parseTableForGraph(userTableFileName, User) + datapoints.label = 'cumulative sweet users' + datapoints.color = 'rgb(75, 192, 192)' + var dxdatapoints = getPerDayRate(datapoints) + dxdatapoints.label = 'sweet users added per day' + dxdatapoints.color = 'rgb(192,75,192)' + res.render('partials/timeGraph', { + layout: false, + chartName: 'userGraph', + datapoint: [datapoints, dxdatapoints] + }) + }) + + var activeUsersTablePromise = undefined + app.get('/admin/justactiveusersgraph', async function (req, res) { + var mostRecentDate + if (!fs.existsSync(activeUserTableFileName)) { + if (!activeUsersTablePromise) { + activeUsersTablePromise = rebuildActiveUsersTable() + } + // note that this is an assignment statement, not a comparison. tableNotUpToDate will give us false if it is up to date or the last line + // of the file (in split/array form) otherwise, which we save in mostRecentDate to pass to rebuildUserTable so it knows where to start building from + } else if (mostRecentDate = tableNotUpToDate(activeUserTableFileName, 3)) { + if (!activeUsersTablePromise) { + activeUsersTablePromise = rebuildActiveUsersTable(mostRecentDate) + } } - - //Write each line in CSV format (so it can be opened in excel or openoffice calc or gnumeric.) - //Note that the file always ends with a \n, and this needs to be true for this code to work when appending new lines to the file - userCountByDay.forEach((date) => { - fs.appendFileSync(userTableFileName, date.toDateString() + "," + date.getFullYear() + "," + date.getMonth() + "," + date.getDate()); - fs.appendFileSync(userTableFileName, "," + date.userCount); - fs.appendFileSync(userTableFileName, "\n"); + await activeUsersTablePromise + activeUsersTablePromise = undefined + var datapoints = await parseTableForGraph(activeUserTableFileName, null, getActiveUsersSinceLastSave) + datapoints.label = 'active sweet users during 3-day intervals' + datapoints.color = 'rgb(75, 192, 192)' + res.render('partials/timeGraph', { + layout: false, + chartName: 'activeUsersGraph', + datapoint: [datapoints] }) + }) + + app.get('/admin/activeusersgraph', function (req, res) { + if (req.isAuthenticated()) { + res.render('asyncPage', { + getUrl: ['/admin/justactiveusersgraph'], + loggedIn: true, + loggedInUserData: req.user + }) + } else { + res.render('asyncPage', { + getUrl: ['/admin/justactiveusersgraph'], + loggedIn: false + }) + } + }) + + app.get('/admin/resetgraphs/:password', function (req, res) { + var passwordHash = '$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq' + if (req.isAuthenticated() && bcrypt.compareSync(req.params.password, passwordHash)) { + if (!postTablePromise && fs.existsSync(postTableFileName)) { + fs.unlinkSync(path.resolve(global.appRoot, postTableFileName)) + } + if (!userTablePromise && fs.existsSync(userTableFileName)) { + fs.unlinkSync(path.resolve(global.appRoot, userTableFileName)) + } + if (!activeUsersTablePromise && fs.existsSync(activeUserTableFileName)) { + fs.unlinkSync(path.resolve(global.appRoot, activeUserTableFileName)) + } + + res.status(200).send('thy will be done') + } else { + res.status(200).send('no dice') + } + }) - //make way for next update/rebuild - userCountByDay = []; + app.get('/admin/secretstuff/:password/lyds.txt', function (req, res) { + var passwordHash = '$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq' + if (bcrypt.compareSync(req.params.password, passwordHash) && fs.existsSync(path.resolve(global.appRoot, 'lyds.txt'))) { + res.status(200).sendFile(path.resolve(global.appRoot, 'lyds.txt')) + } + }) } +var postTableFileName = 'postTimeline.csv' +var userTableFileName = 'userTimeline.csv' +var activeUserTableFileName = 'activeUsersTimeline.csv' - -//this is called when the file is finished and it's time to turn it's csv data into json for handlebars to parse. there's probably a -//decent argument for saving a json file in the first place, huh. this also adds a datapoint representing the current date/time/post count, which -//should be current every time we build/display the graph. it either uses collection to query for the current y value or the callback we provide -async function parseTableForGraph(filename, collection, callbackForCurrentY, endOfDay = true) { - var jsonVersion = []; - //reads in file values - for (const line of fs.readFileSync(filename, 'utf-8').split('\n')) { - if (line && line !== "\n") { - var lineComps = line.split(","); - jsonVersion.push({ - label: lineComps[0], - year: lineComps[1], - month: lineComps[2], - date: lineComps[3], - hour: endOfDay ? 23 : 0, - minute: endOfDay ? 59 : 0, - second: endOfDay ? 59 : 0, - y: lineComps[4] - }); - } - }; - //add in a datapoint representing the current exact second, if we have a source to obtain it from - var now = new Date(); - if (collection) { - var numberOfDocs = await collection.countDocuments(); - } else if (callbackForCurrentY) { - var numberOfDocs = await callbackForCurrentY(); - } else { - return jsonVersion; +// Checks if the last line of the table file is for a day more recent than dateInterval days ago. +function tableNotUpToDate (tableFilename, dateInterval = 1) { + var lastLine = '' + fs.readFileSync(tableFilename, 'utf-8').split('\n').forEach(function (line) { + if (line && line != '\n') { + lastLine = line } - jsonVersion.push({ - label: numberOfDocs == 69 ? "nice" : now.toLocaleString(), - year: now.getFullYear(), - month: now.getMonth(), - date: now.getDate(), - hour: now.getHours(), - minute: now.getMinutes(), - second: now.getSeconds(), - y: numberOfDocs - }); - return jsonVersion; + }) + var shouldBeInThere = new Date() + shouldBeInThere.setDate(shouldBeInThere.getDate() - dateInterval) + if (new Date(lastLine.split(',')[0]).getTime() > shouldBeInThere.getTime()) { + return false + } else { + // return the last line so the calling function knows what the most recent date saved is and thus what dates to update with + return lastLine.split(',') + } } -//this takes the datpoints type thing above and takes the derivative by subtracting the last date -function getPerDayRate(datapoints) { - var newpoints = []; - var previousPoint = undefined; - for (const point of datapoints) { - var y = point.y; - if (previousPoint) { - y -= previousPoint; +// Creates a file that contains dates and the number of posts that were stored by that date. Starts from the earliest post or the +// last line in the existing file (startDate). This function figures out the date range that our posts are in and then saves the number of posts that +// were created as of that day into the postCountByDay array. Then we write all the dates into our file. Then we're done. +async function rebuildPostTable (startDate) { + // if we're rebuilding (which means we're starting from the earliest post and don't have a startDate), we throw out any existing old version of the file. + if (fs.existsSync(postTableFileName) && !startDate) { + fs.unlinkSync(path.resolve(global.appRoot, postTableFileName)) + } + + var today = new Date(new Date().setDate(new Date().getDate() - 1)) + today.setHours(23) + today.setMinutes(59) + today.setSeconds(59) + today.setMilliseconds(999) // this actually sets today to the last millisecond that counts as yesterday. that's the most recent data we're looking at for end-of-day totals. + + // before will store the end of day time upon which we'll base our first end-of-day total. + var before + if (!startDate) { + await Post.find({ + timestamp: { + $exists: true + } + }).sort('timestamp').then(async posts => { + before = new Date(posts[0].timestamp.getFullYear(), posts[0].timestamp.getMonth(), posts[0].timestamp.getDate(), 23, 59, 59, 999) + }) + } else { + // if we have it, startDate is passed in as the components of the last line (describing the most recent saved date) from the existing file. + before = new Date(startDate[1], startDate[2], parseInt(startDate[3]) + 1, 23, 59, 59, 999) // start with the day after the most recently saved one (hence the plus one) + } + + var totalDays = (today.getTime() - before.getTime()) / (24 * 60 * 60 * 1000) + 1 // it's plus one to account for the day before the date stored by before + + var postCountByDay = [] + + // populate postCountByDay with date objects that also have a property indicating what the post count was at the end of that day + for (var i = 0; i < totalDays; i++) { + var sequentialDate = new Date(before) + await Post.find({ + $or: [{ + timestamp: { + $lte: sequentialDate } - newpoints.push({ - label: point.label, - year: point.year, - month: point.month, - date: point.date, - hour: point.hour, - minute: point.minute, - second: point.second, - y: y - }); - previousPoint = point.y; - } - return newpoints; + }, { + timestamp: undefined + }] + }).then(posts => { + sequentialDate.postCount = posts.length + postCountByDay.push(sequentialDate) + }) + before.setDate(before.getDate() + 1) + } + + // Write each line in CSV format (so it can be opened in excel or openoffice calc or gnumeric.) + // Note that the file always ends with a \n, and this needs to be true for this code to work when appending new lines to the file + postCountByDay.forEach((date) => { + fs.appendFileSync(postTableFileName, date.toDateString() + ',' + date.getFullYear() + ',' + date.getMonth() + ',' + date.getDate()) + fs.appendFileSync(postTableFileName, ',' + date.postCount) + fs.appendFileSync(postTableFileName, '\n') + }) + + // make way for next update/rebuild + postCountByDay = [] } -//==============================================the following doesn't work right now bc it doesn't look at nested comments====================================================== - - -//Creates a file that contains dates and the number of users that made a post or comment during the [interval] day period ending at that date. Starts from the earliest post or the -//last line in the existing file (startDate). This function figures out how many users were active during each [interval] day interval starting at the end of the day that -//the first post was posted on and finishing on the most recent date that's the start date plus a multiple of [interval]. -async function rebuildActiveUsersTable(startDate, interval = 3) { - //if we're rebuilding (which means we're starting from the earliest post and don't have a startDate), we throw out any existing old version of the file. - if (fs.existsSync(activeUserTableFileName) && !startDate) { - fs.unlinkSync(path.resolve(global.appRoot, activeUserTableFileName)); - } +// Creates a file that contains dates and the number of users that were stored by that date. Starts from the earliest user or the +// last line in the existing file (startDate). This function figures out the date range that our users are in and then saves the number of users that +// were created as of that day into the userCountByDay array. Then we write all the dates into our file. Then we're done. +async function rebuildUserTable (startDate) { + // if we're rebuilding (which means we're starting from the earliest user and don't have a startDate), we throw out any existing old version of the file. + if (fs.existsSync(userTableFileName) && !startDate) { + fs.unlinkSync(path.resolve(global.appRoot, userTableFileName)) + } + + var today = new Date(new Date().setDate(new Date().getDate() - 1)) + today.setHours(23) + today.setMinutes(59) + today.setSeconds(59) + today.setMilliseconds(999) // this actually sets today to the last millisecond that counts as yesterday. that's the most recent data we're looking at for end-of-day totals. + + // before will store the end of day time upon which we'll base our first end-of-day total. + var before + if (!startDate) { + await User.find({ + joined: { + $exists: true + } + }).sort('joined').then(async users => { + before = new Date(users[0].joined.getFullYear(), users[0].joined.getMonth(), users[0].joined.getDate(), 23, 59, 59, 999) + }) + } else { + // if we have it, startDate is passed in as the components of the last line (describing the most recent saved date) from the existing file. + before = new Date(startDate[1], startDate[2], parseInt(startDate[3]) + 1, 23, 59, 59, 999) // start with the day after the most recently saved one (hence the plus one) + } + + var totalDays = (today.getTime() - before.getTime()) / (24 * 60 * 60 * 1000) + 1 // it's plus one to account for the day before the date stored by before + + var userCountByDay = [] + + // populate userCountByDay with date objects that also have a property indicating what the user count was at the end of that day + for (var i = 0; i < totalDays; i++) { + var sequentialDate = new Date(before) + await User.find({ + $or: [{ + joined: { + $lte: sequentialDate + } + }, { + joined: undefined + }] + }).then(users => { + sequentialDate.userCount = users.length + userCountByDay.push(sequentialDate) + }) + before.setDate(before.getDate() + 1) + } + + // Write each line in CSV format (so it can be opened in excel or openoffice calc or gnumeric.) + // Note that the file always ends with a \n, and this needs to be true for this code to work when appending new lines to the file + userCountByDay.forEach((date) => { + fs.appendFileSync(userTableFileName, date.toDateString() + ',' + date.getFullYear() + ',' + date.getMonth() + ',' + date.getDate()) + fs.appendFileSync(userTableFileName, ',' + date.userCount) + fs.appendFileSync(userTableFileName, '\n') + }) + + // make way for next update/rebuild + userCountByDay = [] +} - var today = new Date(); - today.setHours(0) - today.setMinutes(0); - today.setSeconds(0); - today.setMilliseconds(0); - - //before will store the time value for the beginning of the first day upon which we have a post - var before; - if (!startDate) { - await Post.find({ - timestamp: { - $exists: true - } - }).sort('timestamp').then(async posts => { - before = new Date(posts[0].timestamp.getFullYear(), posts[0].timestamp.getMonth(), posts[0].timestamp.getDate(), 0, 0, 0, 0); - }) - } else { - //if we have it, startDate is passed in as the components of the last line (describing the most recent saved date) from the existing file. - before = new Date(startDate[1], startDate[2], parseInt(startDate[3]), 0, 0, 0, 0); //our interval will start with the day most recently saved +// this is called when the file is finished and it's time to turn it's csv data into json for handlebars to parse. there's probably a +// decent argument for saving a json file in the first place, huh. this also adds a datapoint representing the current date/time/post count, which +// should be current every time we build/display the graph. it either uses collection to query for the current y value or the callback we provide +async function parseTableForGraph (filename, collection, callbackForCurrentY, endOfDay = true) { + var jsonVersion = [] + // reads in file values + for (const line of fs.readFileSync(filename, 'utf-8').split('\n')) { + if (line && line !== '\n') { + var lineComps = line.split(',') + jsonVersion.push({ + label: lineComps[0], + year: lineComps[1], + month: lineComps[2], + date: lineComps[3], + hour: endOfDay ? 23 : 0, + minute: endOfDay ? 59 : 0, + second: endOfDay ? 59 : 0, + y: lineComps[4] + }) } + }; + // add in a datapoint representing the current exact second, if we have a source to obtain it from + var now = new Date() + if (collection) { + var numberOfDocs = await collection.countDocuments() + } else if (callbackForCurrentY) { + var numberOfDocs = await callbackForCurrentY() + } else { + return jsonVersion + } + jsonVersion.push({ + label: numberOfDocs == 69 ? 'nice' : now.toLocaleString(), + year: now.getFullYear(), + month: now.getMonth(), + date: now.getDate(), + hour: now.getHours(), + minute: now.getMinutes(), + second: now.getSeconds(), + y: numberOfDocs + }) + return jsonVersion +} - var activeUsersByInterval = []; - - //populate activeUsersByInterval with date objects that also have a property indicating how many active users there were in the three days previous - while (true) { - var intervalStart = new Date(before); - var intervalEnd = new Date(new Date(before).setDate(before.getDate() + interval)); - if (intervalEnd.getTime() >= today.getTime()) { - break; - } - intervalEnd.activeUserCount = await getActiveUsersForInterval(intervalStart, intervalEnd); - activeUsersByInterval.push(intervalEnd); - before.setDate(before.getDate() + interval); +// this takes the datpoints type thing above and takes the derivative by subtracting the last date +function getPerDayRate (datapoints) { + var newpoints = [] + var previousPoint = undefined + for (const point of datapoints) { + var y = point.y + if (previousPoint) { + y -= previousPoint } - - //Write each line in CSV format (so it can be opened in excel or openoffice calc or gnumeric.) - //Note that the file always ends with a \n, and this needs to be true for this code to work when appending new lines to the file - activeUsersByInterval.forEach((date) => { - fs.appendFileSync(activeUserTableFileName, date.toDateString() + "," + date.getFullYear() + "," + date.getMonth() + "," + date.getDate()); - fs.appendFileSync(activeUserTableFileName, "," + date.activeUserCount); - fs.appendFileSync(activeUserTableFileName, "\n"); + newpoints.push({ + label: point.label, + year: point.year, + month: point.month, + date: point.date, + hour: point.hour, + minute: point.minute, + second: point.second, + y: y }) + previousPoint = point.y + } + return newpoints +} - //make way for next update/rebuild - activeUsersByInterval = []; +//= =============================================the following doesn't work right now bc it doesn't look at nested comments====================================================== + +// Creates a file that contains dates and the number of users that made a post or comment during the [interval] day period ending at that date. Starts from the earliest post or the +// last line in the existing file (startDate). This function figures out how many users were active during each [interval] day interval starting at the end of the day that +// the first post was posted on and finishing on the most recent date that's the start date plus a multiple of [interval]. +async function rebuildActiveUsersTable (startDate, interval = 3) { + // if we're rebuilding (which means we're starting from the earliest post and don't have a startDate), we throw out any existing old version of the file. + if (fs.existsSync(activeUserTableFileName) && !startDate) { + fs.unlinkSync(path.resolve(global.appRoot, activeUserTableFileName)) + } + + var today = new Date() + today.setHours(0) + today.setMinutes(0) + today.setSeconds(0) + today.setMilliseconds(0) + + // before will store the time value for the beginning of the first day upon which we have a post + var before + if (!startDate) { + await Post.find({ + timestamp: { + $exists: true + } + }).sort('timestamp').then(async posts => { + before = new Date(posts[0].timestamp.getFullYear(), posts[0].timestamp.getMonth(), posts[0].timestamp.getDate(), 0, 0, 0, 0) + }) + } else { + // if we have it, startDate is passed in as the components of the last line (describing the most recent saved date) from the existing file. + before = new Date(startDate[1], startDate[2], parseInt(startDate[3]), 0, 0, 0, 0) // our interval will start with the day most recently saved + } + + var activeUsersByInterval = [] + + // populate activeUsersByInterval with date objects that also have a property indicating how many active users there were in the three days previous + while (true) { + var intervalStart = new Date(before) + var intervalEnd = new Date(new Date(before).setDate(before.getDate() + interval)) + if (intervalEnd.getTime() >= today.getTime()) { + break + } + intervalEnd.activeUserCount = await getActiveUsersForInterval(intervalStart, intervalEnd) + activeUsersByInterval.push(intervalEnd) + before.setDate(before.getDate() + interval) + } + + // Write each line in CSV format (so it can be opened in excel or openoffice calc or gnumeric.) + // Note that the file always ends with a \n, and this needs to be true for this code to work when appending new lines to the file + activeUsersByInterval.forEach((date) => { + fs.appendFileSync(activeUserTableFileName, date.toDateString() + ',' + date.getFullYear() + ',' + date.getMonth() + ',' + date.getDate()) + fs.appendFileSync(activeUserTableFileName, ',' + date.activeUserCount) + fs.appendFileSync(activeUserTableFileName, '\n') + }) + + // make way for next update/rebuild + activeUsersByInterval = [] } -//this is a helper function that gets the number of active users during a time interval. it's called by the above function and also passed to -//parseTableForGraph so that that function can figure out how many users were active right up until the present moment. -async function getActiveUsersForInterval(intervalStart, intervalEnd) { - var count = await Post.aggregate([{ - //find posts that either themselves were made during this time interval or have comments that were - '$match': { - '$or': [{ - '$and': [{ - 'timestamp': { - '$gt': intervalStart - } - }, { - 'timestamp': { - '$lte': intervalEnd - } - }] - }, { - '$and': [{ - 'comments.timestamp': { - '$gt': intervalStart - } - }, { - 'comments.timestamp': { - '$lte': intervalEnd - } - }] - }] - } - }, { - //these two stages create an "authors" array consisting of the author of the post and all of the comments, including the timestamp of their acts of authorship - '$addFields': { - 'ac': [{ - 'author': '$author', - 'timestamp': '$timestamp' - }] - } - }, { - '$project': { - '_id': '$_id', - 'authors': { - '$concatArrays': [ - '$ac', '$comments' - ] - } - } +// this is a helper function that gets the number of active users during a time interval. it's called by the above function and also passed to +// parseTableForGraph so that that function can figure out how many users were active right up until the present moment. +async function getActiveUsersForInterval (intervalStart, intervalEnd) { + var count = await Post.aggregate([{ + // find posts that either themselves were made during this time interval or have comments that were + $match: { + $or: [{ + $and: [{ + timestamp: { + $gt: intervalStart + } }, { - //make a new document for each act of authorship - '$unwind': { - 'path': '$authors' - } + timestamp: { + $lte: intervalEnd + } + }] + }, { + $and: [{ + 'comments.timestamp': { + $gt: intervalStart + } }, { - '$match': { - //match only the specific acts of authorship that fall within our time interval - '$and': [{ - 'authors.timestamp': { - '$gt': intervalStart - } - }, { - 'authors.timestamp': { - '$lt': intervalEnd - } - }] - - } - }, - { - //group the matched acts of authorship into one document per author - '$group': { - '_id': '$authors.author' - } - }, - { - //count how many documents we end up with and put it in the "numberOfAuthors" property of the result - '$group': { - '_id': null, - 'numberOfAuthors': { - '$sum': 1 - } - } + 'comments.timestamp': { + $lte: intervalEnd + } + }] + }] + } + }, { + // these two stages create an "authors" array consisting of the author of the post and all of the comments, including the timestamp of their acts of authorship + $addFields: { + ac: [{ + author: '$author', + timestamp: '$timestamp' + }] + } + }, { + $project: { + _id: '$_id', + authors: { + $concatArrays: [ + '$ac', '$comments' + ] + } + } + }, { + // make a new document for each act of authorship + $unwind: { + path: '$authors' + } + }, { + $match: { + // match only the specific acts of authorship that fall within our time interval + $and: [{ + 'authors.timestamp': { + $gt: intervalStart } - ]); - return count.length > 0 ? count[0].numberOfAuthors : 0; //the fact that it's an array isn't relevant, aggregate just assumes it's meant to return an array of documents even though we only want one here + }, { + 'authors.timestamp': { + $lt: intervalEnd + } + }] + + } + }, + { + // group the matched acts of authorship into one document per author + $group: { + _id: '$authors.author' + } + }, + { + // count how many documents we end up with and put it in the "numberOfAuthors" property of the result + $group: { + _id: null, + numberOfAuthors: { + $sum: 1 + } + } + } + ]) + return count.length > 0 ? count[0].numberOfAuthors : 0 // the fact that it's an array isn't relevant, aggregate just assumes it's meant to return an array of documents even though we only want one here } -async function getActiveUsersSinceLastSave() { - var lastLine = ""; - fs.readFileSync(activeUserTableFileName, 'utf-8').split('\n').forEach(function (line) { - if (line && line != "\n") { - lastLine = line; - } - }); - var lastLineSplit = lastLine.split(','); - var lastSave = new Date(lastLineSplit[1], lastLineSplit[2], lastLineSplit[3], 0, 0, 0, 0); - return await getActiveUsersForInterval(lastSave, new Date()); -} \ No newline at end of file +async function getActiveUsersSinceLastSave () { + var lastLine = '' + fs.readFileSync(activeUserTableFileName, 'utf-8').split('\n').forEach(function (line) { + if (line && line != '\n') { + lastLine = line + } + }) + var lastLineSplit = lastLine.split(',') + var lastSave = new Date(lastLineSplit[1], lastLineSplit[2], lastLineSplit[3], 0, 0, 0, 0) + return await getActiveUsersForInterval(lastSave, new Date()) +} diff --git a/app/utilityFunctionsMostlyText.js b/app/utilityFunctionsMostlyText.js index 9a2096dd..37092a38 100644 --- a/app/utilityFunctionsMostlyText.js +++ b/app/utilityFunctionsMostlyText.js @@ -1,537 +1,535 @@ -//these requires are not in server.js bc they're only used here -const urlParser = require('url'); +// these requires are not in server.js bc they're only used here +const urlParser = require('url') const metascraper = require('metascraper')([ - require('metascraper-description')(), - require('metascraper-image')(), - require('metascraper-title')(), - require('metascraper-url')() -]); -const request = require('request'); + require('metascraper-description')(), + require('metascraper-image')(), + require('metascraper-title')(), + require('metascraper-url')() +]) +const request = require('request') module.exports = { - // Parses new post and new comment content. Input: a text string. Output: a parsed text string. - parseText: async function(rawText, mentionsEnabled = true, hashtagsEnabled = true, urlsEnabled = true) { - console.log("Parsing content") - if (typeof rawText != "string") { //it is an array of paragraphs and inline elements - var parsedParagraphList = await this.parseParagraphList(rawText); - rawText = parsedParagraphList.text; - var inlineElements = parsedParagraphList.inlineElements; - } else { - var inlineElements = []; - //this is also done by parseParagraphList, but if we're not using that we use this, here - rawText = rawText.replace(/^(

(
|\s)*<\/p>)*/, '') //filter out blank lines from beginning - rawText = rawText.replace(/(

(
|\s)<\/p>)*$/, '') //filter them out from the end - rawText = rawText.replace(/(

(
|\s)<\/p>){2,}/g, '


') //filters out multiple blank lines in a row within the post - } + // Parses new post and new comment content. Input: a text string. Output: a parsed text string. + parseText: async function (rawText, mentionsEnabled = true, hashtagsEnabled = true, urlsEnabled = true) { + console.log('Parsing content') + if (typeof rawText !== 'string') { // it is an array of paragraphs and inline elements + var parsedParagraphList = await this.parseParagraphList(rawText) + rawText = parsedParagraphList.text + var inlineElements = parsedParagraphList.inlineElements + } else { + var inlineElements = [] + // this is also done by parseParagraphList, but if we're not using that we use this, here + rawText = rawText.replace(/^(

(
|\s)*<\/p>)*/, '') // filter out blank lines from beginning + rawText = rawText.replace(/(

(
|\s)<\/p>)*$/, '') // filter them out from the end + rawText = rawText.replace(/(

(
|\s)<\/p>){2,}/g, '


') // filters out multiple blank lines in a row within the post + } - var mentionRegex = /(^|[^@\w])@([\w-]{1,30})[\b-]*/g - var mentionReplace = '$1@$2'; - var hashtagRegex = /(^|>|\n|\ |\t)#(\w{1,60})\b/g - var hashtagReplace = '$1#$2'; + var mentionRegex = /(^|[^@\w])@([\w-]{1,30})[\b-]*/g + var mentionReplace = '$1@$2' + var hashtagRegex = /(^|>|\n|\ |\t)#(\w{1,60})\b/g + var hashtagReplace = '$1#$2' - if (urlsEnabled) { - rawText = Autolinker.link(rawText); - } - if (mentionsEnabled) { - rawText = rawText.replace(mentionRegex, mentionReplace) - } - if (hashtagsEnabled) { - rawText = rawText.replace(hashtagRegex, hashtagReplace); - } + if (urlsEnabled) { + rawText = Autolinker.link(rawText) + } + if (mentionsEnabled) { + rawText = rawText.replace(mentionRegex, mentionReplace) + } + if (hashtagsEnabled) { + rawText = rawText.replace(hashtagRegex, hashtagReplace) + } - rawText = this.sanitizeHtmlForSweet(rawText); + rawText = this.sanitizeHtmlForSweet(rawText) - let mentionsArray = Array.from(new Set(rawText.replace(/<[^>]*>/g, " ").match(mentionRegex))) - let tagsArray = Array.from(new Set(rawText.replace(/<[^>]*>/g, " ").match(hashtagRegex))) - let trimmedMentions = [] - let trimmedTags = [] - if (mentionsArray) { - mentionsArray.forEach((el) => { - trimmedMentions.push(el.replace(/(@|\s)*/i, '')); - }) - } - if (tagsArray) { - tagsArray.forEach((el) => { - trimmedTags.push(el.replace(/(#|\s)*/i, '')); - }) - } + const mentionsArray = Array.from(new Set(rawText.replace(/<[^>]*>/g, ' ').match(mentionRegex))) + const tagsArray = Array.from(new Set(rawText.replace(/<[^>]*>/g, ' ').match(hashtagRegex))) + const trimmedMentions = [] + const trimmedTags = [] + if (mentionsArray) { + mentionsArray.forEach((el) => { + trimmedMentions.push(el.replace(/(@|\s)*/i, '')) + }) + } + if (tagsArray) { + tagsArray.forEach((el) => { + trimmedTags.push(el.replace(/(#|\s)*/i, '')) + }) + } - var parsedStuff = { - text: rawText, //well, not raw anymore - mentions: trimmedMentions, - tags: trimmedTags, - inlineElements: inlineElements - } + var parsedStuff = { + text: rawText, // well, not raw anymore + mentions: trimmedMentions, + tags: trimmedTags, + inlineElements: inlineElements + } - //console.log(JSON.stringify(parsedStuff, null, 4)); + // console.log(JSON.stringify(parsedStuff, null, 4)); - return parsedStuff; - }, - parseParagraphList: async function(pList) { - var inlineElements = []; - //iterate over list, remove blank lines from the beginning, collapse consecutive blank lines into one, and finally remove the last line if it's blank - //also, pull out the inline elements and put them in their own array with their position - var lastWasBlank = false; - for (var i = 0; i < pList.length; i++) { - if (typeof pList[i] == "string") { - var isBlank = pList[i].match(/^

(
|\s)*<\/p>$/); - if (((i == 0 && !inlineElements.length) || lastWasBlank) && isBlank) { - pList.splice(i, 1); - i--; - } - lastWasBlank = isBlank; - } else { - if (pList[i].type == "link-preview") { - try { - pList[i] = await this.getLinkMetadata(pList[i].linkUrl); - } catch (err) { - console.log("could not parse link preview while creating post:"); - console.log(err); - pList.splice(i, 1); - i--; - } - } - lastWasBlank = false; - pList[i].position = i; - inlineElements.push(pList[i]); - pList.splice(i, 1); - i--; - } - } - if (pList.length && lastWasBlank) { - pList.splice(pList.length - 1, 1); + return parsedStuff + }, + parseParagraphList: async function (pList) { + var inlineElements = [] + // iterate over list, remove blank lines from the beginning, collapse consecutive blank lines into one, and finally remove the last line if it's blank + // also, pull out the inline elements and put them in their own array with their position + var lastWasBlank = false + for (var i = 0; i < pList.length; i++) { + if (typeof pList[i] === 'string') { + var isBlank = pList[i].match(/^

(
|\s)*<\/p>$/) + if (((i == 0 && !inlineElements.length) || lastWasBlank) && isBlank) { + pList.splice(i, 1) + i-- } - return { text: pList.join(''), inlineElements: inlineElements }; - }, - sanitizeHtmlForSweet: function(parsedContent) { - return sanitizeHtml(parsedContent, { - allowedTags: ['blockquote', 'ul', 'li', 'em', 'i', 'b', 'strong', 'a', 'p', 'br'], - allowedAttributes: { - 'a': ['href', 'target'] - }, - transformTags: { - 'a': function(tagName, attribs) { - //if a link is not explicitly relative due to an initial / (like mention and hashtag links are) and doesn't already include a protocol: - if (attribs.href.substring(0, 1) != "/" && !attribs.href.includes('//')) { - //make the link explicitly non-relative (even though technically maybe it should be https browsers will generally figure that out) - attribs.href = "http://" + attribs.href; - } - attribs.target = "_blank"; - return { - tagName: 'a', - attribs: attribs - }; - } - } - }); - }, - //stolen from mustache.js (https://github.com/janl/mustache.js/blob/master/mustache.js#L73) via stack overflow (https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery) - escapeHTMLChars: function(string) { - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=', - '\t': ' ' //this one is my idea - }; - return String(string).replace(/[&<>"'`=\/\t]/g, function(s) { - return entityMap[s]; - }); - }, - getLinkMetadata: async function(url) { - //remove the protocol and the path if it's empty from the url bc that's how it's stored in the cache (that way it matches it with or without) - var parsedUrl = Object.assign(urlParser.parse(url),{protocol:"",slashes:false}); - if(parsedUrl.path=="/" && parsedUrl.pathname=="/"){ - parsedUrl.path = ""; - parsedUrl.pathname = ""; - } - var retrievalUrl = urlParser.format(parsedUrl); - var finalUrl; //this will have the correct protocol, obtained either by the cache or the request package - var cache = mongoose.model('Cached Link Metadata'); - var found = await cache.findOne({ retrievalUrl: retrievalUrl }); - var cacheHit = !!found; - if (!cacheHit) { - var urlreq = new Promise(function(resolve, reject) { - request({ url: url.includes("//") ? url : ("http://"+url), gzip: true }, function(error, response, body) { //(faking a maybe-wrong protocol here just so the this thing will accept it) - if (error) { - reject(error); - } else { - finalUrl = response.request.href; - resolve(body); - } - }); - }) - const html = await urlreq; - var metadata = await metascraper({ html, url: finalUrl }); - } else { - var metadata = found; - var finalUrl = metadata.linkUrl; + lastWasBlank = isBlank + } else { + if (pList[i].type == 'link-preview') { + try { + pList[i] = await this.getLinkMetadata(pList[i].linkUrl) + } catch (err) { + console.log('could not parse link preview while creating post:') + console.log(err) + pList.splice(i, 1) + i-- + } } - var result = { - type: "link-preview", - retrievalUrl: retrievalUrl, - linkUrl: finalUrl, - image: metadata.image, - title: metadata.title, - description: metadata.description, - domain: urlParser.parse(finalUrl).hostname - } - //taken from https://stackoverflow.com/questions/19377262/regex-for-youtube-url - var youtubeUrlFindingRegex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/ - //taken from https://github.com/regexhq/vimeo-regex/blob/master/index.js - var vimeoUrlFindingRegex = /^(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)$/ - var parsed = undefined; - var isEmbeddableVideo = false; - if (parsed = youtubeUrlFindingRegex.exec(finalUrl)) { - result.embedUrl = "https://www.youtube.com/embed/" + parsed[5] + "?autoplay=1"; //won't actually autoplay until link preview is clicked - isEmbeddableVideo = true; - try { - var time = /t=(?:([0-9]*)m)?((?:[0-9])*)(?:s)?/.exec(parsed[6]); - if (time) { - var seconds = 0; - if (time[2]) { - seconds += parseInt(time[2]); - } - if (time[1]) { - seconds += (parseInt(time[1]) * 60); - } - if (seconds) { - result.embedUrl += "&start=" + seconds; - } - } - } catch (err) { //catch potential parseInt errors - console.log("youtube link had time specifier that was apparently malformed! error:") - console.log(err); - } - } else if (parsed = vimeoUrlFindingRegex.exec(finalUrl)) { - result.embedUrl = "https://player.vimeo.com/video/" + parsed[4] + "?autoplay=1"; //won't actually autoplay until link preview is clicked - isEmbeddableVideo = true; + lastWasBlank = false + pList[i].position = i + inlineElements.push(pList[i]) + pList.splice(i, 1) + i-- + } + } + if (pList.length && lastWasBlank) { + pList.splice(pList.length - 1, 1) + } + return { text: pList.join(''), inlineElements: inlineElements } + }, + sanitizeHtmlForSweet: function (parsedContent) { + return sanitizeHtml(parsedContent, { + allowedTags: ['blockquote', 'ul', 'li', 'em', 'i', 'b', 'strong', 'a', 'p', 'br'], + allowedAttributes: { + a: ['href', 'target'] + }, + transformTags: { + a: function (tagName, attribs) { + // if a link is not explicitly relative due to an initial / (like mention and hashtag links are) and doesn't already include a protocol: + if (attribs.href.substring(0, 1) != '/' && !attribs.href.includes('//')) { + // make the link explicitly non-relative (even though technically maybe it should be https browsers will generally figure that out) + attribs.href = 'http://' + attribs.href + } + attribs.target = '_blank' + return { + tagName: 'a', + attribs: attribs + } } - result.isEmbeddableVideo = isEmbeddableVideo; - if (!cacheHit) { - (new cache(result)).save(); + } + }) + }, + // stolen from mustache.js (https://github.com/janl/mustache.js/blob/master/mustache.js#L73) via stack overflow (https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery) + escapeHTMLChars: function (string) { + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=', + '\t': ' ' // this one is my idea + } + return String(string).replace(/[&<>"'`=\/\t]/g, function (s) { + return entityMap[s] + }) + }, + getLinkMetadata: async function (url) { + // remove the protocol and the path if it's empty from the url bc that's how it's stored in the cache (that way it matches it with or without) + var parsedUrl = Object.assign(urlParser.parse(url), { protocol: '', slashes: false }) + if (parsedUrl.path == '/' && parsedUrl.pathname == '/') { + parsedUrl.path = '' + parsedUrl.pathname = '' + } + var retrievalUrl = urlParser.format(parsedUrl) + var finalUrl // this will have the correct protocol, obtained either by the cache or the request package + var cache = mongoose.model('Cached Link Metadata') + var found = await cache.findOne({ retrievalUrl: retrievalUrl }) + var cacheHit = !!found + if (!cacheHit) { + var urlreq = new Promise(function (resolve, reject) { + request({ url: url.includes('//') ? url : ('http://' + url), gzip: true }, function (error, response, body) { // (faking a maybe-wrong protocol here just so the this thing will accept it) + if (error) { + reject(error) + } else { + finalUrl = response.request.href + resolve(body) + } + }) + }) + const html = await urlreq + var metadata = await metascraper({ html, url: finalUrl }) + } else { + var metadata = found + var finalUrl = metadata.linkUrl + } + var result = { + type: 'link-preview', + retrievalUrl: retrievalUrl, + linkUrl: finalUrl, + image: metadata.image, + title: metadata.title, + description: metadata.description, + domain: urlParser.parse(finalUrl).hostname + } + // taken from https://stackoverflow.com/questions/19377262/regex-for-youtube-url + var youtubeUrlFindingRegex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/ + // taken from https://github.com/regexhq/vimeo-regex/blob/master/index.js + var vimeoUrlFindingRegex = /^(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)$/ + var parsed = undefined + var isEmbeddableVideo = false + if (parsed = youtubeUrlFindingRegex.exec(finalUrl)) { + result.embedUrl = 'https://www.youtube.com/embed/' + parsed[5] + '?autoplay=1' // won't actually autoplay until link preview is clicked + isEmbeddableVideo = true + try { + var time = /t=(?:([0-9]*)m)?((?:[0-9])*)(?:s)?/.exec(parsed[6]) + if (time) { + var seconds = 0 + if (time[2]) { + seconds += parseInt(time[2]) + } + if (time[1]) { + seconds += (parseInt(time[1]) * 60) + } + if (seconds) { + result.embedUrl += '&start=' + seconds + } } - return result; - }, - //moves them out of temp storage, creates image documents for them in the database, and returns arrays with their horizontality/verticality - //the non-first arguments are just stored in the image documents in the database. postType is "original" or "community" - finalizeImages: async function(imageFileNames, postType, community, posterID, privacy, postImageQuality, imagesCurrentFolder = (global.appRoot + "/cdn/images/temp/")) { - var imageIsVertical = []; - var imageIsHorizontal = []; - for (const imageFileName of imageFileNames) { - await new Promise((resolve, reject) => { - fs.rename(imagesCurrentFolder + imageFileName, "./cdn/images/" + imageFileName, function(e) { - if (e) { console.error("could not move " + imageFileName + " out of temp\n" + e); } - resolve(); - }) - }) + } catch (err) { // catch potential parseInt errors + console.log('youtube link had time specifier that was apparently malformed! error:') + console.log(err) + } + } else if (parsed = vimeoUrlFindingRegex.exec(finalUrl)) { + result.embedUrl = 'https://player.vimeo.com/video/' + parsed[4] + '?autoplay=1' // won't actually autoplay until link preview is clicked + isEmbeddableVideo = true + } + result.isEmbeddableVideo = isEmbeddableVideo + if (!cacheHit) { + (new cache(result)).save() + } + return result + }, + // moves them out of temp storage, creates image documents for them in the database, and returns arrays with their horizontality/verticality + // the non-first arguments are just stored in the image documents in the database. postType is "original" or "community" + finalizeImages: async function (imageFileNames, postType, community, posterID, privacy, postImageQuality, imagesCurrentFolder = (global.appRoot + '/cdn/images/temp/')) { + var imageIsVertical = [] + var imageIsHorizontal = [] + for (const imageFileName of imageFileNames) { + await new Promise((resolve, reject) => { + fs.rename(imagesCurrentFolder + imageFileName, './cdn/images/' + imageFileName, function (e) { + if (e) { console.error('could not move ' + imageFileName + ' out of temp\n' + e) } + resolve() + }) + }) - var metadata = await sharp('./cdn/images/' + imageFileName).metadata() - var image = new Image({ - //posts' types are either original or community; the image's contexts are either user or community, meaning the same things. - context: postType == "community" ? "community" : "user", - community: postType == "community" ? community : undefined, - filename: imageFileName, - privacy: privacy, - user: posterID, - quality: postImageQuality, - height: metadata.height, - width: metadata.width - }) - await image.save(); + var metadata = await sharp('./cdn/images/' + imageFileName).metadata() + var image = new Image({ + // posts' types are either original or community; the image's contexts are either user or community, meaning the same things. + context: postType == 'community' ? 'community' : 'user', + community: postType == 'community' ? community : undefined, + filename: imageFileName, + privacy: privacy, + user: posterID, + quality: postImageQuality, + height: metadata.height, + width: metadata.width + }) + await image.save() - if (fs.existsSync(path.resolve('./cdn/images/' + imageFileName))) { - imageIsVertical.push(((metadata.width / metadata.height) < 0.75) ? "vertical-image" : ""); - imageIsHorizontal.push(((metadata.width / metadata.height) > 1.33) ? "horizontal-image" : ""); + if (fs.existsSync(path.resolve('./cdn/images/' + imageFileName))) { + imageIsVertical.push(((metadata.width / metadata.height) < 0.75) ? 'vertical-image' : '') + imageIsHorizontal.push(((metadata.width / metadata.height) > 1.33) ? 'horizontal-image' : '') + } else { + console.log('image ' + './cdn/images/' + imageFileName + ' not found when determining orientation! Oh no') + imageIsVertical.push('') + imageIsHorizontal.push('') + } + } + return { imageIsHorizontal: imageIsHorizontal, imageIsVertical: imageIsVertical } + }, + // returns a string with the html contents of the post or comment rendered based on parsedContent, inlineElements, embeds, and/or the image parallel arrays. also, some extra info in the inlineElements, but that won't get saved 'cause it's not in the inlineElement schema + renderHTMLContent: async function (postOrComment, forEditor = false) { + if (forEditor) { + // remove relative links (those that start with a / but not //) from content for the editor. this is so that @ mention links and + // hashtag links don't get picked up and re-linked when the edit is parsed after submission. + var cleanedParsedContent = sanitizeHtml(postOrComment.parsedContent, { + transformTags: { + a: function (tagName, attribs) { + if (attribs.href.match(/^\/\w/)) { + return { + tagName: '', + attribs: {} + } } else { - console.log("image " + './cdn/images/' + imageFileName + " not found when determining orientation! Oh no") - imageIsVertical.push(""); - imageIsHorizontal.push(""); + return { + tagName: 'a', + attribs: attribs + } } - + } } - return { imageIsHorizontal: imageIsHorizontal, imageIsVertical: imageIsVertical }; - }, - //returns a string with the html contents of the post or comment rendered based on parsedContent, inlineElements, embeds, and/or the image parallel arrays. also, some extra info in the inlineElements, but that won't get saved 'cause it's not in the inlineElement schema - renderHTMLContent: async function(postOrComment, forEditor = false) { + }) + } else { + var cleanedParsedContent = postOrComment.parsedContent + } + if (postOrComment.inlineElements && postOrComment.inlineElements.length) { + var lines = [] // they're more like paragraphs, really + const lineFinder = /(

.*?<\/p>)|(

    .*?<\/ul>)|(
    .*?<\/blockquote>)/g + while (line = lineFinder.exec(cleanedParsedContent)) { + lines.push(line[0]) + } + var addedLines = 0 + for (const il of postOrComment.inlineElements) { + if (il.type == 'link-preview') { + if (forEditor) { + il.editing = true + var html = await hbs.render('./views/partials/scriptPartials/linkPreviewPreview.handlebars', il) + } else { + if (il.isEmbeddableVideo) { + console.log('embed!!!!') + il.type = 'video' // the template looks for "video" in this field, like what older posts with embeds have + } + var html = await hbs.render('./views/partials/embed.handlebars', il) + il.type = 'link-preview' // yes, this is dumb. the alternative is to list all the different variables the template expects in the rendering options with type: (isEmbeddableVideo ? "video" : "asdj;lfkfdsajkfl;") or something + } + } else if (il.type == 'image(s)') { + if (forEditor) { + var html = '' + for (var i = 0; i < il.images.length; i++) { + html += (await hbs.render('./views/partials/scriptPartials/imagePreview.handlebars', { editing: true, image: il.images[i], imageUrl: '/api/image/display/' + il.images[i], description: il.imageDescriptions[i] })) + } + } else { + il.contentWarnings = postOrComment.contentWarnings + il.author = { username: (await User.findById(postOrComment.author, { username: 1 })).username } + var filenames = il.images + il.images = il.images.map(v => '/api/image/display/' + v) + var html = await hbs.render('./views/partials/imagegallery.handlebars', il) + il.images = filenames // yes, this is dumb. the alternative is to specify each variable the template expects individually in the rendering options with like images: fullImagePaths + } + } + lines.splice(il.position + addedLines, 0, html) + addedLines++ + } + return lines.join('') + } else if ((postOrComment.images && postOrComment.images.length) || (postOrComment.embeds && postOrComment.embeds.length)) { + var endHTML = '' + if (postOrComment.embeds && postOrComment.embeds.length) { + // this is a post from before the inlineElements array, render its embed (mandated to be just one) and put it at the end of html if (forEditor) { - //remove relative links (those that start with a / but not //) from content for the editor. this is so that @ mention links and - //hashtag links don't get picked up and re-linked when the edit is parsed after submission. - var cleanedParsedContent = sanitizeHtml(postOrComment.parsedContent, { - transformTags: { - 'a': function(tagName, attribs) { - if (attribs.href.match(/^\/\w/)) { - return { - tagName: '', - attribs: {} - } - } else { - return { - tagName: 'a', - attribs: attribs - }; - } - } - } - }) + postOrComment.embeds[0].editing = true + endHTML += await hbs.render('./views/partials/scriptPartials/linkPreviewPreview.handlebars', postOrComment.embeds[0]) } else { - var cleanedParsedContent = postOrComment.parsedContent + endHTML += await hbs.render('./views/partials/embed.handlebars', postOrComment.embeds[0]) } - if (postOrComment.inlineElements && postOrComment.inlineElements.length) { - var lines = []; //they're more like paragraphs, really - const lineFinder = /(

    .*?<\/p>)|(

      .*?<\/ul>)|(
      .*?<\/blockquote>)/g; - while (line = lineFinder.exec(cleanedParsedContent)) { - lines.push(line[0]); - } - var addedLines = 0; - for (const il of postOrComment.inlineElements) { - if (il.type == "link-preview") { - if (forEditor) { - il.editing = true; - var html = await hbs.render('./views/partials/scriptPartials/linkPreviewPreview.handlebars', il); - } else { - if (il.isEmbeddableVideo) { - console.log("embed!!!!"); - il.type = "video"; //the template looks for "video" in this field, like what older posts with embeds have - } - var html = await hbs.render('./views/partials/embed.handlebars', il); - il.type = "link-preview"; //yes, this is dumb. the alternative is to list all the different variables the template expects in the rendering options with type: (isEmbeddableVideo ? "video" : "asdj;lfkfdsajkfl;") or something - } - } else if (il.type == "image(s)") { - if (forEditor) { - var html = ""; - for (var i = 0; i < il.images.length; i++) { - html += (await hbs.render('./views/partials/scriptPartials/imagePreview.handlebars', { editing: true, image: il.images[i], imageUrl: "/api/image/display/" + il.images[i], description: il.imageDescriptions[i] })); - } - } else { - il.contentWarnings = postOrComment.contentWarnings; - il.author = { username: (await User.findById(postOrComment.author, { username: 1 })).username }; - var filenames = il.images; - il.images = il.images.map(v => "/api/image/display/" + v); - var html = await hbs.render('./views/partials/imagegallery.handlebars', il); - il.images = filenames; //yes, this is dumb. the alternative is to specify each variable the template expects individually in the rendering options with like images: fullImagePaths - } - } - lines.splice(il.position + addedLines, 0, html); - addedLines++; - } - return lines.join(''); - } else if ((postOrComment.images && postOrComment.images.length) || (postOrComment.embeds && postOrComment.embeds.length)) { - var endHTML = ""; - if (postOrComment.embeds && postOrComment.embeds.length) { - //this is a post from before the inlineElements array, render its embed (mandated to be just one) and put it at the end of html - if (forEditor) { - postOrComment.embeds[0].editing = true; - endHTML += await hbs.render('./views/partials/scriptPartials/linkPreviewPreview.handlebars', postOrComment.embeds[0]); - } else { - endHTML += await hbs.render('./views/partials/embed.handlebars', postOrComment.embeds[0]) - } - } - if (postOrComment.images && postOrComment.images.length) { - //if it's not a comment and it either has no registered image version or the registered image version is less than 2, it uses the old url scheme. - var imageUrlPrefix = !postOrComment.parent && (!postOrComment.imageVersion || postOrComment.imageVersion < 2) ? "/images/uploads/" : "/api/image/display/"; - if (forEditor) { - for (var i = 0; i < postOrComment.images.length; i++) { - endHTML += (await hbs.render('./views/partials/scriptPartials/imagePreview.handlebars', { editing: true, image: postOrComment.images[i], imageUrl: imageUrlPrefix + postOrComment.images[i], description: postOrComment.imageDescriptions[i] })) + ''; //is this line long enough yet - } - } else { - //this is a post or comment from before the inlineElements array, render its images (with determined full urls) with the parallel arrays and put that at the end of html - var filenames = postOrComment.images; - postOrComment.images = postOrComment.images.map(v => imageUrlPrefix + v); - endHTML += await hbs.render('./views/partials/imagegallery.handlebars', postOrComment) - postOrComment.images = filenames; //yes, this is dumb - } - } - return cleanedParsedContent + endHTML; + } + if (postOrComment.images && postOrComment.images.length) { + // if it's not a comment and it either has no registered image version or the registered image version is less than 2, it uses the old url scheme. + var imageUrlPrefix = !postOrComment.parent && (!postOrComment.imageVersion || postOrComment.imageVersion < 2) ? '/images/uploads/' : '/api/image/display/' + if (forEditor) { + for (var i = 0; i < postOrComment.images.length; i++) { + endHTML += (await hbs.render('./views/partials/scriptPartials/imagePreview.handlebars', { editing: true, image: postOrComment.images[i], imageUrl: imageUrlPrefix + postOrComment.images[i], description: postOrComment.imageDescriptions[i] })) + '' // is this line long enough yet + } } else { - return cleanedParsedContent; + // this is a post or comment from before the inlineElements array, render its images (with determined full urls) with the parallel arrays and put that at the end of html + var filenames = postOrComment.images + postOrComment.images = postOrComment.images.map(v => imageUrlPrefix + v) + endHTML += await hbs.render('./views/partials/imagegallery.handlebars', postOrComment) + postOrComment.images = filenames // yes, this is dumb } - }, - isEven: function(n) { - return n % 2 == 0; - }, - isOdd: function(n) { - return Math.abs(n % 2) == 1; - }, - slugify: function(string) { - const a = 'àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœṕŕßśșțùúüûǘẃẍÿź·/_,:;' - const b = 'aaaaaaaaceeeeghiiiimnnnoooooprssstuuuuuwxyz------' - const p = new RegExp(a.split('').join('|'), 'g') + } + return cleanedParsedContent + endHTML + } else { + return cleanedParsedContent + } + }, + isEven: function (n) { + return n % 2 == 0 + }, + isOdd: function (n) { + return Math.abs(n % 2) == 1 + }, + slugify: function (string) { + const a = 'àáäâãåăæçèéëêǵḧìíïîḿńǹñòóöôœṕŕßśșțùúüûǘẃẍÿź·/_,:;' + const b = 'aaaaaaaaceeeeghiiiimnnnoooooprssstuuuuuwxyz------' + const p = new RegExp(a.split('').join('|'), 'g') - return string.toString().toLowerCase() - .replace(/\s+/g, '-') // Replace spaces with - - .replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters - .replace(/&/g, '-and-') // Replace & with 'and' - .replace(/[^\w\-]+/g, '') // Remove all non-word characters - .replace(/\-\-+/g, '-') // Replace multiple - with single - - .replace(/^-+/, '') // Trim - from start of text - .replace(/-+$/, '') // Trim - from end of text - }, + return string.toString().toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(p, c => b.charAt(a.indexOf(c))) // Replace special characters + .replace(/&/g, '-and-') // Replace & with 'and' + .replace(/[^\w\-]+/g, '') // Remove all non-word characters + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, '') // Trim - from end of text + } } -function wordCount(str) { - return str.split(' ').filter(function(n) { return n != '' }).length; +function wordCount (str) { + return str.split(' ').filter(function (n) { return n != '' }).length } +// the following is a function i wrote to parse quilljs' delta format and turn formatted text into html and inline elements into an array. it turned +// out to be more complicated than necessary so right now we're actually just going to pull the html from the quill editor directly and process it in +// the parseParagraphList function above. this function works well to the best of my knowledge and could still be used for turning quilljs deltas into +// custom formatted html if such a need ever arises. -//the following is a function i wrote to parse quilljs' delta format and turn formatted text into html and inline elements into an array. it turned -//out to be more complicated than necessary so right now we're actually just going to pull the html from the quill editor directly and process it in -//the parseParagraphList function above. this function works well to the best of my knowledge and could still be used for turning quilljs deltas into -//custom formatted html if such a need ever arises. +// called by parse text to turn the quilljs delta format (which can be used for text with embeds) into html +function parseDeltaNotUsedRightNow (delta) { + // taken from https://stackoverflow.com/questions/19377262/regex-for-youtube-url + var youtubeUrlFindingRegex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/ + // taken from https://github.com/regexhq/vimeo-regex/blob/master/index.js + var vimeoUrlFindingRegex = /^(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)$/ -//called by parse text to turn the quilljs delta format (which can be used for text with embeds) into html -function parseDeltaNotUsedRightNow(delta) { - //taken from https://stackoverflow.com/questions/19377262/regex-for-youtube-url - var youtubeUrlFindingRegex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/ - //taken from https://github.com/regexhq/vimeo-regex/blob/master/index.js - var vimeoUrlFindingRegex = /^(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)$/ + // this starts the first line. it may be modified if it turns out this is a line in a list or blockquote + var linesOfParsedString = ['

      '] - //this starts the first line. it may be modified if it turns out this is a line in a list or blockquote - var linesOfParsedString = ["

      "]; + var inlineElements = [] + var imagesAdded = 0 + var linkPreviewsAdded = 0 + const imagesAllowed = 4 + const linkPreviewsAllowed = 4 - var inlineElements = []; - var imagesAdded = 0; - var linkPreviewsAdded = 0; - const imagesAllowed = 4; - const linkPreviewsAllowed = 4; - - var linesFinished = 0; // the assumption is that the text parsing in parseText will not add or remove any lines - var withinList = false; - //the delta format stores a series of operations in "ops." in this context, they will all be "insert" ops, with their main content in .insert and the extra attributes of that content in .attributes - //the best way to understand this function is probably to step through it. basically, we start with a line "in progress" (the

      that starts the array above) and add each insert onto it, - //accompanied by the tags for its formatting, until we hit an end of line signal, when we finish the line with an end tag. it may turn out that the end of line is blockquote or list formatted, - //in which case we have to go back and start the line with the appropriate tag (or tags in the case of lists,

        and
      • ), and then for lists we start starting lines with
      • and then - //when we leave that formatting mode (when we hit a non list-fomatted newline) we have to end the previous (finished) line with
      • and
      and go back and start the current one with

      . - //embeds are added with a position attribute describing how many lines have been completed when they are encountered, which serves to place them in the text later. - for (var i = 0; i < delta.ops.length; i++) { - var op = delta.ops[i]; - if (typeof op.insert == "string" && (op.insert !== "" || op.attributes)) { - op.insert = this.escapeHTMLChars(op.insert); - var formattingStartTags = ""; - var formattingEndTags = ""; - if (op.attributes) { - // blockquote and list formatted lines end with "\n"s with that formatting attached, that's the only way you can tell what they are. as far as i can tell, it is guaranteed that only - //newlines will have this formatting. - if (op.attributes.blockquote) { - if (withinList) { - withinList = false; - linesOfParsedString[linesOfParsedString.length - 2] += "

    "; - linesOfParsedString[linesOfParsedString.length - 1] = "
    " + linesOfParsedString[linesOfParsedString.length - 1].substring(4, linesOfParsedString[linesOfParsedString.length - 1].length) + "
    "; - linesFinished++; - linesOfParsedString.push("

    "); - } else { - linesOfParsedString[linesOfParsedString.length - 1] = "

    " + linesOfParsedString[linesOfParsedString.length - 1].substring(3, linesOfParsedString[linesOfParsedString.length - 1].length) + "
    "; - linesFinished++; - linesOfParsedString.push("

    "); - } - continue; - } else if (op.attributes.list == "bullet") { - if (!withinList) { - withinList = true; - linesOfParsedString[linesOfParsedString.length - 1] = "

    • " + linesOfParsedString[linesOfParsedString.length - 1].substring(3, linesOfParsedString[linesOfParsedString.length - 1].length) + "
    • "; - linesFinished++; - linesOfParsedString.push("
    • "); - continue; - } else { - linesOfParsedString[linesOfParsedString.length - 1] += "
    • "; - linesFinished++; - linesOfParsedString.push("
    • "); - continue; - } - } - //other formatting is attached directly to the text it applies to - if (op.attributes.bold) { - formattingStartTags = "" + formattingStartTags; - formattingEndTags = formattingEndTags + ""; - } - if (op.attributes.italic) { - formattingStartTags = "" + formattingStartTags; - formattingEndTags = formattingEndTags + ""; - } - if (op.attributes.link) { - formattingStartTags = '' + formattingStartTags; - formattingEndTags = formattingEndTags + ""; - } - } - //splitting the string into lines like this means that the first element is part of the previous line and then all subsequent ones start and finish their own lines, except the last one doesn't finish its - var lines = op.insert.split('\n'); - linesOfParsedString[linesOfParsedString.length - 1] += formattingStartTags + lines[0] + formattingEndTags; - for (var i = 1; i < lines.length; i++) { - if (withinList) { - withinList = false; - linesOfParsedString[linesOfParsedString.length - 2] += "
    "; - linesOfParsedString[linesOfParsedString.length - 1] = "

    " + linesOfParsedString[linesOfParsedString.length - 1].substring(4, linesOfParsedString[linesOfParsedString.length - 1].length); - } - if (linesOfParsedString[linesOfParsedString.length - 1] == "

    ") { //if the line we're finishing has no actual content - linesOfParsedString[linesOfParsedString.length - 1] += "
    "; - } - linesOfParsedString[linesOfParsedString.length - 1] += "

    "; - linesFinished++; - linesOfParsedString.push("

    " + formattingStartTags + lines[i] + formattingEndTags); - } - } else if (op.insert.LinkPreview && linkPreviewsAdded <= linkPreviewsAllowed) { - //i'm assuming that there will always be a newline text insert in the text right before an inline embed, so we don't have to go through the end-of-line process - var embed = op.attributes; - embed.type = "link-preview"; - embed.position = linesFinished; - embed.linkUrl = (op.insert.LinkPreview.includes("//") ? "" : "//") + op.insert.LinkPreview; - if (youtubeUrlFindingRegex.test(op.insert.LinkPreview)) { - embed.isEmbeddableVideo = true; - embed.embedUrl = "https://www.youtube.com/embed/" + youtubeUrlFindingRegex.exec(op.insert.LinkPreview)[5] + "?autoplay=1"; - } else if (vimeoUrlFindingRegex.test(op.insert.LinkPreview)) { - embed.isEmbeddableVideo = true; - embed.embedUrl = "https://www.youtube.com/embed/" + vimeoUrlFindingRegex.exec(op.insert.LinkPreview)[4] + "?autoplay=1"; - } else { - embed.isEmbeddableVideo = false; - } - inlineElements.push(embed); - linkPreviewsAdded++; + var linesFinished = 0 // the assumption is that the text parsing in parseText will not add or remove any lines + var withinList = false + // the delta format stores a series of operations in "ops." in this context, they will all be "insert" ops, with their main content in .insert and the extra attributes of that content in .attributes + // the best way to understand this function is probably to step through it. basically, we start with a line "in progress" (the

    that starts the array above) and add each insert onto it, + // accompanied by the tags for its formatting, until we hit an end of line signal, when we finish the line with an end tag. it may turn out that the end of line is blockquote or list formatted, + // in which case we have to go back and start the line with the appropriate tag (or tags in the case of lists,

      and
    • ), and then for lists we start starting lines with
    • and then + // when we leave that formatting mode (when we hit a non list-fomatted newline) we have to end the previous (finished) line with
    • and
    and go back and start the current one with

    . + // embeds are added with a position attribute describing how many lines have been completed when they are encountered, which serves to place them in the text later. + for (var i = 0; i < delta.ops.length; i++) { + var op = delta.ops[i] + if (typeof op.insert === 'string' && (op.insert !== '' || op.attributes)) { + op.insert = this.escapeHTMLChars(op.insert) + var formattingStartTags = '' + var formattingEndTags = '' + if (op.attributes) { + // blockquote and list formatted lines end with "\n"s with that formatting attached, that's the only way you can tell what they are. as far as i can tell, it is guaranteed that only + // newlines will have this formatting. + if (op.attributes.blockquote) { + if (withinList) { + withinList = false + linesOfParsedString[linesOfParsedString.length - 2] += '

' + linesOfParsedString[linesOfParsedString.length - 1] = '
' + linesOfParsedString[linesOfParsedString.length - 1].substring(4, linesOfParsedString[linesOfParsedString.length - 1].length) + '
' + linesFinished++ + linesOfParsedString.push('

') + } else { + linesOfParsedString[linesOfParsedString.length - 1] = '

' + linesOfParsedString[linesOfParsedString.length - 1].substring(3, linesOfParsedString[linesOfParsedString.length - 1].length) + '
' + linesFinished++ + linesOfParsedString.push('

') + } + continue + } else if (op.attributes.list == 'bullet') { + if (!withinList) { + withinList = true + linesOfParsedString[linesOfParsedString.length - 1] = '

  • ' + linesOfParsedString[linesOfParsedString.length - 1].substring(3, linesOfParsedString[linesOfParsedString.length - 1].length) + '
  • ' + linesFinished++ + linesOfParsedString.push('
  • ') + continue + } else { + linesOfParsedString[linesOfParsedString.length - 1] += '
  • ' + linesFinished++ + linesOfParsedString.push('
  • ') + continue + } + } + // other formatting is attached directly to the text it applies to + if (op.attributes.bold) { + formattingStartTags = '' + formattingStartTags + formattingEndTags = formattingEndTags + '' + } + if (op.attributes.italic) { + formattingStartTags = '' + formattingStartTags + formattingEndTags = formattingEndTags + '' + } + if (op.attributes.link) { + formattingStartTags = '' + formattingStartTags + formattingEndTags = formattingEndTags + '' + } + } + // splitting the string into lines like this means that the first element is part of the previous line and then all subsequent ones start and finish their own lines, except the last one doesn't finish its + var lines = op.insert.split('\n') + linesOfParsedString[linesOfParsedString.length - 1] += formattingStartTags + lines[0] + formattingEndTags + for (var i = 1; i < lines.length; i++) { + if (withinList) { + withinList = false + linesOfParsedString[linesOfParsedString.length - 2] += '
' + linesOfParsedString[linesOfParsedString.length - 1] = '

' + linesOfParsedString[linesOfParsedString.length - 1].substring(4, linesOfParsedString[linesOfParsedString.length - 1].length) + } + if (linesOfParsedString[linesOfParsedString.length - 1] == '

') { // if the line we're finishing has no actual content + linesOfParsedString[linesOfParsedString.length - 1] += '
' + } + linesOfParsedString[linesOfParsedString.length - 1] += '

' + linesFinished++ + linesOfParsedString.push('

' + formattingStartTags + lines[i] + formattingEndTags) + } + } else if (op.insert.LinkPreview && linkPreviewsAdded <= linkPreviewsAllowed) { + // i'm assuming that there will always be a newline text insert in the text right before an inline embed, so we don't have to go through the end-of-line process + var embed = op.attributes + embed.type = 'link-preview' + embed.position = linesFinished + embed.linkUrl = (op.insert.LinkPreview.includes('//') ? '' : '//') + op.insert.LinkPreview + if (youtubeUrlFindingRegex.test(op.insert.LinkPreview)) { + embed.isEmbeddableVideo = true + embed.embedUrl = 'https://www.youtube.com/embed/' + youtubeUrlFindingRegex.exec(op.insert.LinkPreview)[5] + '?autoplay=1' + } else if (vimeoUrlFindingRegex.test(op.insert.LinkPreview)) { + embed.isEmbeddableVideo = true + embed.embedUrl = 'https://www.youtube.com/embed/' + vimeoUrlFindingRegex.exec(op.insert.LinkPreview)[4] + '?autoplay=1' + } else { + embed.isEmbeddableVideo = false + } + inlineElements.push(embed) + linkPreviewsAdded++ - console.log("link preview on line: " + linesFinished); - console.log("it is to " + op.insert.LinkPreview); - if (embed.isEmbeddableVideo) { - console.log("it is an embeddable video"); - } - } else if (op.insert.PostImage && imagesAdded <= imagesAllowed && op.attributes.imageURL != "loading...") { - if (imagesAdded > 0 && inlineElements[inlineElements.length - 1].type == "image(s)" && inlineElements[inlineElements.length - 1].position == linesFinished) { - var image = inlineElements[inlineElements.length - 1]; //the below should modify this actual array element - } else { - var image = { images: [], imageDescriptions: [], position: linesFinished, type: "image(s)" }; - inlineElements.push(image); - } - image.images.push(op.attributes.imageURL); - image.imageDescriptions.push(op.attributes.description); - imagesAdded++; + console.log('link preview on line: ' + linesFinished) + console.log('it is to ' + op.insert.LinkPreview) + if (embed.isEmbeddableVideo) { + console.log('it is an embeddable video') + } + } else if (op.insert.PostImage && imagesAdded <= imagesAllowed && op.attributes.imageURL != 'loading...') { + if (imagesAdded > 0 && inlineElements[inlineElements.length - 1].type == 'image(s)' && inlineElements[inlineElements.length - 1].position == linesFinished) { + var image = inlineElements[inlineElements.length - 1] // the below should modify this actual array element + } else { + var image = { images: [], imageDescriptions: [], position: linesFinished, type: 'image(s)' } + inlineElements.push(image) + } + image.images.push(op.attributes.imageURL) + image.imageDescriptions.push(op.attributes.description) + imagesAdded++ - console.log("image on line: " + linesFinished); - console.log("its file name will be " + op.attributes.imageURL); - console.log("its description is " + op.attributes.description); - } + console.log('image on line: ' + linesFinished) + console.log('its file name will be ' + op.attributes.imageURL) + console.log('its description is ' + op.attributes.description) } - if (withinList) { - if (typeof delta.ops[delta.ops.length - 1].insert == "string" && (!delta.ops[delta.ops.length - 1].attributes || !delta.ops[delta.ops.length - 1].attributes.list)) { - linesOfParsedString[linesOfParsedString.length - 2] += ""; - linesOfParsedString[linesOfParsedString.length - 1] = "

" + linesOfParsedString[linesOfParsedString.length - 1].substring(4, linesOfParsedString[linesOfParsedString.length - 1].length) + "

"; - } else { - linesOfParsedString[linesOfParsedString.length - 2] += ""; - linesOfParsedString.pop(); - } + } + if (withinList) { + if (typeof delta.ops[delta.ops.length - 1].insert === 'string' && (!delta.ops[delta.ops.length - 1].attributes || !delta.ops[delta.ops.length - 1].attributes.list)) { + linesOfParsedString[linesOfParsedString.length - 2] += '' + linesOfParsedString[linesOfParsedString.length - 1] = '

' + linesOfParsedString[linesOfParsedString.length - 1].substring(4, linesOfParsedString[linesOfParsedString.length - 1].length) + '

' } else { - if ((typeof delta.ops[delta.ops.length - 1].insert != "string")) { - linesOfParsedString.pop(); - } else { - if (linesOfParsedString[linesOfParsedString.length - 1] == "

") { - linesOfParsedString[linesOfParsedString.length - 1] += "

"; - } else { - linesOfParsedString[linesOfParsedString.length - 1] += "

"; - } - } + linesOfParsedString[linesOfParsedString.length - 2] += '' + linesOfParsedString.pop() + } + } else { + if ((typeof delta.ops[delta.ops.length - 1].insert !== 'string')) { + linesOfParsedString.pop() + } else { + if (linesOfParsedString[linesOfParsedString.length - 1] == '

') { + linesOfParsedString[linesOfParsedString.length - 1] += '

' + } else { + linesOfParsedString[linesOfParsedString.length - 1] += '

' + } } - console.log("finished html:"); - console.log(linesOfParsedString.join("\n")); - return { text: linesOfParsedString.join(""), inlineElements: inlineElements }; -} \ No newline at end of file + } + console.log('finished html:') + console.log(linesOfParsedString.join('\n')) + return { text: linesOfParsedString.join(''), inlineElements: inlineElements } +} diff --git a/app/viewingSweet.js b/app/viewingSweet.js index dbd09f17..84dc57eb 100644 --- a/app/viewingSweet.js +++ b/app/viewingSweet.js @@ -1,1454 +1,1465 @@ -var auth = require('../config/auth.js'); //used on the settings page to set up push notifications - -// APIs - -var apiConfig = require('../config/apis.js'); - -const sgMail = require('@sendgrid/mail'); -sgMail.setApiKey(apiConfig.sendgrid); - - -module.exports = function(app) { - - //Responds to get requests for images on the server. If the image is private, checks to see - //if the user is trusted/in the community first. - //Input: URL of an image - //Output: Responds with either the image file or a redirect response to /404 with 404 status. - app.get('/api/image/display/:filename', function(req, res) { - - function sendImageFile() { - imagePath = global.appRoot + '/cdn/images/' + req.params.filename; - try { - if (fs.existsSync(imagePath)) { - res.sendFile(imagePath); - } - } catch (err) { - // Image file doesn't exist on server - console.log("Image " + req.params.filename + " doesn't exist on server!") - console.log(err) - res.status('404') - res.redirect('/404'); - } +const fs = require('fs') +const path = require('path') +const bcrypt = require('bcrypt-nodejs') +const User = require('./models/user') +const Relationship = require('./models/relationship') +const Community = require('./models/community') + +// used on the settings page to set up push notifications +var auth = require('../config/auth.js') + +module.exports = function (app) { + // Responds to get requests for images on the server. If the image is private, checks to see + // if the user is trusted/in the community first. + // Input: URL of an image + // Output: Responds with either the image file or a redirect response to /404 with 404 status. + app.get('/api/image/display/:filename', function (req, res) { + function sendImageFile () { + imagePath = global.appRoot + '/cdn/images/' + req.params.filename + try { + if (fs.existsSync(imagePath)) { + res.sendFile(imagePath) } + } catch (err) { + // Image file doesn't exist on server + console.log('Image ' + req.params.filename + " doesn't exist on server!") + console.log(err) + res.status('404') + res.redirect('/404') + } + } - Image.findOne({ filename: req.params.filename }).then(image => { - if (image) { - if (image.privacy === "public") { - sendImageFile() - } else if (image.privacy === "private") { - if (req.isAuthenticated()) { - if (image.user == req.user._id.toString()) { - sendImageFile() - } else if (image.context === "user") { - Relationship.findOne({ toUser: req.user._id, value: "trust", fromUser: image.user }).then(rel => { - if (rel) { - sendImageFile() - } else { - // User not trusted by image's uploader - console.log("User not trusted!") - res.status('404') - res.redirect('/404'); - } - }) - } else if (image.context === "community") { - Community.findOne({ _id: image.community, members: req.user._id }).then(comm => { - if (comm) { - sendImageFile() - } else { - // User not a member of this community - console.log(req); - console.log(image); - console.log("User not a community member!") - res.status('404') - res.redirect('/404'); - } - }) - } - } else { - // User not logged in, but has to be to see this image - console.log("User not logged in!") - res.status('404'); - res.redirect('/404'); - } - } + Image.findOne({ filename: req.params.filename }).then(image => { + if (image) { + if (image.privacy === 'public') { + sendImageFile() + } else if (image.privacy === 'private') { + if (req.isAuthenticated()) { + if (image.user == req.user._id.toString()) { + sendImageFile() + } else if (image.context === 'user') { + Relationship.findOne({ toUser: req.user._id, value: 'trust', fromUser: image.user }).then(rel => { + if (rel) { + sendImageFile() } else { - // Image entry not found in database - console.log("Image " + image.filename + " not in database!") - res.status('404'); - res.redirect('/404'); + // User not trusted by image's uploader + console.log('User not trusted!') + res.status('404') + res.redirect('/404') } - }) - .catch(error => { - // Unexpected error - console.log("Unexpected error displaying image") - console.log(error) - res.status('404'); - res.redirect('/404'); - }) - }) - - //Responds to get requests for '/'. - //Input: none - //Output: redirect to '/home' if logged in, render of the index page if logged out. - app.get('/', function(req, res) { - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1. - res.setHeader("Pragma", "no-cache"); // HTTP 1.0. - res.setHeader("Expires", "0"); // Proxies. - if (req.isAuthenticated()) { - res.redirect('/home'); - } else { - User.count().then(users => { - Community.find().sort('-lastUpdated').then(communities => { - var publicCommunites = communities.filter(c => c.settings.visibility == "public" && c.settings.joinType == "open"); - publicCommunites.sort(function() { - return .5 - Math.random(); - }); - publicCommunites.length = 8; - res.render('index', { layout: 'logged-out', userCount: users, communities: publicCommunites, communityCount: communities.length, sessionFlash: res.locals.sessionFlash }); - }) - }) - } - }); - - //Responds to get requests for the login page. - //Input: flash message - //Output: rendering of the login page with the flash message included. - app.get('/login', function(req, res) { - if (req.isAuthenticated()) { - return res.redirect('back'); - } - res.render('login', { - layout: 'logged-out', - sessionFlash: res.locals.sessionFlash - }); - }); - - //Responds to get requests for the signup page. - //Input: flash message - //Output: rendering of the signup page with the flash message included. - app.get('/signup', function(req, res) { - if (req.isAuthenticated()) { - return res.redirect('back'); - } - res.render('signup', { - layout: 'logged-out', - sessionFlash: res.locals.sessionFlash - }); - }); - - //Responds to get requests for the profile of someone with a certain email. Deprecated? Is this used? - //Input: the email - //Output: redirect to /the username of the person with that email, unless isLoggedInOrRedirect redirects you - app.get('/getprofile/:email', isLoggedInOrRedirect, function(req, res) { - User.findOne({ - email: req.params.email - }).then((user) => { - res.redirect('/' + user.username); - }) - }); - - //Responds to get requests for the home page. - //Input: none - //Output: the home page, if isLoggedInOrRedirect doesn't redirect you. - app.get('/home', isLoggedInOrRedirect, async function(req, res) { - async function getRecommendations() { - //console.time('getRecommendationsFunction') - popularCommunities = []; - recommendedUsers = {}; - relationshipWeights = { - "trust": 2, - "follow": 0.5 - }; - lastFortnight = moment(new Date()).subtract(14, 'days'); - async function getRelationships(id, type) { - var users = {}; - return Relationship.find({ - fromUser: id, - value: type - }) - .then((relationships) => { - relationships.forEach(relationship => { - if (!relationship.toUser.equals(req.user._id)) { - let id = relationship.toUser.toString(); - let weight = relationshipWeights[relationship.value] - if (!(id in users)) { - users[id] = weight; - } else { - users[id] += weight; - } - } - }) - return users; - }) - } - //console.time('popularHashtags') - popularHashtags = await Tag.find() - .limit(15) - .sort('-lastUpdated') - .then(tags => { - return tags; - }) - //console.timeEnd('popularHashtags') - - // Trusted and followed users of people the user - // trusts or follows are retrieved and placed in - // an array with weighted scores - trust gives a - // score of 2, following gives a score of 0.5. - // (The scores have been arbitrarily selected.) - - //console.time('recommendedUsers') - primaryRelationships = await getRelationships(req.user._id, ["trust", "follow"]); - for (const primaryUser in primaryRelationships) { - const secondaryRelationships = await getRelationships(primaryUser, ["trust", "follow"]) - for (const secondaryUser in secondaryRelationships) { - let id = secondaryUser; - let weight = secondaryRelationships[secondaryUser] - if (!(id in recommendedUsers)) { - recommendedUsers[id] = weight; - } else { - recommendedUsers[id] += weight; - } + }) + } else if (image.context === 'community') { + Community.findOne({ _id: image.community, members: req.user._id }).then(comm => { + if (comm) { + sendImageFile() + } else { + // User not a member of this community + console.log(req) + console.log(image) + console.log('User not a community member!') + res.status('404') + res.redirect('/404') } + }) } - recommendedUserIds = Object.keys(recommendedUsers) - //console.timeEnd('recommendedUsers') - - usersKnown = Object.keys(primaryRelationships) - - // Shows all recently active communities if the user's only friend is sweetbot, - // otherwise only recently active communities with a friend in them - if (usersKnown.length == 1 && usersKnown[0] === "5c962bccf0b0d14286e99b68") { - membersQuery = {} - } else { - membersQuery = { members: { $in: usersKnown } } - } - popularCommunities = await Community.find({ - $and: [ - { lastUpdated: { $gt: lastFortnight } }, - { members: { $ne: req.user._id } }, - membersQuery - ] - }) - .then(communities => { - return communities; - }) - - return User.findOne({ - _id: req.user._id - }) - .then(user => { - //console.time('userFunctions') - popularCommunities = popularCommunities.filter(e => !user.hiddenRecommendedCommunities.includes(e._id.toString())) - - if (popularCommunities.length > 16) - popularCommunities.length = 16; - - unknownUsers = recommendedUserIds.filter(e => !usersKnown.includes(e)); - unknownUsers = unknownUsers.filter(e => !user.hiddenRecommendedUsers.includes(e)); - - if (unknownUsers.length > 16) - unknownUsers.length = 16; - - return User.find({ - _id: unknownUsers - }, { username: 1, image: 1, imageEnabled: 1, displayName: 1 }) - .then(userData => { - userData.forEach(user => { - user.weight = recommendedUsers[user._id.toString()]; - }) - userData.sort((a, b) => (a.weight > b.weight) ? -1 : 1) - results = { - popularCommunities: popularCommunities, - userRecommendations: userData, - popularHashtags: popularHashtags - } - //console.timeEnd('userFunctions') - //console.timeEnd('getRecommendationsFunction') - return results; - }) - }); - } - recommendations = await getRecommendations(); - res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1. - res.setHeader("Pragma", "no-cache"); // HTTP 1.0. - res.setHeader("Expires", "0"); // Proxies. - res.render('home', { - loggedIn: true, - loggedInUserData: req.user, - activePage: 'home', - popularCommunities: (recommendations.popularCommunities.length > 0 ? recommendations.popularCommunities : false), - userRecommendations: (recommendations.userRecommendations.length > 0 ? recommendations.userRecommendations : false), - popularHashtags: (recommendations.popularHashtags.length > 0 ? recommendations.popularHashtags : false) - }); - }); - - //Responds to get requests for the 404 page. - //Input: user data from req.user - //Output: the 404 page - app.get('/404', function(req, res) { - if (req.isAuthenticated()) { - res.render('404', { loggedIn: true, loggedInUserData: req.user }); - } else { - res.render('404', { loggedIn: false }); + } else { + // User not logged in, but has to be to see this image + console.log('User not logged in!') + res.status('404') + res.redirect('/404') + } } - }); - - //Responds to get requests for tag pages. - //Input: the name of the tag from the url - //Output: the tag page rendered if it exists, redirect to the 404 page otherwise, unless isLoggedInOrRedirect redirects you - app.get('/tag/:name', isLoggedInOrRedirect, function(req, res) { - Tag.findOne({ name: req.params.name }).then((tag) => { - if (tag) { - res.render('tag', { - name: req.params.name, - loggedIn: true, - loggedInUserData: req.user - }) - } else { - res.redirect('/404'); - } + } else { + // Image entry not found in database + console.log('Image ' + image.filename + ' not in database!') + res.status('404') + res.redirect('/404') + } + }) + .catch(error => { + // Unexpected error + console.log('Unexpected error displaying image') + console.log(error) + res.status('404') + res.redirect('/404') + }) + }) + + // Responds to get requests for '/'. + // Input: none + // Output: redirect to '/home' if logged in, render of the index page if logged out. + app.get('/', function (req, res) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') // HTTP 1.1. + res.setHeader('Pragma', 'no-cache') // HTTP 1.0. + res.setHeader('Expires', '0') // Proxies. + if (req.isAuthenticated()) { + res.redirect('/home') + } else { + User.count().then(users => { + Community.find().sort('-lastUpdated').then(communities => { + var publicCommunites = communities.filter(c => c.settings.visibility == 'public' && c.settings.joinType == 'open') + publicCommunites.sort(function () { + return 0.5 - Math.random() + }) + publicCommunites.length = 8 + res.render('index', { layout: 'logged-out', userCount: users, communities: publicCommunites, communityCount: communities.length, sessionFlash: res.locals.sessionFlash }) }) + }) + } + }) + + // Responds to get requests for the login page. + // Input: flash message + // Output: rendering of the login page with the flash message included. + app.get('/login', function (req, res) { + if (req.isAuthenticated()) { + return res.redirect('back') + } + res.render('login', { + layout: 'logged-out', + sessionFlash: res.locals.sessionFlash }) + }) - //Responds to get requests for /notifications. I think this is only used on mobile? - //Input: none - //Output: renders notifications page, which renders as "you're not logged in" if you're not logged in - app.get('/notifications', function(req, res) { - if (req.isAuthenticated()) { - User.findOne({ _id: req.user._id }, 'notifications').then(user => { - user.notifications.reverse(); - res.render('notifications', { - loggedIn: true, - loggedInUserData: req.user, - notifications: user.notifications, - activePage: 'notifications' - }); + // Responds to get requests for the signup page. + // Input: flash message + // Output: rendering of the signup page with the flash message included. + app.get('/signup', function (req, res) { + if (req.isAuthenticated()) { + return res.redirect('back') + } + res.render('signup', { + layout: 'logged-out', + sessionFlash: res.locals.sessionFlash + }) + }) + + // Responds to get requests for the profile of someone with a certain email. Deprecated? Is this used? + // Input: the email + // Output: redirect to /the username of the person with that email, unless isLoggedInOrRedirect redirects you + app.get('/getprofile/:email', isLoggedInOrRedirect, function (req, res) { + User.findOne({ + email: req.params.email + }).then((user) => { + res.redirect('/' + user.username) + }) + }) + + // Responds to get requests for the home page. + // Input: none + // Output: the home page, if isLoggedInOrRedirect doesn't redirect you. + app.get('/home', isLoggedInOrRedirect, async function (req, res) { + async function getRecommendations () { + // console.time('getRecommendationsFunction') + popularCommunities = [] + recommendedUsers = {} + relationshipWeights = { + trust: 2, + follow: 0.5 + } + lastFortnight = moment(new Date()).subtract(14, 'days') + async function getRelationships (id, type) { + var users = {} + return Relationship.find({ + fromUser: id, + value: type + }) + .then((relationships) => { + relationships.forEach(relationship => { + if (!relationship.toUser.equals(req.user._id)) { + const id = relationship.toUser.toString() + const weight = relationshipWeights[relationship.value] + if (!(id in users)) { + users[id] = weight + } else { + users[id] += weight + } + } }) - } else { - res.render('notifications', { - loggedIn: false, - activePage: 'notifications' - }); + return users + }) + } + // console.time('popularHashtags') + popularHashtags = await Tag.find() + .limit(15) + .sort('-lastUpdated') + .then(tags => { + return tags + }) + // console.timeEnd('popularHashtags') + + // Trusted and followed users of people the user + // trusts or follows are retrieved and placed in + // an array with weighted scores - trust gives a + // score of 2, following gives a score of 0.5. + // (The scores have been arbitrarily selected.) + + // console.time('recommendedUsers') + primaryRelationships = await getRelationships(req.user._id, ['trust', 'follow']) + for (const primaryUser in primaryRelationships) { + const secondaryRelationships = await getRelationships(primaryUser, ['trust', 'follow']) + for (const secondaryUser in secondaryRelationships) { + const id = secondaryUser + const weight = secondaryRelationships[secondaryUser] + if (!(id in recommendedUsers)) { + recommendedUsers[id] = weight + } else { + recommendedUsers[id] += weight + } } - }) - - //Responds to get request for /settings - //Input: none - //Output: the settings page is rendered, unless isLoggedInOrRedirect redirects you first. - app.get('/settings', isLoggedInOrRedirect, function(req, res) { - res.render('settings', { - loggedIn: true, - loggedInUserData: req.user, - notifierPublicKey: auth.vapidPublicKey, - activePage: 'settings' + } + recommendedUserIds = Object.keys(recommendedUsers) + // console.timeEnd('recommendedUsers') + + usersKnown = Object.keys(primaryRelationships) + + // Shows all recently active communities if the user's only friend is sweetbot, + // otherwise only recently active communities with a friend in them + if (usersKnown.length == 1 && usersKnown[0] === '5c962bccf0b0d14286e99b68') { + membersQuery = {} + } else { + membersQuery = { members: { $in: usersKnown } } + } + popularCommunities = await Community.find({ + $and: [ + { lastUpdated: { $gt: lastFortnight } }, + { members: { $ne: req.user._id } }, + membersQuery + ] + }) + .then(communities => { + return communities }) - }) - app.get('/support', isLoggedInOrRedirect, function(req, res) { - res.render('support', { - loggedIn: true, - loggedInUserData: req.user, - activePage: 'support' + return User.findOne({ + _id: req.user._id + }) + .then(user => { + // console.time('userFunctions') + popularCommunities = popularCommunities.filter(e => !user.hiddenRecommendedCommunities.includes(e._id.toString())) + + if (popularCommunities.length > 16) { popularCommunities.length = 16 } + + unknownUsers = recommendedUserIds.filter(e => !usersKnown.includes(e)) + unknownUsers = unknownUsers.filter(e => !user.hiddenRecommendedUsers.includes(e)) + + if (unknownUsers.length > 16) { unknownUsers.length = 16 } + + return User.find({ + _id: unknownUsers + }, { username: 1, image: 1, imageEnabled: 1, displayName: 1 }) + .then(userData => { + userData.forEach(user => { + user.weight = recommendedUsers[user._id.toString()] + }) + userData.sort((a, b) => (a.weight > b.weight) ? -1 : 1) + results = { + popularCommunities: popularCommunities, + userRecommendations: userData, + popularHashtags: popularHashtags + } + // console.timeEnd('userFunctions') + // console.timeEnd('getRecommendationsFunction') + return results + }) }) + } + recommendations = await getRecommendations() + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') // HTTP 1.1. + res.setHeader('Pragma', 'no-cache') // HTTP 1.0. + res.setHeader('Expires', '0') // Proxies. + res.render('home', { + loggedIn: true, + loggedInUserData: req.user, + activePage: 'home', + popularCommunities: (recommendations.popularCommunities.length > 0 ? recommendations.popularCommunities : false), + userRecommendations: (recommendations.userRecommendations.length > 0 ? recommendations.userRecommendations : false), + popularHashtags: (recommendations.popularHashtags.length > 0 ? recommendations.popularHashtags : false) }) + }) - app.get('/about', function(req, res) { - res.render('about', { - loggedIn: req.isAuthenticated(), - loggedInUserData: req.user, - activePage: 'support' + // Responds to get requests for the 404 page. + // Input: user data from req.user + // Output: the 404 page + app.get('/404', function (req, res) { + if (req.isAuthenticated()) { + res.render('404', { loggedIn: true, loggedInUserData: req.user }) + } else { + res.render('404', { loggedIn: false }) + } + }) + + // Responds to get requests for tag pages. + // Input: the name of the tag from the url + // Output: the tag page rendered if it exists, redirect to the 404 page otherwise, unless isLoggedInOrRedirect redirects you + app.get('/tag/:name', isLoggedInOrRedirect, function (req, res) { + Tag.findOne({ name: req.params.name }).then((tag) => { + if (tag) { + res.render('tag', { + name: req.params.name, + loggedIn: true, + loggedInUserData: req.user }) + } else { + res.redirect('/404') + } }) + }) - //Responds to get requests for /search. - //Input: none - //Output: renders search page unless isLoggedInOrRedirect redirects you - app.get('/search', isLoggedInOrRedirect, function(req, res) { - res.render('search', { - loggedIn: true, - loggedInUserData: req.user, - activePage: 'search' + // Responds to get requests for /notifications. I think this is only used on mobile? + // Input: none + // Output: renders notifications page, which renders as "you're not logged in" if you're not logged in + app.get('/notifications', function (req, res) { + if (req.isAuthenticated()) { + User.findOne({ _id: req.user._id }, 'notifications').then(user => { + user.notifications.reverse() + res.render('notifications', { + loggedIn: true, + loggedInUserData: req.user, + notifications: user.notifications, + activePage: 'notifications' }) + }) + } else { + res.render('notifications', { + loggedIn: false, + activePage: 'notifications' + }) + } + }) + + // Responds to get request for /settings + // Input: none + // Output: the settings page is rendered, unless isLoggedInOrRedirect redirects you first. + app.get('/settings', isLoggedInOrRedirect, function (req, res) { + res.render('settings', { + loggedIn: true, + loggedInUserData: req.user, + notifierPublicKey: auth.vapidPublicKey, + activePage: 'settings' }) + }) - //Responds to get requests for /search that include a query. - //Input: the query - //Output: the rendered search page, unless isLoggedInOrRedirect redirects you - app.get('/search/:query', isLoggedInOrRedirect, function(req, res) { - res.render('search', { - loggedIn: true, - loggedInUserData: req.user, - activePage: 'search', - query: req.params.query - }) + app.get('/support', isLoggedInOrRedirect, function (req, res) { + res.render('support', { + loggedIn: true, + loggedInUserData: req.user, + activePage: 'support' }) + }) - //Responds to post requests (?) for the users that follow the logged in user. used to build the @ mention list for tribute to auto-suggest stuff. - //Input: none - //Output: JSON data describing the users that follow the logged in user or a redirect from isLoggedInOrRedirect. - app.post('/api/user/followers', isLoggedInOrRedirect, function(req, res) { - followedUserData = [] - Relationship.find({ fromUser: req.user._id, value: "follow" }).populate("toUser").then((followedUsers) => { - followedUsers.forEach(relationship => { - console.log(relationship) - var follower = { - key: helper.escapeHTMLChars(relationship.toUser.displayName ? relationship.toUser.displayName + ' (' + '@' + relationship.toUser.username + ')' : '@' + relationship.toUser.username), - value: relationship.toUser.username, - image: (relationship.toUser.imageEnabled ? relationship.toUser.image : 'cake.svg') - } - followedUserData.push(follower); - }) - res.setHeader('content-type', 'text/plain'); - res.end(JSON.stringify({ followers: followedUserData })); - }) - .catch((err) => { - console.log("Error in profileData.") - console.log(err); - }); + app.get('/about', function (req, res) { + res.render('about', { + loggedIn: req.isAuthenticated(), + loggedInUserData: req.user, + activePage: 'support' }) - - //Responds to requests for search queries by page. - //Input: query, timestamp of oldest result yet loaded (in milliseconds) - //Output: 404 response if no results, the rendered search results otherwise, unless isLoggedInOrRedirect redirects you - app.get('/showsearch/:query/:olderthanthis', isLoggedInOrRedirect, function(req, res) { - - let resultsPerPage = 10; - let olderthan = new Date(parseInt(req.params.olderthanthis)) - - let query = req.params.query.trim(); - if (!query.length) { - res.status(404).send('Not found'); - } else { - Tag.find({ - name: { - '$regex': query, - '$options': 'i' - }, - lastUpdated: { $lt: olderthan } - }) - .sort('-lastUpdated') - .limit(resultsPerPage) // this won't completely keep us from getting more than we need, but the point is we'll never need more results than this per page load from any collection - .then(tagResults => { - User.find({ - "$or": [{ - username: { - '$regex': query, - '$options': 'i' - } - }, - { - displayName: { - '$regex': query, - '$options': 'i' - } - }, - { - aboutParsed: { - '$regex': query, - '$options': 'i' - } - } - ], - lastUpdated: { $lt: olderthan } - }) - .sort('-lastUpdated') - .limit(resultsPerPage) - .then((userResults) => { - Community.find({ - "$or": [{ - name: { - '$regex': query, - '$options': 'i' - } - }, - { - descriptionParsed: { - '$regex': query, - '$options': 'i' - } - } - ], - lastUpdated: { $lt: olderthan } - }) - .sort('-lastUpdated') - .limit(resultsPerPage) - .then(communityResults => { - var combinedResults = userResults.concat(communityResults, tagResults); - if (!combinedResults.length) { - res.sendStatus(404) - } else { - var parsedResults = []; - combinedResults.forEach(result => { - var constructedResult = {}; - if (result.username) { - // It's a user - constructedResult.type = ' User' - constructedResult.sort = result.lastUpdated - constructedResult.email = result.email - if (result.displayName) { - constructedResult.title = '' + result.displayName + ' · @' + result.username + ''; - } else { - constructedResult.title = '@' + result.username + ''; - } - constructedResult.url = '/' + result.username - if (result.imageEnabled) - constructedResult.image = '/images/' + result.image - else - constructedResult.image = '/images/cake.svg' - constructedResult.description = result.aboutParsed - } else if (result.members) { - // It's a community - constructedResult.type = ' Community' - constructedResult.sort = result.lastUpdated - constructedResult.title = '' + result.name + ' · ' + result.membersCount + ' member' + (result.membersCount == 1 ? '' : 's') + ''; - constructedResult.url = '/community/' + result.slug - if (result.imageEnabled) - constructedResult.image = '/images/communities/' + result.image - else - constructedResult.image = '/images/communities/cake.svg' - constructedResult.description = result.descriptionParsed - } else { - // It's a tag - constructedResult.type = ' Tag' - constructedResult.sort = result.lastUpdated - constructedResult.title = '#' + result.name + ''; - constructedResult.url = '/tag/' + result.name - constructedResult.description = '

' + result.posts.length + ' post' + (result.posts.length == 1 ? '' : 's') + '

' - constructedResult.image = '/images/biscuit.svg' - } - parsedResults.push(constructedResult) - }) - parsedResults.sort(function(a, b) { - var timestampA = a.sort, - timestampB = b.sort; - if (timestampA > timestampB) //sort timestamp descending - return -1; - if (timestampA < timestampB) - return 1; - return 0; - }); - parsedResults = parsedResults.slice(0, resultsPerPage); - var oldesttimestamp = parsedResults[parsedResults.length - 1].sort; - res.render('partials/searchresults', { - layout: false, - loggedIn: true, - loggedInUserData: req.user, - oldesttimestamp: oldesttimestamp.getTime(), - results: parsedResults.slice(0, resultsPerPage) - }); - } - }) - }) - }) + }) + + // Responds to get requests for /search. + // Input: none + // Output: renders search page unless isLoggedInOrRedirect redirects you + app.get('/search', isLoggedInOrRedirect, function (req, res) { + res.render('search', { + loggedIn: true, + loggedInUserData: req.user, + activePage: 'search' + }) + }) + + // Responds to get requests for /search that include a query. + // Input: the query + // Output: the rendered search page, unless isLoggedInOrRedirect redirects you + app.get('/search/:query', isLoggedInOrRedirect, function (req, res) { + res.render('search', { + loggedIn: true, + loggedInUserData: req.user, + activePage: 'search', + query: req.params.query + }) + }) + + // Responds to post requests (?) for the users that follow the logged in user. used to build the @ mention list for tribute to auto-suggest stuff. + // Input: none + // Output: JSON data describing the users that follow the logged in user or a redirect from isLoggedInOrRedirect. + app.post('/api/user/followers', isLoggedInOrRedirect, function (req, res) { + followedUserData = [] + Relationship.find({ fromUser: req.user._id, value: 'follow' }).populate('toUser').then((followedUsers) => { + followedUsers.forEach(relationship => { + console.log(relationship) + var follower = { + key: helper.escapeHTMLChars(relationship.toUser.displayName ? relationship.toUser.displayName + ' (' + '@' + relationship.toUser.username + ')' : '@' + relationship.toUser.username), + value: relationship.toUser.username, + image: (relationship.toUser.imageEnabled ? relationship.toUser.image : 'cake.svg') } + followedUserData.push(follower) + }) + res.setHeader('content-type', 'text/plain') + res.end(JSON.stringify({ followers: followedUserData })) }) - - app.get('/drafts/:olderthanthis', isLoggedInOrRedirect, function(req, res) { - Post.find({ type: "draft", author: req.user._id, timestamp: { $lt: new Date(parseInt(req.params.olderthanthis)) } }) - .sort('-timestamp').limit(10).populate('author').then(async posts => { - if (!posts.length) { - res.sendStatus(404); - } else { - for (var post of posts) { - await keepCachedHTMLUpToDate(post); - post.internalPostHTML = post.cachedHTML.fullContentHTML; - post.commentsDisabled = true; - post.isYourPost = true; - var momentTimestamp = moment(post.timestamp); - if (momentTimestamp.isSame(moment(), 'd')) { - post.parsedTimestamp = momentTimestamp.fromNow(); - } else if (momentTimestamp.isSame(moment(), 'y')) { - post.parsedTimestamp = momentTimestamp.format('D MMM'); + .catch((err) => { + console.log('Error in profileData.') + console.log(err) + }) + }) + + // Responds to requests for search queries by page. + // Input: query, timestamp of oldest result yet loaded (in milliseconds) + // Output: 404 response if no results, the rendered search results otherwise, unless isLoggedInOrRedirect redirects you + app.get('/showsearch/:query/:olderthanthis', isLoggedInOrRedirect, function (req, res) { + const resultsPerPage = 10 + const olderthan = new Date(parseInt(req.params.olderthanthis)) + + const query = req.params.query.trim() + if (!query.length) { + res.status(404).send('Not found') + } else { + Tag.find({ + name: { + $regex: query, + $options: 'i' + }, + lastUpdated: { $lt: olderthan } + }) + .sort('-lastUpdated') + .limit(resultsPerPage) // this won't completely keep us from getting more than we need, but the point is we'll never need more results than this per page load from any collection + .then(tagResults => { + User.find({ + $or: [{ + username: { + $regex: query, + $options: 'i' + } + }, + { + displayName: { + $regex: query, + $options: 'i' + } + }, + { + aboutParsed: { + $regex: query, + $options: 'i' + } + } + ], + lastUpdated: { $lt: olderthan } + }) + .sort('-lastUpdated') + .limit(resultsPerPage) + .then((userResults) => { + Community.find({ + $or: [{ + name: { + $regex: query, + $options: 'i' + } + }, + { + descriptionParsed: { + $regex: query, + $options: 'i' + } + } + ], + lastUpdated: { $lt: olderthan } + }) + .sort('-lastUpdated') + .limit(resultsPerPage) + .then(communityResults => { + var combinedResults = userResults.concat(communityResults, tagResults) + if (!combinedResults.length) { + res.sendStatus(404) + } else { + var parsedResults = [] + combinedResults.forEach(result => { + var constructedResult = {} + if (result.username) { + // It's a user + constructedResult.type = ' User' + constructedResult.sort = result.lastUpdated + constructedResult.email = result.email + if (result.displayName) { + constructedResult.title = '' + result.displayName + ' · @' + result.username + '' } else { - post.parsedTimestamp = momentTimestamp.format('D MMM YYYY'); + constructedResult.title = '@' + result.username + '' } - post.fullTimestamp = momentTimestamp.calendar(); - } - res.render('partials/posts_v2', { - layout: false, - loggedIn: true, - loggedInUserData: req.user, - posts: posts, - context: "drafts", - canReply: false, - isMuted: false, - oldesttimestamp: posts[posts.length - 1].timestamp.getTime() + "" + constructedResult.url = '/' + result.username + if (result.imageEnabled) { constructedResult.image = '/images/' + result.image } else { constructedResult.image = '/images/cake.svg' } + constructedResult.description = result.aboutParsed + } else if (result.members) { + // It's a community + constructedResult.type = ' Community' + constructedResult.sort = result.lastUpdated + constructedResult.title = '' + result.name + ' · ' + result.membersCount + ' member' + (result.membersCount == 1 ? '' : 's') + '' + constructedResult.url = '/community/' + result.slug + if (result.imageEnabled) { constructedResult.image = '/images/communities/' + result.image } else { constructedResult.image = '/images/communities/cake.svg' } + constructedResult.description = result.descriptionParsed + } else { + // It's a tag + constructedResult.type = ' Tag' + constructedResult.sort = result.lastUpdated + constructedResult.title = '#' + result.name + '' + constructedResult.url = '/tag/' + result.name + constructedResult.description = '

' + result.posts.length + ' post' + (result.posts.length == 1 ? '' : 's') + '

' + constructedResult.image = '/images/biscuit.svg' + } + parsedResults.push(constructedResult) }) - } + parsedResults.sort(function (a, b) { + var timestampA = a.sort + var timestampB = b.sort + if (timestampA > timestampB) // sort timestamp descending + { return -1 } + if (timestampA < timestampB) { return 1 } + return 0 + }) + parsedResults = parsedResults.slice(0, resultsPerPage) + var oldesttimestamp = parsedResults[parsedResults.length - 1].sort + res.render('partials/searchresults', { + layout: false, + loggedIn: true, + loggedInUserData: req.user, + oldesttimestamp: oldesttimestamp.getTime(), + results: parsedResults.slice(0, resultsPerPage) + }) + } + }) }) - }) + }) + } + }) - //this function checks if there are some newer posts than the given timestamp for the user's home feed. it duplicates some query logic from /showposts to do this. - app.get("/heyaretherenewposts/:newerthanthis", isLoggedInOrRedirect, async function(req, res) { - var myFollowedUserIds = ((await Relationship.find({ fromUser: req.user._id, value: "follow" })).map(v => v.toUser)); - var myMutedUserIds = ((await Relationship.find({ fromUser: req.user._id, value: "mute" })).map(v => v.toUser)); - var usersWhoTrustMe = ((await Relationship.find({ toUser: req.user._id, value: "trust" })).map(v => v.fromUser)); - var query = { - $and: [{ - $or: [ - { $and: [{ author: { $in: myFollowedUserIds } }, { $or: [{ type: { $ne: "community" } }, { community: { $in: req.user.communities } }] }] }, { community: { $in: req.user.communities } } - ] - }, - { $or: [{ privacy: "public" }, { author: { $in: usersWhoTrustMe } }] }, - { author: { $not: { $in: myMutedUserIds } } }, - { type: { $ne: "draft" } } - ], - }; - var sortMethod = req.user.settings.homeTagTimelineSorting == "fluid" ? "lastUpdated" : "timestamp"; - var newerThanDate = new Date(parseInt(req.params.newerthanthis)); - var newerThanQuery = {}; - newerThanQuery[sortMethod] = { $gt: newerThanDate }; - query.$and.push(newerThanQuery); - Post.find(query).then(async posts => { - - //if we're sorting by last updated, comments can prompt posts to look new, but we only want those with a comment that's newer than our newerthan timesamp - //AND wasn't left by the logged in user, who knows about their own comments. so, we search recursively for that. - function findNewComment(postOrComment) { - if (postOrComment.timestamp > newerThanDate && !postOrComment.author.equals(req.user._id)) { - return true; - } - if (postOrComment.replies && postOrComment.replies.length) { - for (const r of postOrComment.replies) { - if (findNewComment(r)) { - return true; - } - } - } else if (postOrComment.comments && postOrComment.comments.length) { - for (const c of postOrComment.comments) { - if (findNewComment(c)) { - return true; - } - } - } - return false; + app.get('/drafts/:olderthanthis', isLoggedInOrRedirect, function (req, res) { + Post.find({ type: 'draft', author: req.user._id, timestamp: { $lt: new Date(parseInt(req.params.olderthanthis)) } }) + .sort('-timestamp').limit(10).populate('author').then(async posts => { + if (!posts.length) { + res.sendStatus(404) + } else { + for (var post of posts) { + await keepCachedHTMLUpToDate(post) + post.internalPostHTML = post.cachedHTML.fullContentHTML + post.commentsDisabled = true + post.isYourPost = true + var momentTimestamp = moment(post.timestamp) + if (momentTimestamp.isSame(moment(), 'd')) { + post.parsedTimestamp = momentTimestamp.fromNow() + } else if (momentTimestamp.isSame(moment(), 'y')) { + post.parsedTimestamp = momentTimestamp.format('D MMM') + } else { + post.parsedTimestamp = momentTimestamp.format('D MMM YYYY') } + post.fullTimestamp = momentTimestamp.calendar() + } + res.render('partials/posts_v2', { + layout: false, + loggedIn: true, + loggedInUserData: req.user, + posts: posts, + context: 'drafts', + canReply: false, + isMuted: false, + oldesttimestamp: posts[posts.length - 1].timestamp.getTime() + '' + }) + } + }) + }) + + // this function checks if there are some newer posts than the given timestamp for the user's home feed. it duplicates some query logic from /showposts to do this. + app.get('/heyaretherenewposts/:newerthanthis', isLoggedInOrRedirect, async function (req, res) { + var myFollowedUserIds = ((await Relationship.find({ fromUser: req.user._id, value: 'follow' })).map(v => v.toUser)) + var myMutedUserIds = ((await Relationship.find({ fromUser: req.user._id, value: 'mute' })).map(v => v.toUser)) + var usersWhoTrustMe = ((await Relationship.find({ toUser: req.user._id, value: 'trust' })).map(v => v.fromUser)) + var query = { + $and: [{ + $or: [ + { $and: [{ author: { $in: myFollowedUserIds } }, { $or: [{ type: { $ne: 'community' } }, { community: { $in: req.user.communities } }] }] }, { community: { $in: req.user.communities } } + ] + }, + { $or: [{ privacy: 'public' }, { author: { $in: usersWhoTrustMe } }] }, + { author: { $not: { $in: myMutedUserIds } } }, + { type: { $ne: 'draft' } } + ] + } + var sortMethod = req.user.settings.homeTagTimelineSorting == 'fluid' ? 'lastUpdated' : 'timestamp' + var newerThanDate = new Date(parseInt(req.params.newerthanthis)) + var newerThanQuery = {} + newerThanQuery[sortMethod] = { $gt: newerThanDate } + query.$and.push(newerThanQuery) + Post.find(query).then(async posts => { + // if we're sorting by last updated, comments can prompt posts to + // look new, but we only want those with a comment that's newer than + // our newerthan timesamp AND wasn't left by the logged in user, who + // knows about their own comments. so, we search recursively for that. + function findNewComment (postOrComment) { + if (postOrComment.timestamp > newerThanDate && !postOrComment.author.equals(req.user._id)) { + return true + } + if (postOrComment.replies && postOrComment.replies.length) { + for (const r of postOrComment.replies) { + if (findNewComment(r)) { + return true + } + } + } else if (postOrComment.comments && postOrComment.comments.length) { + for (const c of postOrComment.comments) { + if (findNewComment(c)) { + return true + } + } + } + return false + } - res.setHeader("content-type", "text/plain") + res.setHeader('content-type', 'text/plain') - for (const post of posts) { - var postCommunity = post.community ? (await Community.findById(post.community)) : undefined; - if ((sortMethod == "lastUpdated" && findNewComment(post)) && (post.type != "community" || !postCommunity.mutedMembers.includes(post.author))) { - res.send("yeah"); - return; - } - } - res.send("no i guess not"); - }) + for (const post of posts) { + var postCommunity = post.community ? (await Community.findById(post.community)) : undefined + if ((sortMethod == 'lastUpdated' && findNewComment(post)) && (post.type != 'community' || !postCommunity.mutedMembers.includes(post.author))) { + res.send('yeah') + return + } + } + res.send('no i guess not') }) + }) + + // Responds to a get response for a specific post. + // Inputs: the username of the user and the string of random letters and numbers that identifies the post (that's how post urls work) + // Outputs: showposts handles it! + app.get('/:username/:posturl', function (req, res, next) { + if (req.params.username != 'images') { // a terrible hack to stop requests for images (/images/[image filename] fits into this route's format) from being sent to showposts + req.url = req.path = '/showposts/single/' + req.params.posturl + '/1' + req.singlepostUsername = req.params.username // slightly sus way to pass this info to showposts + next('route') + } + }) + + app.get('/tag/:tagname', function (req, res, next) { + req.url = req.path = '/showposts/tag/' + req.params.tagname + '/1' + next('route') + }) + + // this function is called per post in the post displaying function below to keep the cached html for image galleries and embeds up to date + // in the post and all of its comments. + async function keepCachedHTMLUpToDate (post) { + // only runs if cached html is out of date + async function updateHTMLRecursive (displayContext) { + var html = await helper.renderHTMLContent(displayContext) + if (displayContext.cachedHTML) { + displayContext.cachedHTML.fullContentHTML = html + } else { + displayContext.cachedHTML = { fullContentHTML: html } + } + if (displayContext.comments) { + for (comment of displayContext.comments) { + await updateHTMLRecursive(comment) + } + } else if (displayContext.replies) { + for (reply of displayContext.replies) { + await updateHTMLRecursive(reply) + } + } + } - //Responds to a get response for a specific post. - //Inputs: the username of the user and the string of random letters and numbers that identifies the post (that's how post urls work) - //Outputs: showposts handles it! - app.get('/:username/:posturl', function(req, res, next) { - if (req.params.username != 'images') { //a terrible hack to stop requests for images (/images/[image filename] fits into this route's format) from being sent to showposts - req.url = req.path = "/showposts/single/" + req.params.posturl + "/1"; - req.singlepostUsername = req.params.username; //slightly sus way to pass this info to showposts - next('route'); + var galleryTemplatePath = './views/partials/imagegallery.handlebars' + var galleryTemplateMTime = undefined + var embedsTemplatePath = './views/partials/embed.handlebars' + var embedTemplateMTime = undefined + + // non-blocking way to retrieve the last modified times for these templates so that we can check if the cached post html is up to data + var mTimes = new Promise(function (resolve, reject) { + fs.stat(galleryTemplatePath, (err, stats) => { + if (err) { + console.err('could not get last modified time for image gallery template, post html will not be updated') + reject(err) + } else { + galleryTemplateMTime = stats.mtime + if (embedTemplateMTime) { + resolve() + } + } + }) + fs.stat(embedsTemplatePath, (err, stats) => { + if (err) { + console.err('could not get last modified time for embed/link preview template, post html will not be updated') + reject(err) + } else { + embedTemplateMTime = stats.mtime + if (galleryTemplateMTime) { + resolve() + } } - return; + }) }) - app.get('/tag/:tagname', function(req, res, next) { - req.url = req.path = "/showposts/tag/" + req.params.tagname + "/1"; - next('route'); - return; + await mTimes.then(async function () { + if ((!post.cachedHTML.imageGalleryMTime || post.cachedHTML.imageGalleryMTime < galleryTemplateMTime) || (!post.cachedHTML.embedsMTime || post.cachedHTML.embedsMTime < embedTemplateMTime)) { + await updateHTMLRecursive(post) + post.cachedHTML.imageGalleryMTime = galleryTemplateMTime + post.cachedHTML.embedsMTime = embedTemplateMTime + await post.save() + } }) + } - //this function is called per post in the post displaying function below to keep the cached html for image galleries and embeds up to date - //in the post and all of its comments. - async function keepCachedHTMLUpToDate(post) { + /* + Responds to requests for posts for feeds. API method, used within the public pages. - //only runs if cached html is out of date - async function updateHTMLRecursive(displayContext) { - var html = await helper.renderHTMLContent(displayContext); - if (displayContext.cachedHTML) { - displayContext.cachedHTML.fullContentHTML = html; - } else { - displayContext.cachedHTML = { fullContentHTML: html }; - } - if (displayContext.comments) { - for (comment of displayContext.comments) { - await updateHTMLRecursive(comment); - } - } else if (displayContext.replies) { - for (reply of displayContext.replies) { - await updateHTMLRecursive(reply); - } - } - } - - var galleryTemplatePath = "./views/partials/imagegallery.handlebars"; - var galleryTemplateMTime = undefined; - var embedsTemplatePath = "./views/partials/embed.handlebars"; - var embedTemplateMTime = undefined; - - //non-blocking way to retrieve the last modified times for these templates so that we can check if the cached post html is up to data - var mTimes = new Promise(function(resolve, reject) { - fs.stat(galleryTemplatePath, (err, stats) => { - if (err) { - console.err('could not get last modified time for image gallery template, post html will not be updated'); - reject(err); - } else { - galleryTemplateMTime = stats.mtime; - if (embedTemplateMTime) { - resolve(); - } - } - }) - fs.stat(embedsTemplatePath, (err, stats) => { - if (err) { - console.err('could not get last modified time for embed/link preview template, post html will not be updated'); - reject(err); - } else { - embedTemplateMTime = stats.mtime; - if (galleryTemplateMTime) { - resolve(); - } - } - }) - }) + Inputs: - await mTimes.then(async function() { - if ((!post.cachedHTML.imageGalleryMTime || post.cachedHTML.imageGalleryMTime < galleryTemplateMTime) || (!post.cachedHTML.embedsMTime || post.cachedHTML.embedsMTime < embedTemplateMTime)) { - await updateHTMLRecursive(post); - post.cachedHTML.imageGalleryMTime = galleryTemplateMTime; - post.cachedHTML.embedsMTime = embedTemplateMTime; - await post.save(); - } - }) - } + context is one of: + - community (posts on a community's page) + - home (posts on the home page) + - user (posts on a user's profile page) + - single (a single post) - //Responds to requests for posts for feeds. API method, used within the public pages. - //Inputs: the context is either community (posts on a community's page), home (posts on the home page), user - //(posts on a user's profile page), or single (a single post.) The identifier identifies either the user, the - //community, or the post. I don't believe it's used when the context is home? It appears to be the url of the image - //of the logged in user in that case. (??????????????????) olderthanthis means we want posts older than this timestamp (milliseconds). - //Output: the rendered HTML of the posts, unless it can't find any posts, in which case it returns a 404 error. - app.get('/showposts/:context/:identifier/:olderthanthis', async function(req, res) { - var loggedInUserData = {}; - if (req.isAuthenticated()) { - loggedInUserData = req.user; - } else { - //logged out users can't get any posts from pages of non-completely-public users and communities - if (req.params.context == "user" && (await User.findById(req.params.identifier)).settings.profileVisibility != "profileAndPosts") { - res.sendStatus(404); - return; - } else if (req.params.context == "community" && (await Community.findById(req.params.identifier)).settings.visibility != "public") { - res.sendStatus(404); - return; - } - } + identifier identifies one of + - user + - community + - post - let postsPerPage = 10; - let olderthanthis = new Date(parseInt(req.params.olderthanthis)) + I don't believe it's used when the context is home? It appears to be the url of the + image of the logged in user in that case. (??????????????????) - //build some user lists. only a thing if the user is logged in. - //todo: instead of pulling them from the relationships collection, at least the first 4 could be arrays of references to other users in the user document, that would speed things up - if (req.isAuthenticated()) { - var myFollowedUserIds = ((await Relationship.find({ from: loggedInUserData.email, value: "follow" })).map(v => v.toUser)).concat([req.user._id]); - var myFlaggedUserEmails = ((await Relationship.find({ from: loggedInUserData.email, value: "flag" })).map(v => v.to)); - var myMutedUserEmails = ((await Relationship.find({ from: loggedInUserData.email, value: "mute" })).map(v => v.to)); - var myTrustedUserEmails = ((await Relationship.find({ from: loggedInUserData.email, value: "trust" })).map(v => v.to)); - var usersFlaggedByMyTrustedUsers = ((await Relationship.find({ from: { $in: myTrustedUserEmails }, value: "flag" })).map(v => v.to)); - var usersWhoTrustMeEmails = ((await Relationship.find({ to: loggedInUserData.email, value: "trust" })).map(v => v.from)).concat([req.user.email]); - var myCommunities = req.user.communities; - if (req.params.context == "community" && req.isAuthenticated()) { - var isMuted = (await Community.findById(req.params.identifier)).mutedMembers.some(v => v.equals(req.user._id)); - } else { - var isMuted = false; - } - var flagged = usersFlaggedByMyTrustedUsers.concat(myFlaggedUserEmails).filter(e => e !== loggedInUserData.email); - } + olderthanthis means we want posts older than this timestamp (milliseconds). - const today = moment().clone().startOf('day'); - const thisyear = moment().clone().startOf('year'); + Output: the rendered HTML of the posts, unless it can't find any posts, in which + case it returns a 404 error. + */ + app.get('/showposts/:context/:identifier/:olderthanthis', async function (req, res) { + var loggedInUserData = {} + if (req.isAuthenticated()) { + loggedInUserData = req.user + } else { + // logged out users can't get any posts from pages of non-completely-public users and communities + if (req.params.context == 'user' && (await User.findById(req.params.identifier)).settings.profileVisibility != 'profileAndPosts') { + res.sendStatus(404) + return + } else if (req.params.context == 'community' && (await Community.findById(req.params.identifier)).settings.visibility != 'public') { + res.sendStatus(404) + return + } + } - //construct the query that will retrieve the posts we want. basically just coming up with criteria to pass to Post.find. also, sortMethod - //is set according to the relevant user setting if they're logged in or to a default way at the bottom of this part if they're not. + const postsPerPage = 10 + const olderthanthis = new Date(parseInt(req.params.olderthanthis)) - if (req.params.context == "home") { - //on the home page, we're looking for posts (and boosts) created by users we follow as well as posts in communities that we're in. - //we're assuming the user is logged in if this request is being made (it's only made by code on a page that only loads if the user is logged in.) - var matchPosts = { - '$or': [{ - 'author': { - $in: myFollowedUserIds - } - }, - { - type: 'community', - community: { - $in: myCommunities - } - } - ], - type: { $ne: 'draft' } - }; - var sortMethod = req.user.settings.homeTagTimelineSorting == "fluid" ? "-lastUpdated" : "-timestamp"; - } else if (req.params.context == "user") { - //if we're on a user's page, obviously we want their posts: - var matchPosts = { - author: req.params.identifier, - type: { $ne: 'draft' } - } - //but we also only want posts if they're non-community or they come from a community that we belong to: - if (req.isAuthenticated()) { - matchPosts.$or = [{ - community: { - $exists: false - } - }, { - community: { - $in: myCommunities - } - }]; - var sortMethod = req.user.settings.userTimelineSorting == "fluid" ? "-lastUpdated" : "-timestamp"; - } else { - //logged out users shouldn't see any community posts on user profile pages - matchPosts.community = { - $exists: false - }; - } - } else if (req.params.context == "community") { - var thisComm = await Community.findById(req.params.identifier); - //we want posts from the community, but only if it's public or we belong to it: - if (thisComm.settings.visibility == 'public' || myCommunities.some(v => v.toString() == req.params.identifier)) { - var matchPosts = { - community: req.params.identifier - } - } else { - //if we're not in the community and it's not public, there are no posts we're allowed to view! - var matchPosts = undefined; - } - if (req.isAuthenticated()) { - var sortMethod = req.user.settings.communityTimelineSorting == "fluid" ? "-lastUpdated" : "-timestamp"; - } - } else if (req.params.context == "tag") { - function getTag() { - return Tag.findOne({ name: req.params.identifier }) - .then((tag) => { - var matchPosts = { _id: { $in: tag.posts }, type: { $ne: "draft" } } - return matchPosts; - }) - } - var matchPosts = await getTag(); - var sortMethod = req.user.settings.homeTagTimelineSorting == "fluid" ? "-lastUpdated" : "-timestamp"; - } else if (req.params.context == "single") { - var author = (await User.findOne({ username: req.singlepostUsername }, { _id: 1 })); - var matchPosts = { - author: author ? author._id : undefined, //won't find anything if the author corresponding to the username couldn't be found - url: req.params.identifier, - type: { $ne: "draft" } - } - var sortMethod = "-lastUpdated" //this shouldn't matter oh well - } + // build some user lists. only a thing if the user is logged in. + // todo: instead of pulling them from the relationships collection, at least the first 4 could be arrays of references to other users in the user document, that would speed things up + if (req.isAuthenticated()) { + var myFollowedUserIds = ((await Relationship.find({ from: loggedInUserData.email, value: 'follow' })).map(v => v.toUser)).concat([req.user._id]) + var myFlaggedUserEmails = ((await Relationship.find({ from: loggedInUserData.email, value: 'flag' })).map(v => v.to)) + var myMutedUserEmails = ((await Relationship.find({ from: loggedInUserData.email, value: 'mute' })).map(v => v.to)) + var myTrustedUserEmails = ((await Relationship.find({ from: loggedInUserData.email, value: 'trust' })).map(v => v.to)) + var usersFlaggedByMyTrustedUsers = ((await Relationship.find({ from: { $in: myTrustedUserEmails }, value: 'flag' })).map(v => v.to)) + var usersWhoTrustMeEmails = ((await Relationship.find({ to: loggedInUserData.email, value: 'trust' })).map(v => v.from)).concat([req.user.email]) + var myCommunities = req.user.communities + if (req.params.context == 'community' && req.isAuthenticated()) { + var isMuted = (await Community.findById(req.params.identifier)).mutedMembers.some(v => v.equals(req.user._id)) + } else { + var isMuted = false + } + var flagged = usersFlaggedByMyTrustedUsers.concat(myFlaggedUserEmails).filter(e => e !== loggedInUserData.email) + } - if (!req.isAuthenticated()) { - matchPosts.privacy = "public"; - var sortMethod = "-lastUpdated"; + const today = moment().clone().startOf('day') + const thisyear = moment().clone().startOf('year') + + // construct the query that will retrieve the posts we want. basically just coming up with criteria to pass to Post.find. also, sortMethod + // is set according to the relevant user setting if they're logged in or to a default way at the bottom of this part if they're not. + + if (req.params.context == 'home') { + // on the home page, we're looking for posts (and boosts) created by users we follow as well as posts in communities that we're in. + // we're assuming the user is logged in if this request is being made (it's only made by code on a page that only loads if the user is logged in.) + var matchPosts = { + $or: [{ + author: { + $in: myFollowedUserIds + } + }, + { + type: 'community', + community: { + $in: myCommunities + } } - - if (req.params.context != "single") { - //in feeds, we only want posts older than ("less than") the parameter sent in, with age being judged by the currently active sorting method. - matchPosts[sortMethod.substring(1, sortMethod.length)] = { $lt: olderthanthis }; + ], + type: { $ne: 'draft' } + } + var sortMethod = req.user.settings.homeTagTimelineSorting == 'fluid' ? '-lastUpdated' : '-timestamp' + } else if (req.params.context == 'user') { + // if we're on a user's page, obviously we want their posts: + var matchPosts = { + author: req.params.identifier, + type: { $ne: 'draft' } + } + // but we also only want posts if they're non-community or they come from a community that we belong to: + if (req.isAuthenticated()) { + matchPosts.$or = [{ + community: { + $exists: false + } + }, { + community: { + $in: myCommunities + } + }] + var sortMethod = req.user.settings.userTimelineSorting == 'fluid' ? '-lastUpdated' : '-timestamp' + } else { + // logged out users shouldn't see any community posts on user profile pages + matchPosts.community = { + $exists: false } - - var query = Post.find( - matchPosts - ).sort(sortMethod) - .limit(postsPerPage) - //these populate commands retrieve the complete data for these things that are referenced in the post documents - .populate('author', '-password') - .populate('community') - // If there's a better way to populate a nested tree lmk because this is... dumb. Mitch says: probably just fetching the authors recursively in actual code below - .populate('comments.author') - .populate('comments.replies.author') - .populate('comments.replies.replies.author') - .populate('comments.replies.replies.replies.author') - .populate('comments.replies.replies.replies.replies.author') - .populate('boostTarget') - .populate('boostsV2.booster'); - - //so this will be called when the query retrieves the posts we want - var posts = await query; - - if (!posts || !posts.length) { - res.status(404).render('singlepost', { // The 404 is required so InfiniteScroll.js stops loading the feed - canDisplay: false, - loggedIn: req.isAuthenticated(), - loggedInUserData: loggedInUserData, - post: null, - metadata: {}, - activePage: 'singlepost' - }) - return; + } + } else if (req.params.context == 'community') { + var thisComm = await Community.findById(req.params.identifier) + // we want posts from the community, but only if it's public or we belong to it: + if (thisComm.settings.visibility == 'public' || myCommunities.some(v => v.toString() == req.params.identifier)) { + var matchPosts = { + community: req.params.identifier } + } else { + // if we're not in the community and it's not public, there are no posts we're allowed to view! + var matchPosts = undefined + } + if (req.isAuthenticated()) { + var sortMethod = req.user.settings.communityTimelineSorting == 'fluid' ? '-lastUpdated' : '-timestamp' + } + } else if (req.params.context == 'tag') { + function getTag () { + return Tag.findOne({ name: req.params.identifier }) + .then((tag) => { + var matchPosts = { _id: { $in: tag.posts }, type: { $ne: 'draft' } } + return matchPosts + }) + } + var matchPosts = await getTag() + var sortMethod = req.user.settings.homeTagTimelineSorting == 'fluid' ? '-lastUpdated' : '-timestamp' + } else if (req.params.context == 'single') { + var author = (await User.findOne({ username: req.singlepostUsername }, { _id: 1 })) + var matchPosts = { + author: author ? author._id : undefined, // won't find anything if the author corresponding to the username couldn't be found + url: req.params.identifier, + type: { $ne: 'draft' } + } + var sortMethod = '-lastUpdated' // this shouldn't matter oh well + } - if (req.params.context != "single") { - //this gets the timestamp of the last post, this tells the browser to ask for posts older than this next time. used in feeds, not with single posts - var oldesttimestamp = "" + posts[posts.length - 1][sortMethod.substring(1, sortMethod.length)].getTime(); - } + if (!req.isAuthenticated()) { + matchPosts.privacy = 'public' + var sortMethod = '-lastUpdated' + } - var displayedPosts = []; //populated by the for loop below - - for (var post of posts) { - //figure out if there is a newer instance of the post we're looking at. if it's an original post, check the boosts from - //the context's relevant users; if it's a boost, check the original post if we're in fluid mode to see if lastUpdated is more - //recent (meaning the original was bumped up from recieving a comment) and then for both fluid and chronological we have to check - //to see if there is a more recent boost. - if (req.params.context != "community" && req.params.context != "single") { - var isThereNewerInstance = false; - var whosePostsCount = req.params.context == "user" ? [new ObjectId(req.params.identifier)] : myFollowedUserIds; - if (post.type == 'original') { - for (boost of post.boostsV2) { - if (boost.timestamp.getTime() > post.lastUpdated.getTime() && whosePostsCount.some(f => { return boost.booster.equals(f) })) { - isThereNewerInstance = true; - } - } - } else if (post.type == 'boost') { - if (post.boostTarget != null) { - if (sortMethod == "-lastUpdated") { - if (post.boostTarget.lastUpdated.getTime() > post.timestamp.getTime()) { - isThereNewerInstance = true; - } - } - for (boost of post.boostTarget.boostsV2) { - if (boost.timestamp.getTime() > post.lastUpdated.getTime() && whosePostsCount.some(f => { return boost.booster.equals(f) })) { - isThereNewerInstance = true; - } - } - } else { - console.log("Error fetching boostTarget of boost") - isThereNewerInstance = true; - } - } + if (req.params.context != 'single') { + // in feeds, we only want posts older than ("less than") the parameter sent in, with age being judged by the currently active sorting method. + matchPosts[sortMethod.substring(1, sortMethod.length)] = { $lt: olderthanthis } + } - if (isThereNewerInstance) { - continue; - } - } + var query = Post.find( + matchPosts + ).sort(sortMethod) + .limit(postsPerPage) + // these populate commands retrieve the complete data for these things that are referenced in the post documents + .populate('author', '-password') + .populate('community') + // If there's a better way to populate a nested tree lmk because this is... dumb. Mitch says: probably just fetching the authors recursively in actual code below + .populate('comments.author') + .populate('comments.replies.author') + .populate('comments.replies.replies.author') + .populate('comments.replies.replies.replies.author') + .populate('comments.replies.replies.replies.replies.author') + .populate('boostTarget') + .populate('boostsV2.booster') + + // so this will be called when the query retrieves the posts we want + var posts = await query + + if (!posts || !posts.length) { + res.status(404).render('singlepost', { // The 404 is required so InfiniteScroll.js stops loading the feed + canDisplay: false, + loggedIn: req.isAuthenticated(), + loggedInUserData: loggedInUserData, + post: null, + metadata: {}, + activePage: 'singlepost' + }) + return + } - var canDisplay = false; - if (req.isAuthenticated()) { - //logged in users can't see private posts by users who don't trust them or community posts by muted members - if ((post.privacy == "private" && usersWhoTrustMeEmails.includes(post.authorEmail)) || post.privacy == "public") { - canDisplay = true; - } - if (post.type == "community") { - //we don't have to check if the user is in the community before displaying posts to them if we're on the community's page, or if it's a single post page and: the community is public or the user wrote the post - //in other words, we do have to check if the user is in the community if those things aren't true, hence the ! - if (!(req.params.context == "community" || (req.params.context == "single" && (post.author.equals(req.user._id) || post.community.settings.visibility == "public")))) { - if (myCommunities.some(m => { return m.equals(post.community._id) })) { - canDisplay = true; - } else { - canDisplay = false; - } - } - // Hide muted community members - let mutedMemberIds = post.community.mutedMembers.map(a => a._id.toString()); - if (mutedMemberIds.includes(post.author._id.toString())) { - canDisplay = false; - } - } - } else { - //for logged out users, we already eliminated private posts by specifying query.privacy = 'public', - //so we just have to hide posts boosted from non-publicly-visible accounts and posts from private communities that - //the user whose profile page we are on wrote (if this is an issue, we're on a profile page, bc non-public - //community pages are hidden from logged-out users by a return at the very, very beginning of this function) - if (post.author.settings.profileVisibility == "profileAndPosts") { - // User has allowed non-logged-in users to see their posts - if (post.community) { - if (post.community.settings.visibility == "public") { - // Public community, can display post - let mutedMemberIds = post.community.mutedMembers.map(a => a._id.toString()); - if (mutedMemberIds.includes(post.author._id.toString())) { - canDisplay = false; - } else { - canDisplay = true; - } - } - } else { - // Not a community post, can display - canDisplay = true; - } - } else if (req.params.context == "community" && thisComm.settings.visibility == "public") { - //also posts in publicly visible communities can be shown, period. i'm 99% sure that posts from private communities won't even - //be fetched for logged out users because of the way the matchPosts query is constructed above but just in case i'm checking it - //again in the above if statement - canDisplay = true; - } - } + if (req.params.context != 'single') { + // this gets the timestamp of the last post, this tells the browser to ask for posts older than this next time. used in feeds, not with single posts + var oldesttimestamp = '' + posts[posts.length - 1][sortMethod.substring(1, sortMethod.length)].getTime() + } - // As a final hurrah, just hide all posts and boosts made by users you've muted - if (req.isAuthenticated() && myMutedUserEmails.includes(post.authorEmail)) { - canDisplay = false; + var displayedPosts = [] // populated by the for loop below + + for (var post of posts) { + // figure out if there is a newer instance of the post we're looking at. if it's an original post, check the boosts from + // the context's relevant users; if it's a boost, check the original post if we're in fluid mode to see if lastUpdated is more + // recent (meaning the original was bumped up from recieving a comment) and then for both fluid and chronological we have to check + // to see if there is a more recent boost. + if (req.params.context != 'community' && req.params.context != 'single') { + var isThereNewerInstance = false + var whosePostsCount = req.params.context == 'user' ? [new ObjectId(req.params.identifier)] : myFollowedUserIds + if (post.type == 'original') { + for (boost of post.boostsV2) { + if (boost.timestamp.getTime() > post.lastUpdated.getTime() && whosePostsCount.some(f => { return boost.booster.equals(f) })) { + isThereNewerInstance = true } - - if (!canDisplay) { - continue; + } + } else if (post.type == 'boost') { + if (post.boostTarget != null) { + if (sortMethod == '-lastUpdated') { + if (post.boostTarget.lastUpdated.getTime() > post.timestamp.getTime()) { + isThereNewerInstance = true + } } - - var displayContext = post; - if (post.type == "boost") { - displayContext = post.boostTarget; - displayContext.author = await User.findById(displayContext.author); - for (const boost of displayContext.boostsV2) { - boost.booster = await User.findById(boost.booster); - } + for (boost of post.boostTarget.boostsV2) { + if (boost.timestamp.getTime() > post.lastUpdated.getTime() && whosePostsCount.some(f => { return boost.booster.equals(f) })) { + isThereNewerInstance = true + } } + } else { + console.log('Error fetching boostTarget of boost') + isThereNewerInstance = true + } + } - await keepCachedHTMLUpToDate(displayContext); + if (isThereNewerInstance) { + continue + } + } - if (moment(displayContext.timestamp).isSame(today, 'd')) { - var parsedTimestamp = moment(displayContext.timestamp).fromNow(); - } else if (moment(displayContext.timestamp).isSame(thisyear, 'y')) { - var parsedTimestamp = moment(displayContext.timestamp).format('D MMM'); + var canDisplay = false + if (req.isAuthenticated()) { + // logged in users can't see private posts by users who don't trust them or community posts by muted members + if ((post.privacy == 'private' && usersWhoTrustMeEmails.includes(post.authorEmail)) || post.privacy == 'public') { + canDisplay = true + } + if (post.type == 'community') { + // we don't have to check if the user is in the community before displaying posts to them if we're on the community's page, or if it's a single post page and: the community is public or the user wrote the post + // in other words, we do have to check if the user is in the community if those things aren't true, hence the ! + if (!(req.params.context == 'community' || (req.params.context == 'single' && (post.author.equals(req.user._id) || post.community.settings.visibility == 'public')))) { + if (myCommunities.some(m => { return m.equals(post.community._id) })) { + canDisplay = true } else { - var parsedTimestamp = moment(displayContext.timestamp).format('D MMM YYYY'); + canDisplay = false } - - if (req.isAuthenticated()) { - // Used to check if you can delete a post - var isYourPost = displayContext.author._id.equals(req.user._id); + } + // Hide muted community members + const mutedMemberIds = post.community.mutedMembers.map(a => a._id.toString()) + if (mutedMemberIds.includes(post.author._id.toString())) { + canDisplay = false + } + } + } else { + // for logged out users, we already eliminated private posts by specifying query.privacy = 'public', + // so we just have to hide posts boosted from non-publicly-visible accounts and posts from private communities that + // the user whose profile page we are on wrote (if this is an issue, we're on a profile page, bc non-public + // community pages are hidden from logged-out users by a return at the very, very beginning of this function) + if (post.author.settings.profileVisibility == 'profileAndPosts') { + // User has allowed non-logged-in users to see their posts + if (post.community) { + if (post.community.settings.visibility == 'public') { + // Public community, can display post + const mutedMemberIds = post.community.mutedMembers.map(a => a._id.toString()) + if (mutedMemberIds.includes(post.author._id.toString())) { + canDisplay = false + } else { + canDisplay = true + } } - //generate some arrays containing usernames that will be put in "boosted by" labels - if (req.isAuthenticated() && (req.params.context != "community")) { - var followedBoosters = []; - var notFollowingBoosters = []; - var youBoosted = false; - if (displayContext.boostsV2.length > 0) { - displayContext.boostsV2.forEach((v, i, a) => { - if (!(v.timestamp.getTime() == displayContext.timestamp.getTime())) { //do not include implicit boost - if (v.booster._id.equals(req.user._id)) { - followedBoosters.push('you'); - youBoosted = true; - } else { - if (myFollowedUserIds.some(following => { if (following) { return following.equals(v.booster._id) } })) { - followedBoosters.push(v.booster.username); - } else { - notFollowingBoosters.push(v.booster.username); - } - } - } - }) - } - if (req.params.context == "user" && !displayContext.author._id.equals(post.author._id)) { - var boostsForHeader = [post.author.username] + } else { + // Not a community post, can display + canDisplay = true + } + } else if (req.params.context == 'community' && thisComm.settings.visibility == 'public') { + // also posts in publicly visible communities can be shown, period. i'm 99% sure that posts from private communities won't even + // be fetched for logged out users because of the way the matchPosts query is constructed above but just in case i'm checking it + // again in the above if statement + canDisplay = true + } + } + + // As a final hurrah, just hide all posts and boosts made by users you've muted + if (req.isAuthenticated() && myMutedUserEmails.includes(post.authorEmail)) { + canDisplay = false + } + + if (!canDisplay) { + continue + } + + var displayContext = post + if (post.type == 'boost') { + displayContext = post.boostTarget + displayContext.author = await User.findById(displayContext.author) + for (const boost of displayContext.boostsV2) { + boost.booster = await User.findById(boost.booster) + } + } + + await keepCachedHTMLUpToDate(displayContext) + + if (moment(displayContext.timestamp).isSame(today, 'd')) { + var parsedTimestamp = moment(displayContext.timestamp).fromNow() + } else if (moment(displayContext.timestamp).isSame(thisyear, 'y')) { + var parsedTimestamp = moment(displayContext.timestamp).format('D MMM') + } else { + var parsedTimestamp = moment(displayContext.timestamp).format('D MMM YYYY') + } + + if (req.isAuthenticated()) { + // Used to check if you can delete a post + var isYourPost = displayContext.author._id.equals(req.user._id) + } + // generate some arrays containing usernames that will be put in "boosted by" labels + if (req.isAuthenticated() && (req.params.context != 'community')) { + var followedBoosters = [] + var notFollowingBoosters = [] + var youBoosted = false + if (displayContext.boostsV2.length > 0) { + displayContext.boostsV2.forEach((v, i, a) => { + if (!(v.timestamp.getTime() == displayContext.timestamp.getTime())) { // do not include implicit boost + if (v.booster._id.equals(req.user._id)) { + followedBoosters.push('you') + youBoosted = true + } else { + if (myFollowedUserIds.some(following => { if (following) { return following.equals(v.booster._id) } })) { + followedBoosters.push(v.booster.username) } else { - var boostsForHeader = followedBoosters.slice(0, 3); + notFollowingBoosters.push(v.booster.username) } - } else { - //logged out users will see boosts only on user profile pages and they only need to know that that user boosted the post. should be obvious anyway but, whatevs - if (!req.isAuthenticated() && req.params.context == "user") { - if (displayContext.author._id.toString() != req.params.identifier) { - var boostsForHeader = [(await (User.findById(req.params.identifier))).username]; - } - } else if (req.isAuthenticated() && req.params.context == "user") { - if (displayContext.author._id.toString() != req.params.identifier) { - var boostsForHeader = [(await (User.findById(req.params.identifier))).username]; - } - } - } - - var displayedPost = Object.assign(displayContext, { - deleteid: displayContext._id, - parsedTimestamp: parsedTimestamp, - timestampMs: displayContext.timestamp.getTime(), - editedTimestampMs: displayContext.lastEdited ? displayContext.lastEdited.getTime() : "", - internalPostHTML: displayContext.cachedHTML.fullContentHTML, - headerBoosters: boostsForHeader, - recentlyCommented: false, // This gets set below - lastCommentAuthor: "", // As does this - }) - - //these are only a thing for logged in users - if (req.isAuthenticated()) { - displayedPost.followedBoosters = followedBoosters; - displayedPost.otherBoosters = notFollowingBoosters; - displayedPost.isYourPost = isYourPost; - displayedPost.youBoosted = youBoosted + } } + }) + } + if (req.params.context == 'user' && !displayContext.author._id.equals(post.author._id)) { + var boostsForHeader = [post.author.username] + } else { + var boostsForHeader = followedBoosters.slice(0, 3) + } + } else { + // logged out users will see boosts only on user profile pages and they only need to know that that user boosted the post. should be obvious anyway but, whatevs + if (!req.isAuthenticated() && req.params.context == 'user') { + if (displayContext.author._id.toString() != req.params.identifier) { + var boostsForHeader = [(await (User.findById(req.params.identifier))).username] + } + } else if (req.isAuthenticated() && req.params.context == 'user') { + if (displayContext.author._id.toString() != req.params.identifier) { + var boostsForHeader = [(await (User.findById(req.params.identifier))).username] + } + } + } + + var displayedPost = Object.assign(displayContext, { + deleteid: displayContext._id, + parsedTimestamp: parsedTimestamp, + timestampMs: displayContext.timestamp.getTime(), + editedTimestampMs: displayContext.lastEdited ? displayContext.lastEdited.getTime() : '', + internalPostHTML: displayContext.cachedHTML.fullContentHTML, + headerBoosters: boostsForHeader, + recentlyCommented: false, // This gets set below + lastCommentAuthor: '' // As does this + }) + + // these are only a thing for logged in users + if (req.isAuthenticated()) { + displayedPost.followedBoosters = followedBoosters + displayedPost.otherBoosters = notFollowingBoosters + displayedPost.isYourPost = isYourPost + displayedPost.youBoosted = youBoosted + } + + // get timestamps and full image urls for each comment + var latestTimestamp = 0 + var sixHoursAgo = moment(new Date()).subtract(6, 'hours') + var threeHoursAgo = moment(new Date()).subtract(3, 'hours') + + function parseComments (element, level) { + if (!level) level = 1 + element.forEach(async function (comment) { + comment.canDisplay = true + comment.muted = false + // I'm not sure why, but boosts in the home feed don't display + // comment authors below the top level - this fixes it, but + // it's kind of a hack - I can't work out what's going on + if (!comment.author.username) { + comment.author = await User.findById(comment.author) + } + if (req.isAuthenticated() && myMutedUserEmails.includes(comment.author.email)) { + comment.muted = true + comment.canDisplay = false + } + if (comment.deleted) { + comment.canDisplay = false + } + momentifiedTimestamp = moment(comment.timestamp) + if (momentifiedTimestamp.isSame(today, 'd')) { + comment.parsedTimestamp = momentifiedTimestamp.fromNow() + } else if (momentifiedTimestamp.isSame(thisyear, 'y')) { + comment.parsedTimestamp = momentifiedTimestamp.format('D MMM') + } else { + comment.parsedTimestamp = momentifiedTimestamp.format('D MMM YYYY') + } + if (comment.timestamp > latestTimestamp) { + latestTimestamp = comment.timestamp + displayedPost.lastCommentAuthor = comment.author + } + // Only pulse comments from people who aren't you + if (req.isAuthenticated() && momentifiedTimestamp.isAfter(threeHoursAgo) && !comment.author._id.equals(req.user._id)) { + comment.isRecent = true + } + for (var i = 0; i < comment.images.length; i++) { + comment.images[i] = '/api/image/display/' + comment.images[i] + } + // If the comment's author is logged in, or the displayContext's author is logged in + if (((comment.author._id.equals(loggedInUserData._id)) || (displayContext.author._id.equals(loggedInUserData._id))) && !comment.deleted) { + comment.canDelete = true + } + if (level < globals.maximumCommentDepth) { + comment.canReply = true + } + comment.level = level + if (comment.replies) { + parseComments(comment.replies, level + 1) + } + }) + if (moment(latestTimestamp).isAfter(sixHoursAgo)) { + displayedPost.recentlyCommented = true + } else { + displayedPost.recentlyCommented = false + } + } + parseComments(displayedPost.comments) - //get timestamps and full image urls for each comment - var latestTimestamp = 0; - var sixHoursAgo = moment(new Date()).subtract(6, 'hours'); - var threeHoursAgo = moment(new Date()).subtract(3, 'hours'); - - function parseComments(element, level) { - if (!level) level = 1; - element.forEach(async function(comment) { - - comment.canDisplay = true; - comment.muted = false; - // I'm not sure why, but boosts in the home feed don't display - // comment authors below the top level - this fixes it, but - // it's kind of a hack - I can't work out what's going on - if (!comment.author.username) { - comment.author = await User.findById(comment.author); - } - if (req.isAuthenticated() && myMutedUserEmails.includes(comment.author.email)) { - comment.muted = true; - comment.canDisplay = false; - } - if (comment.deleted) { - comment.canDisplay = false; - } - momentifiedTimestamp = moment(comment.timestamp); - if (momentifiedTimestamp.isSame(today, 'd')) { - comment.parsedTimestamp = momentifiedTimestamp.fromNow(); - } else if (momentifiedTimestamp.isSame(thisyear, 'y')) { - comment.parsedTimestamp = momentifiedTimestamp.format('D MMM'); - } else { - comment.parsedTimestamp = momentifiedTimestamp.format('D MMM YYYY'); - } - if (comment.timestamp > latestTimestamp) { - latestTimestamp = comment.timestamp; - displayedPost.lastCommentAuthor = comment.author; - } - // Only pulse comments from people who aren't you - if (req.isAuthenticated() && momentifiedTimestamp.isAfter(threeHoursAgo) && !comment.author._id.equals(req.user._id)) { - comment.isRecent = true; - } - for (var i = 0; i < comment.images.length; i++) { - comment.images[i] = '/api/image/display/' + comment.images[i]; - } - // If the comment's author is logged in, or the displayContext's author is logged in - if (((comment.author._id.equals(loggedInUserData._id)) || (displayContext.author._id.equals(loggedInUserData._id))) && !comment.deleted) { - comment.canDelete = true; - } - if (level < globals.maximumCommentDepth) { - comment.canReply = true; - } - comment.level = level; - if (comment.replies) { - parseComments(comment.replies, level + 1) - } - }); - if (moment(latestTimestamp).isAfter(sixHoursAgo)) { - displayedPost.recentlyCommented = true; - } else { - displayedPost.recentlyCommented = false; - } - } - parseComments(displayedPost.comments); + if (req.isAuthenticated() && req.params.context == 'single') { + // Mark associated notifications read if post is visible + notifier.markRead(loggedInUserData._id, displayContext._id) + } - if (req.isAuthenticated() && req.params.context == "single") { - // Mark associated notifications read if post is visible - notifier.markRead(loggedInUserData._id, displayContext._id) - } + // wow, finally. + displayedPosts.push(displayedPost) + } - //wow, finally. - displayedPosts.push(displayedPost); - } + if (!displayedPosts.length) { + res.status(404).render('singlepost', { // The 404 is required so InfiniteScroll.js stops loading the feed + canDisplay: false, + loggedIn: req.isAuthenticated(), + loggedInUserData: loggedInUserData, + post: null, + metadata: {}, + activePage: 'singlepost' + }) + return + } - if (!displayedPosts.length) { - res.status(404).render('singlepost', { // The 404 is required so InfiniteScroll.js stops loading the feed - canDisplay: false, - loggedIn: req.isAuthenticated(), - loggedInUserData: loggedInUserData, - post: null, - metadata: {}, - activePage: 'singlepost' - }) - return; + var metadata = {} + if (req.params.context == 'single') { + // For single posts, we are going to render a different template so that we can include its metadata in the HTML "head" section + // We can only get the post metadata if the post array is filled (and it'll only be filled + // if the post was able to be displayed, so this checks to see if we should display + // our vague error message on the frontend) + var displayedPost = displayedPosts[0] + let canDisplay + let isMember = false + if (typeof displayedPost !== 'undefined') { + canDisplay = true + let imageCont + let metadataImage + if (displayedPost.inlineElements && displayedPost.inlineElements.length && (imageCont = (displayedPost.inlineElements.find(v => v.type === 'image(s)')))) { + metadataImage = 'https://sweet.sh/api/image/display/' + imageCont.images[0] + } else if (displayedPost.images && displayedPost.images.length) { + metadataImage = ((!displayedPost.imageVersion || displayedPost.imageVersion < 2) ? 'https://sweet.sh/images/uploads/' : 'https://sweet.sh/api/image/display/') + displayedPost.images[0] + } else if (displayedPost.author.imageEnabled) { + metadataImage = 'https://sweet.sh/images/' + displayedPost.author.image + } else { + metadataImage = 'https://sweet.sh/images/cake.svg' } - - var metadata = {}; - if (req.params.context == "single") { - // For single posts, we are going to render a different template so that we can include its metadata in the HTML "head" section - // We can only get the post metadata if the post array is filled (and it'll only be filled - // if the post was able to be displayed, so this checks to see if we should display - // our vague error message on the frontend) - var displayedPost = displayedPosts[0]; - if (typeof displayedPost !== 'undefined') { - var canDisplay = true; - var imageCont = undefined; - if (displayedPost.inlineElements && displayedPost.inlineElements.length && (imageCont = (displayedPost.inlineElements.find(v => v.type == "image(s)")))) { - var metadataImage = "https://sweet.sh/api/image/display/" + imageCont.images[0]; - } else if (displayedPost.images && displayedPost.images.length) { - var metadataImage = ((!displayedPost.imageVersion || displayedPost.imageVersion < 2) ? "https://sweet.sh/images/uploads/" : "https://sweet.sh/api/image/display/") + displayedPost.images[0]; - } else if (displayedPost.author.imageEnabled) { - var metadataImage = "https://sweet.sh/images/" + displayedPost.author.image; - } else { - var metadataImage = "https://sweet.sh/images/cake.svg"; - } - var firstLine = /

(.+?)<\/p>|

  • (.+?)<\/li>|
    (.+?)<\/blockquote>/.exec(displayedPost.internalPostHTML) - if (firstLine && firstLine[1]) { - firstLine = firstLine[1].replace(/<.*?>/g, '').substring(0, 100) + (firstLine[1].length > 100 ? '...' : ''); - } else { - firstLine = "Just another ol' good post on sweet"; - } - metadata = { - title: "@" + displayedPost.author.username + " on sweet", - description: firstLine, - image: metadataImage, - url: 'https://sweet.sh/' + displayedPost.author.username + '/' + displayedPost.url - } - if (displayedPost.community && req.isAuthenticated() && displayedPost.community.members.some(m => { return m.equals(req.user._id) })) { - var isMember = true; - } else { - var isMember = false; - } - } else { - var canDisplay = false; - // We add some dummy metadata for posts which error - metadata = { - title: "sweet • a social network", - description: "", - image: "https://sweet.sh/images/cake.svg", - url: "https://sweet.sh/" - } - } - res.render('singlepost', { - canDisplay: canDisplay, - loggedIn: req.isAuthenticated(), - loggedInUserData: loggedInUserData, - posts: [displayedPost], // This is so it loads properly inside the posts_v2 partial - flaggedUsers: flagged, - metadata: metadata, - isMuted: isMuted, - isMember: isMember, - canReply: !(displayedPost.type == "community" && !isMember), - activePage: 'singlepost' - }) + var firstLine = /

    (.+?)<\/p>|

    • (.+?)<\/li>|
      (.+?)<\/blockquote>/.exec(displayedPost.internalPostHTML) + if (firstLine && firstLine[1]) { + firstLine = firstLine[1].replace(/<.*?>/g, '').substring(0, 100) + (firstLine[1].length > 100 ? '...' : '') } else { - function canReply() { - if (req.isAuthenticated()) { - // These contexts already hide posts from communites you're not a member of - if (req.params.context == "home" || req.params.context == "tag" || req.params.context == "user") { - return true; - } else if (req.params.context == "community") { - return myCommunities.some(m => { return m.equals(req.params.identifier) }); - } - } else { - return false; - } - } - res.render('partials/posts_v2', { - layout: false, - loggedIn: req.isAuthenticated(), - isMuted: isMuted, - loggedInUserData: loggedInUserData, - posts: displayedPosts, - flaggedUsers: flagged, - context: req.params.context, - canReply: canReply(), - oldesttimestamp: oldesttimestamp - }); + firstLine = "Just another ol' good post on sweet" } - }) - - //Responds to get requests for a user's profile page. - //Inputs: username is the user's username. - //Outputs: a 404 if the user isn't found - app.get('/:username', async function(req, res) { - - function c(e) { - console.error("error in query in /:username user list builders"); - console.error(e); + metadata = { + title: '@' + displayedPost.author.username + ' on sweet', + description: firstLine, + image: metadataImage, + url: 'https://sweet.sh/' + displayedPost.author.username + '/' + displayedPost.url } - - var profileData = await User.findOne({ username: req.params.username }).catch(err => { - console.error("error in username query in /:username"); - console.error(err); - }); - if (!profileData) { - console.log("user " + req.params.username + " not found"); - res.status(404).redirect('/404'); - return; + isMember = ( + displayedPost.community && + req.isAuthenticated() && + displayedPost.community.members.some(m => m.equals(req.user._id)) + ) + } else { + canDisplay = false + // We add some dummy metadata for posts which error + metadata = { + title: 'sweet • a social network', + description: '', + image: 'https://sweet.sh/images/cake.svg', + url: 'https://sweet.sh/' } - var communitiesData = await Community.find({ members: profileData._id }).catch(c); //given to the renderer at the end - var followersArray = (await Relationship.find({ to: profileData.email, value: "follow" }, { from: 1 }).catch(c)).map(v => v.from); //only used for the below - var followers = await User.find({ email: { $in: followersArray } }).catch(c); //passed directly to the renderer - var theirFollowedUserEmails = (await Relationship.find({ from: profileData.email, value: "follow" }, { to: 1 }).catch(c)).map(v => v.to); //used in the below and to see if the profile user follows you - var theirFollowedUserData = await User.find({ email: { $in: theirFollowedUserEmails } }); //passed directly to the renderer - var usersWhoTrustThemArray = (await Relationship.find({ to: profileData.email, value: "trust" }).catch(c)).map(v => v.from) //only used for the below - var usersWhoTrustThem = await User.find({ email: { $in: usersWhoTrustThemArray } }).catch(c); //passed directly to the renderer - var theirTrustedUserEmails = (await Relationship.find({ from: profileData.email, value: "trust" }).catch(c)).map(v => v.to); //used to see if the profile user trusts the logged in user (if not isOwnProfile) and the below - var theirTrustedUserData = await User.find({ email: { $in: theirTrustedUserEmails } }).catch(c); //given directly to the renderer - - var userTrustsYou = false; - var userFollowsYou = false; + } + res.render('singlepost', { + canDisplay: canDisplay, + loggedIn: req.isAuthenticated(), + loggedInUserData: loggedInUserData, + posts: [displayedPost], // This is so it loads properly inside the posts_v2 partial + flaggedUsers: flagged, + metadata: metadata, + isMuted: isMuted, + isMember: isMember, + canReply: !(displayedPost.type === 'community' && !isMember), + activePage: 'singlepost' + }) + } else { + const getCanReply = () => { if (req.isAuthenticated()) { - // Is this the logged in user's own profile? - if (profileData.email == req.user.email) { - var isOwnProfile = true; - var userTrustsYou = false; - var userFollowsYou = false; - var trusted = false; - var followed = false; - var muted = false; - var flagged = false; - var flagsFromTrustedUsers = 0; - var myFlaggedUserEmails = (await Relationship.find({ from: req.user.email, value: "flag" }).catch(c)).map(v => v.to); //only used in the below line - var myFlaggedUserData = await User.find({ email: { $in: myFlaggedUserEmails } }).catch(c); //passed directly to the renderer, but only actually used if isOwnProfile, so we're only actually defining it in here - } else { - var isOwnProfile = false; - - var myTrustedUserEmails = (await Relationship.find({ from: req.user.email, value: "trust" }).catch(c)).map(v => v.to); //used for flag checking and to see if the logged in user trusts this user - - // Check if profile user follows and/or trusts logged in user - var userTrustsYou = theirTrustedUserEmails.includes(req.user.email) //not sure if these includes are faster than an indexed query of the relationships collection would be - var userFollowsYou = theirFollowedUserEmails.includes(req.user.email) - - // Check if logged in user follows and/or trusts and/or has muted profile user - var trusted = myTrustedUserEmails.includes(profileData.email); - var followed = !!(await Relationship.findOne({ from: req.user.email, to: profileData.email, value: "follow" }).catch(c)); - var muted = !!(await Relationship.findOne({ from: req.user.email, to: profileData.email, value: "mute" }).catch(c)); - - var flagsOnUser = await Relationship.find({ to: profileData.email, value: "flag" }).catch(c); - var flagsFromTrustedUsers = 0; - var flagged = false; - for (var flag of flagsOnUser) { - // Check if logged in user has flagged profile user - if (flag.from == req.user.email) { - var flagged = true; - } - // Check if any of the logged in user's trusted users have flagged profile user - if (myTrustedUserEmails.includes(flag.from)) { - flagsFromTrustedUsers++; - } - } - } - - } else { - var isOwnProfile = false; - var flagsFromTrustedUsers = 0; - var trusted = false; - var followed = false; - var flagged = false; + switch (req.params.context) { + // These contexts already hide posts from communites you're not a member of + case 'home': + case 'tag': + case 'user': + return true + case 'community': + default: + return false + } } - res.render('user', { - loggedIn: req.isAuthenticated(), - isOwnProfile: isOwnProfile, - loggedInUserData: req.user, - profileData: profileData, - trusted: trusted, - flagged: flagged, - muted: muted, - followed: followed, - followersData: followers, - usersWhoTrustThemData: usersWhoTrustThem, - userFollowsYou: userFollowsYou, - userTrustsYou: userTrustsYou, - trustedUserData: theirTrustedUserData, - followedUserData: theirFollowedUserData, - communitiesData: communitiesData, - flaggedUserData: myFlaggedUserData, - flagsFromTrustedUsers: flagsFromTrustedUsers, - activePage: profileData.username, - visibleSidebarArray: ['profileOnly', 'profileAndPosts'] - }); - }); - - //Responds to post request from the browser informing us that the user has seen the comments of some post by setting notifications about those comments - //to seen=true - //Input: - app.post("/api/notification/update/:id", isLoggedInOrRedirect, function(req, res) { - User.findOneAndUpdate({ - "_id": req.user._id, - "notifications._id": req.params.id - }, { - "$set": { - "notifications.$.seen": true - } - }, - function(err, doc) { - res.sendStatus(200) - } - ); - }) + return false + } + res.render('partials/posts_v2', { + layout: false, + loggedIn: req.isAuthenticated(), + isMuted: isMuted, + loggedInUserData: loggedInUserData, + posts: displayedPosts, + flaggedUsers: flagged, + context: req.params.context, + canReply: getCanReply(), + oldesttimestamp: oldesttimestamp + }) + } + }) + + // Responds to get requests for a user's profile page. + // Inputs: username is the user's username. + // Outputs: a 404 if the user isn't found + app.get('/:username', async function (req, res) { + function c (e) { + console.error('error in query in /:username user list builders') + console.error(e) + } - app.post("/api/notification/update-by-subject/:subjectid", isLoggedInOrRedirect, function(req, res) { - User.findOne({ - _id: req.user._id - }) - .then(user => { - user.notifications.forEach(notification => { - if (notification.subjectId == req.params.subjectid) { - notification.seen = true; - } - }) - user.save() - .then(response => { - res.sendStatus(200); - }) - }) + var profileData = await User.findOne({ username: req.params.username }).catch(err => { + console.error('error in username query in /:username') + console.error(err) }) - - app.get('/api/notification/display', function(req, res) { - if (req.isAuthenticated()) { - User.findOne({ - _id: req.user._id - }, 'notifications') - .then(user => { - user.notifications.reverse(); - res.render('partials/notifications', { - layout: false, - loggedIn: true, - loggedInUserData: req.user, - notifications: user.notifications - }); - }) - } else { - res.render('partials/notifications', { - layout: false, - loggedIn: false - }); + if (!profileData) { + console.log('user ' + req.params.username + ' not found') + res.status(404).redirect('/404') + return + } + var communitiesData = await Community.find({ members: profileData._id }).catch(c) // given to the renderer at the end + var followersArray = (await Relationship.find({ to: profileData.email, value: 'follow' }, { from: 1 }).catch(c)).map(v => v.from) // only used for the below + var followers = await User.find({ email: { $in: followersArray } }).catch(c) // passed directly to the renderer + var theirFollowedUserEmails = (await Relationship.find({ from: profileData.email, value: 'follow' }, { to: 1 }).catch(c)).map(v => v.to) // used in the below and to see if the profile user follows you + var theirFollowedUserData = await User.find({ email: { $in: theirFollowedUserEmails } }) // passed directly to the renderer + var usersWhoTrustThemArray = (await Relationship.find({ to: profileData.email, value: 'trust' }).catch(c)).map(v => v.from) // only used for the below + var usersWhoTrustThem = await User.find({ email: { $in: usersWhoTrustThemArray } }).catch(c) // passed directly to the renderer + var theirTrustedUserEmails = (await Relationship.find({ from: profileData.email, value: 'trust' }).catch(c)).map(v => v.to) // used to see if the profile user trusts the logged in user (if not isOwnProfile) and the below + var theirTrustedUserData = await User.find({ email: { $in: theirTrustedUserEmails } }).catch(c) // given directly to the renderer + + let userFollowsYou = false + let userTrustsYou = false + let isOwnProfile + let flagsFromTrustedUsers + let flagged + let trusted + let followed + let muted + if (req.isAuthenticated()) { + // Is this the logged in user's own profile? + if (profileData.email === req.user.email) { + isOwnProfile = true + userTrustsYou = false + userFollowsYou = false + trusted = false + followed = false + muted = false + flagged = false + flagsFromTrustedUsers = 0 + var myFlaggedUserEmails = (await Relationship.find({ from: req.user.email, value: 'flag' }).catch(c)).map(v => v.to) // only used in the below line + var myFlaggedUserData = await User.find({ email: { $in: myFlaggedUserEmails } }).catch(c) // passed directly to the renderer, but only actually used if isOwnProfile, so we're only actually defining it in here + } else { + isOwnProfile = false + + var myTrustedUserEmails = (await Relationship.find({ from: req.user.email, value: 'trust' }).catch(c)).map(v => v.to) // used for flag checking and to see if the logged in user trusts this user + + // Check if profile user follows and/or trusts logged in user + userTrustsYou = theirTrustedUserEmails.includes(req.user.email) // not sure if these includes are faster than an indexed query of the relationships collection would be + userFollowsYou = theirFollowedUserEmails.includes(req.user.email) + + // Check if logged in user follows and/or trusts and/or has muted profile user + trusted = myTrustedUserEmails.includes(profileData.email) + followed = !!(await Relationship.findOne({ from: req.user.email, to: profileData.email, value: 'follow' }).catch(c)) + muted = !!(await Relationship.findOne({ from: req.user.email, to: profileData.email, value: 'mute' }).catch(c)) + + var flagsOnUser = await Relationship.find({ to: profileData.email, value: 'flag' }).catch(c) + flagsFromTrustedUsers = 0 + flagged = false + for (var flag of flagsOnUser) { + // Check if logged in user has flagged profile user + if (flag.from === req.user.email) { + flagged = true + } + // Check if any of the logged in user's trusted users have flagged profile user + if (myTrustedUserEmails.includes(flag.from)) { + flagsFromTrustedUsers++ + } } + } + } else { + isOwnProfile = false + flagsFromTrustedUsers = 0 + trusted = false + followed = false + flagged = false + } + res.render('user', { + loggedIn: req.isAuthenticated(), + isOwnProfile: isOwnProfile, + loggedInUserData: req.user, + profileData: profileData, + trusted: trusted, + flagged: flagged, + muted: muted, + followed: followed, + followersData: followers, + usersWhoTrustThemData: usersWhoTrustThem, + userFollowsYou: userFollowsYou, + userTrustsYou: userTrustsYou, + trustedUserData: theirTrustedUserData, + followedUserData: theirFollowedUserData, + communitiesData: communitiesData, + flaggedUserData: myFlaggedUserData, + flagsFromTrustedUsers: flagsFromTrustedUsers, + activePage: profileData.username, + visibleSidebarArray: ['profileOnly', 'profileAndPosts'] }) - - app.post('/api/newpostform/linkpreviewdata', async function(req, res) { - try { - const metadata = await helper.getLinkMetadata(req.body.url); - res.setHeader('content-type', 'text/plain'); - res.send(JSON.stringify(metadata)) - } catch (err) { - console.log("could not get link preview information for url " + req.body.url) - console.log(err); - res.send("invalid url i guess"); - } + }) + + // Responds to post request from the browser informing us that the user has seen the comments of some post by setting notifications about those comments + // to seen=true + // Input: + app.post('/api/notification/update/:id', isLoggedInOrRedirect, function (req, res) { + User.findOneAndUpdate({ + _id: req.user._id, + 'notifications._id': req.params.id + }, { + $set: { + 'notifications.$.seen': true + } + }, + (_, doc) => res.sendStatus(200) + ) + }) + + app.post('/api/notification/update-by-subject/:subjectid', isLoggedInOrRedirect, function (req, res) { + User.findOne({ + _id: req.user._id }) + .then(user => { + user.notifications.forEach(notification => { + if (notification.subjectId === req.params.subjectid) { + notification.seen = true + } + }) + user.save() + .then(response => { + res.sendStatus(200) + }) + }) + }) + + app.get('/api/notification/display', function (req, res) { + if (req.isAuthenticated()) { + User.findOne({ + _id: req.user._id + }, 'notifications') + .then(user => { + user.notifications.reverse() + res.render('partials/notifications', { + layout: false, + loggedIn: true, + loggedInUserData: req.user, + notifications: user.notifications + }) + }) + } else { + res.render('partials/notifications', { + layout: false, + loggedIn: false + }) + } + }) + + app.post('/api/newpostform/linkpreviewdata', async function (req, res) { + try { + const metadata = await helper.getLinkMetadata(req.body.url) + res.setHeader('content-type', 'text/plain') + res.send(JSON.stringify(metadata)) + } catch (err) { + console.log('could not get link preview information for url ' + req.body.url) + console.log(err) + res.send('invalid url i guess') + } + }) - app.post('/admin/reporterror', function(req, res) { - fs.appendFile("clientsideerrors.txt", req.body.errorstring + "\n\n", (error) => { - if (error) { - console.error(error) - } - }); - res.status(200).send('thank'); + app.post('/admin/reporterror', function (req, res) { + fs.appendFile('clientsideerrors.txt', req.body.errorstring + '\n\n', (error) => { + if (error) { + console.error(error) + } }) + res.status(200).send('thank') + }) - app.get('/admin/errorlogs/:password', function(req, res) { - var passwordHash = "$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq" - if (req.isAuthenticated() && bcrypt.compareSync(req.params.password, passwordHash) && fs.existsSync(path.resolve(global.appRoot, "clientsideerrors.txt"))) { - res.status(200).sendFile(path.resolve(global.appRoot, "clientsideerrors.txt")); - } - }) + app.get('/admin/errorlogs/:password', function (req, res) { + var passwordHash = '$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq' + if (req.isAuthenticated() && bcrypt.compareSync(req.params.password, passwordHash) && fs.existsSync(path.resolve(global.appRoot, 'clientsideerrors.txt'))) { + res.status(200).sendFile(path.resolve(global.appRoot, 'clientsideerrors.txt')) + } + }) - app.get('/admin/emaillogs/:password', function(req, res) { - var passwordHash = "$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq" - if (req.isAuthenticated() && bcrypt.compareSync(req.params.password, passwordHash) && fs.existsSync(path.resolve(global.appRoot, "emailLog.txt"))) { - res.status(200).sendFile(path.resolve(global.appRoot, "emailLog.txt")); - } - }) -}; + app.get('/admin/emaillogs/:password', function (req, res) { + var passwordHash = '$2a$08$RDb0G8GsaJZ0TIC/GcpZY.7eaASgXX0HO6d5RZ7JHMmD8eiJiGaGq' + if (req.isAuthenticated() && bcrypt.compareSync(req.params.password, passwordHash) && fs.existsSync(path.resolve(global.appRoot, 'emailLog.txt'))) { + res.status(200).sendFile(path.resolve(global.appRoot, 'emailLog.txt')) + } + }) +} -//For post and get requests where the browser will handle the response automatically and so redirects will work -function isLoggedInOrRedirect(req, res, next) { - if (req.isAuthenticated()) { - // A potentially expensive way to update a user's last logged in timestamp (currently only relevant to sorting search results) - currentTime = new Date(); - if ((currentTime - req.user.lastUpdated) > 3600000) { // If the timestamp is older than an hour - User.findOne({ - _id: req.user._id - }) - .then(user => { - user.lastUpdated = currentTime; - user.save() - }) - } - return next(); +// For post and get requests where the browser will handle the response automatically and so redirects will work +function isLoggedInOrRedirect (req, res, next) { + if (req.isAuthenticated()) { + // A potentially expensive way to update a user's last logged in timestamp (currently only relevant to sorting search results) + const currentTime = new Date() + if ((currentTime - req.user.lastUpdated) > 3600000) { // If the timestamp is older than an hour + User.findOne({ + _id: req.user._id + }) + .then(user => { + user.lastUpdated = currentTime + user.save() + }) } - res.redirect('/'); + return next() + } + res.redirect('/') } diff --git a/config/passport.js b/config/passport.js index c406a9b6..3faff2b2 100644 --- a/config/passport.js +++ b/config/passport.js @@ -6,9 +6,7 @@ const Relationship = require('../app/models/relationship'); const crypto = require('crypto'); -var apiConfig = require('./apis.js'); -const sgMail = require('@sendgrid/mail'); -sgMail.setApiKey(apiConfig.sendgrid); +const sgMail = require('../app/mail'); // expose this function to our app using module.exports module.exports = function(passport) { @@ -112,7 +110,7 @@ function(req, email, password, done) { 'If you did not create an account on sweet, please ignore and delete this email. The token will expire in an hour.\n' }; sgMail.send(msg) - .then(user => { + .then(() => { var sweetbotFollow = new Relationship(); sweetbotFollow.from = email; sweetbotFollow.to = 'support@sweet.sh'; diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index c982156a..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6784 +0,0 @@ -{ - "name": "sweet", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@metascraper/helpers": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@metascraper/helpers/-/helpers-5.4.0.tgz", - "integrity": "sha512-kGg8Z3fcSyvfXWKaSWt7xUsZWzC40/TUg3SFTqvrykw92QGH4wZvpjTv989Cp2QbdzQO3LuAllkio6sHYZT+sg==", - "requires": { - "audio-extensions": "0.0.0", - "chrono-node": "~1.3.11", - "condense-whitespace": "~2.0.0", - "entities": "~1.1.2", - "file-extension": "~4.0.5", - "image-extensions": "~1.1.0", - "is-relative-url": "~3.0.0", - "is-uri": "~1.2.0", - "iso-639-3": "~1.2.0", - "isostring": "0.0.1", - "lodash": "~4.17.11", - "mem": "~5.0.0", - "mime-types": "~2.1.24", - "normalize-url": "~4.3.0", - "smartquotes": "~2.3.1", - "title": "~3.4.1", - "truncate": "~2.1.0", - "url-regex": "~5.0.0", - "video-extensions": "~1.1.0" - }, - "dependencies": { - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "requires": { - "mime-db": "1.40.0" - } - } - } - }, - "@sendgrid/client": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.3.0.tgz", - "integrity": "sha512-fTy8vRpA9Whtf8ULQr/0vkSZaQvGQ97rY5N5PrevKRtugJMsJqFMKO0pwzEWeqITSg71aMMTj57QTgw3SjZvnQ==", - "requires": { - "@sendgrid/helpers": "^6.3.0", - "@types/request": "^2.0.3", - "request": "^2.81.0" - } - }, - "@sendgrid/helpers": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-6.3.0.tgz", - "integrity": "sha512-uTFcmhCDFg/2Uhz+z/cLwyLHH0UsblG49hKwdR7nKbWsGKWv4js7W32FlPdXqy2C/plTJ20vcPLgKM1m3F/MjQ==", - "requires": { - "chalk": "^2.0.1", - "deepmerge": "^2.1.1" - } - }, - "@sendgrid/mail": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-6.3.1.tgz", - "integrity": "sha512-5zIeAV9iU+0hQkrOQ/D4RB2MfpK+lNbOortIfQdCh95aMDF/TRc9WB8FGNhmQrx9YMuJTms5eiBklF0Fi/dbVg==", - "requires": { - "@sendgrid/client": "^6.3.0", - "@sendgrid/helpers": "^6.3.0" - } - }, - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.0.tgz", - "integrity": "sha512-rx29MMkRdVmzunmiA4lzBYJNnXsW/PhG4kMBy2ATsYaDjGGR75dCFEVVROKpNwlVdcUX3xxlghKQOeDPBJobng==" - }, - "@types/request": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", - "requires": { - "@types/caseless": "*", - "@types/form-data": "*", - "@types/node": "*", - "@types/tough-cookie": "*" - } - }, - "@types/tough-cookie": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", - "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", - "dev": true, - "requires": { - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "ansi-bgblack": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bgblack/-/ansi-bgblack-0.1.1.tgz", - "integrity": "sha1-poulAHiHcBtqr74/oNrf36juPKI=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bgblue": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bgblue/-/ansi-bgblue-0.1.1.tgz", - "integrity": "sha1-Z73ATtybm1J4lp2hlt6j11yMNhM=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bgcyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bgcyan/-/ansi-bgcyan-0.1.1.tgz", - "integrity": "sha1-WEiUJWAL3p9VBwaN2Wnr/bUP52g=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bggreen": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bggreen/-/ansi-bggreen-0.1.1.tgz", - "integrity": "sha1-TjGRJIUplD9DIelr8THRwTgWr0k=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bgmagenta": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bgmagenta/-/ansi-bgmagenta-0.1.1.tgz", - "integrity": "sha1-myhDLAduqpmUGGcqPvvhk5HCx6E=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bgred": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bgred/-/ansi-bgred-0.1.1.tgz", - "integrity": "sha1-p2+Sg4OCukMpCmwXeEJPmE1vEEE=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bgwhite": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bgwhite/-/ansi-bgwhite-0.1.1.tgz", - "integrity": "sha1-ZQRlE3elim7OzQMxmU5IAljhG6g=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bgyellow": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bgyellow/-/ansi-bgyellow-0.1.1.tgz", - "integrity": "sha1-w/4usIzUdmSAKeaHTRWgs49h1E8=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-black": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-black/-/ansi-black-0.1.1.tgz", - "integrity": "sha1-9hheiJNgslRaHsUMC/Bj/EMDJFM=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-blue": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-blue/-/ansi-blue-0.1.1.tgz", - "integrity": "sha1-FbgEmQ6S/JyoxUds6PaZd3wh7b8=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-bold": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-bold/-/ansi-bold-0.1.1.tgz", - "integrity": "sha1-PmOVCvWswq4uZw5vZ96xFdGl9QU=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-colors": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-0.2.0.tgz", - "integrity": "sha1-csMd4qDZoszQysMMyYI+6y9kNLU=", - "requires": { - "ansi-bgblack": "^0.1.1", - "ansi-bgblue": "^0.1.1", - "ansi-bgcyan": "^0.1.1", - "ansi-bggreen": "^0.1.1", - "ansi-bgmagenta": "^0.1.1", - "ansi-bgred": "^0.1.1", - "ansi-bgwhite": "^0.1.1", - "ansi-bgyellow": "^0.1.1", - "ansi-black": "^0.1.1", - "ansi-blue": "^0.1.1", - "ansi-bold": "^0.1.1", - "ansi-cyan": "^0.1.1", - "ansi-dim": "^0.1.1", - "ansi-gray": "^0.1.1", - "ansi-green": "^0.1.1", - "ansi-grey": "^0.1.1", - "ansi-hidden": "^0.1.1", - "ansi-inverse": "^0.1.1", - "ansi-italic": "^0.1.1", - "ansi-magenta": "^0.1.1", - "ansi-red": "^0.1.1", - "ansi-reset": "^0.1.1", - "ansi-strikethrough": "^0.1.1", - "ansi-underline": "^0.1.1", - "ansi-white": "^0.1.1", - "ansi-yellow": "^0.1.1", - "lazy-cache": "^2.0.1" - } - }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-dim": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-dim/-/ansi-dim-0.1.1.tgz", - "integrity": "sha1-QN5MYDqoCG2Oeoa4/5mNXDbu/Ww=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-green": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-green/-/ansi-green-0.1.1.tgz", - "integrity": "sha1-il2al55FjVfEDjNYCzc5C44Q0Pc=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-grey": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-grey/-/ansi-grey-0.1.1.tgz", - "integrity": "sha1-WdmLasK6GfilF5jphT+6eDOaM8E=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-hidden": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-hidden/-/ansi-hidden-0.1.1.tgz", - "integrity": "sha1-7WpMSY0rt8uyidvyqNHcyFZ/rg8=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-inverse": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-inverse/-/ansi-inverse-0.1.1.tgz", - "integrity": "sha1-tq9Fgm/oJr+1KKbHmIV5Q1XM0mk=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-italic": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-italic/-/ansi-italic-0.1.1.tgz", - "integrity": "sha1-EEdDRj9iXBQqA2c5z4XtpoiYbyM=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-magenta": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-magenta/-/ansi-magenta-0.1.1.tgz", - "integrity": "sha1-BjtboW+z8j4c/aKwfAqJ3hHkMK4=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-reset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-reset/-/ansi-reset-0.1.1.tgz", - "integrity": "sha1-5+cSksPH3c1NYu9KbHwFmAkRw7c=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-strikethrough": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-strikethrough/-/ansi-strikethrough-0.1.1.tgz", - "integrity": "sha1-2Eh3FAss/wfRyT685pkE9oiF5Wg=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "ansi-underline": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-underline/-/ansi-underline-0.1.1.tgz", - "integrity": "sha1-38kg9Ml7WXfqFi34/7mIMIqqcaQ=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-white": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-white/-/ansi-white-0.1.1.tgz", - "integrity": "sha1-nHe3wZPF7pkuYBHTbsTJIbRXiUQ=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=" - }, - "ansi-yellow": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-yellow/-/ansi-yellow-0.1.1.tgz", - "integrity": "sha1-y5NW8vRscy8OMZnmEClVp32oPB0=", - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "arch": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.1.tgz", - "integrity": "sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "arg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/arg/-/arg-1.0.0.tgz", - "integrity": "sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw==" - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "array-sort": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-0.1.4.tgz", - "integrity": "sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ==", - "requires": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.2.0.tgz", - "integrity": "sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "audio-extensions": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/audio-extensions/-/audio-extensions-0.0.0.tgz", - "integrity": "sha1-0O7+B3+562JYmO7ZmFiQVIzx+NI=" - }, - "autolinker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.0.5.tgz", - "integrity": "sha512-3cUfuYHAZHEluASJcGdZ/VSRHqOMKfNCHNso2RDCuMTdWKIjEf4+DUZ9ZVnLHP7NC7FSa7CII+xYdzF346WWlg==", - "requires": { - "tslib": "^1.9.3" - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - } - }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, - "bcrypt-nodejs": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/bcrypt-nodejs/-/bcrypt-nodejs-0.0.3.tgz", - "integrity": "sha1-xgkX8m3CNWYVZsaBBhwwPCsohCs=" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" - }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, - "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "bson": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", - "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, - "busboy": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", - "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", - "requires": { - "dicer": "0.2.5", - "readable-stream": "1.1.x" - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cacheable-request": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.0.0.tgz", - "integrity": "sha512-2N7AmszH/WPPpl5Z3XMw1HAP+8d+xugnKQAeKvxFZ/04dbT/CAznqwbl+7eSr3HkwdepNwtb2yx3CAMQWvG01Q==", - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^4.0.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^1.0.1", - "normalize-url": "^3.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - } - }, - "capture-stack-trace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", - "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "cheerio": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", - "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", - "requires": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.1", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash": "^4.15.0", - "parse5": "^3.0.1" - } - }, - "cheerio-advanced-selectors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cheerio-advanced-selectors/-/cheerio-advanced-selectors-2.0.1.tgz", - "integrity": "sha512-5wHR8bpiD5pdUtaS81A6hnJezzoDzL1TLWfK6bxnLkIgEKPV26BlOdMCcvuj3fTE7JSalsTUeNU7AOD/u6bYhw==" - }, - "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" - }, - "chrono-node": { - "version": "1.3.11", - "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-1.3.11.tgz", - "integrity": "sha512-jDWRnY6nYvzfV3HPYBqo+tot7tcsUs9i3arGbMdI0TouPSXP2C2y/Ctp27rxKTQDi6yuTxAB2cw+Q6igGhOhdQ==", - "requires": { - "moment": "2.21.0" - }, - "dependencies": { - "moment": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", - "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" - } - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "clean-stack": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.0.0.tgz", - "integrity": "sha512-VEoL9Qh7I8s8iHnV53DaeWSt8NJ0g3khMfK6NiCPB7H657juhro+cSw2O88uo3bo0c0X5usamtXk0/Of0wXa5A==" - }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "clipboardy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-1.2.2.tgz", - "integrity": "sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw==", - "requires": { - "arch": "^2.1.0", - "execa": "^0.8.0" - } - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", - "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", - "requires": { - "mime-db": ">= 1.40.0 < 2" - }, - "dependencies": { - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - } - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-with-sourcemaps": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", - "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", - "requires": { - "source-map": "^0.6.1" - } - }, - "condense-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/condense-whitespace/-/condense-whitespace-2.0.0.tgz", - "integrity": "sha512-Ath9o58/0rxZXbyoy3zZgrVMoIemi30sukG/btuMKCLyqfQt3dNOWc9N3EHEMa2Q3i0tXQPDJluYFLwy7pJuQw==" - }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "connect-flash": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", - "integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA=" - }, - "connect-mongo": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-2.0.3.tgz", - "integrity": "sha512-Vs+QZ/6X6gbCrP1Ls7Oh/wlyY6pgpbPSrUKF5yRT+zd+4GZPNbjNquxquZ+Clv2+03HBXE7T4lVM0PUcaBhihg==", - "requires": { - "mongodb": "^2.0.36" - }, - "dependencies": { - "bson": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", - "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" - }, - "mongodb": { - "version": "2.2.36", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.36.tgz", - "integrity": "sha512-P2SBLQ8Z0PVx71ngoXwo12+FiSfbNfGOClAao03/bant5DgLNkOPAck5IaJcEk4gKlQhDEURzfR3xuBG1/B+IA==", - "requires": { - "es6-promise": "3.2.1", - "mongodb-core": "2.1.20", - "readable-stream": "2.2.7" - } - }, - "mongodb-core": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.20.tgz", - "integrity": "sha512-IN57CX5/Q1bhDq6ShAR6gIv4koFsZP7L8WOK1S0lR0pVDQaScffSMV5jxubLsmZ7J+UdqmykKw4r9hG3XQEGgQ==", - "requires": { - "bson": "~1.0.4", - "require_optional": "~1.0.0" - } - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "readable-stream": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", - "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", - "requires": { - "buffer-shims": "~1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~1.0.0", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-parser": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", - "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", - "requires": { - "cookie": "0.3.1", - "cookie-signature": "1.0.6" - } - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "crc": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz", - "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "create-frame": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/create-frame/-/create-frame-1.0.0.tgz", - "integrity": "sha1-i5XyaR4ySbYIBEPjPQutn49pdao=", - "requires": { - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "isobject": "^3.0.0", - "lazy-cache": "^2.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cron-parser": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.9.0.tgz", - "integrity": "sha512-WkHhWssz4OxEdepOIt3dXYQiKZgPwgeF1yzBMlLwnUCwv0ziNeINNbMs5haoG10ikM/j0LMrrhEsuK2Blt638w==", - "requires": { - "is-nan": "^1.2.1", - "moment-timezone": "^0.5.23" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - }, - "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - } - } - }, - "css-what": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", - "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==" - }, - "cssfilter": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", - "integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4=" - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "date.js": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", - "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==", - "requires": { - "debug": "~3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deepmerge": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", - "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" - }, - "default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "requires": { - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "defer-to-connect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.0.2.tgz", - "integrity": "sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw==" - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "dicer": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", - "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", - "requires": { - "readable-stream": "1.1.x", - "streamsearch": "0.1.2" - } - }, - "dom-serializer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", - "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", - "requires": { - "domelementtype": "^1.3.0", - "entities": "^1.1.1" - } - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" - }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - } - } - }, - "error-symbol": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/error-symbol/-/error-symbol-0.1.0.tgz", - "integrity": "sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y=" - }, - "es6-promise": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", - "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - }, - "dependencies": { - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - } - } - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "execa": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", - "integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=", - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" - }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "express-fileupload": { - "version": "1.1.3-alpha.1", - "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.1.3-alpha.1.tgz", - "integrity": "sha512-u87l4778UiOKAPWL+SOw+hIRV6CMPgz96H39AAsXGJar6ke7LNbdOZCkG9aXxJHDC0cmX/cHfgilXee0MHDgDQ==", - "requires": { - "busboy": "^0.2.14" - } - }, - "express-handlebars": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-3.0.2.tgz", - "integrity": "sha512-rPaSqR8xPnSqfvWvI8Mhtn7nifaMmySq6yhWkjH07Ks/nuDaRngJyf7eDN2I7PBkNVdZHf0Bz+1rY1yrZFdx8g==", - "requires": { - "glob": "^7.1.3", - "graceful-fs": "^4.1.2", - "handlebars": "^4.0.13", - "object.assign": "^4.1.0", - "promise": "^8.0.2" - } - }, - "express-messages": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/express-messages/-/express-messages-1.0.1.tgz", - "integrity": "sha1-mYGoWl0rEYx5/DP1K0GDT/7paFo=" - }, - "express-mongo-sanitize": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/express-mongo-sanitize/-/express-mongo-sanitize-1.3.2.tgz", - "integrity": "sha1-+6QE9sBBV3y+7sTdkFfO+7Q53lo=" - }, - "express-session": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz", - "integrity": "sha512-r0nrHTCYtAMrFwZ0kBzZEXa1vtPVrw0dKvGSrKP4dahwBQ1BJpF2/y1Pp4sCD/0kvxV4zZeclyvfmw0B4RMJQA==", - "requires": { - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "crc": "3.4.4", - "debug": "2.6.9", - "depd": "~1.1.1", - "on-headers": "~1.0.1", - "parseurl": "~1.3.2", - "uid-safe": "~2.1.5", - "utils-merge": "1.0.1" - } - }, - "express-validator": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-5.3.1.tgz", - "integrity": "sha512-g8xkipBF6VxHbO1+ksC7nxUU7+pWif0+OZXjZTybKJ/V0aTVhuCoHbyhIPgSYVldwQLocGExPtB2pE0DqK4jsw==", - "requires": { - "lodash": "^4.17.10", - "validator": "^10.4.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "falsey": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/falsey/-/falsey-0.3.2.tgz", - "integrity": "sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg==", - "requires": { - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "file-extension": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/file-extension/-/file-extension-4.0.5.tgz", - "integrity": "sha512-l0rOL3aKkoi6ea7MNZe6OHgqYYpn48Qfflr8Pe9G9JPPTx5A+sfboK91ZufzIs59/lPqh351l0eb6iKU9J5oGg==" - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "requires": { - "for-in": "^1.0.1" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" - }, - "fs-minipass": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", - "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-object": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/get-object/-/get-object-0.2.0.tgz", - "integrity": "sha1-2S/31RkMZFMM2gVD2sY6PUf+jAw=", - "requires": { - "is-number": "^2.0.2", - "isobject": "^0.2.0" - }, - "dependencies": { - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "isobject": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-0.2.0.tgz", - "integrity": "sha1-o0MhkvObkQtfAsyYlIeDbscKqF4=" - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" - }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "dependencies": { - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" - }, - "gulp-header": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", - "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", - "requires": { - "concat-with-sourcemaps": "*", - "lodash.template": "^4.4.0", - "through2": "^2.0.0" - } - }, - "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - } - }, - "handlebars-helper-create-frame": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/handlebars-helper-create-frame/-/handlebars-helper-create-frame-0.1.0.tgz", - "integrity": "sha1-iqUdEK62QI/MZgXUDXc1YohIegM=", - "requires": { - "create-frame": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "handlebars-helpers": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/handlebars-helpers/-/handlebars-helpers-0.10.0.tgz", - "integrity": "sha512-QiyhQz58u/DbuV41VnfpE0nhy6YCH4vB514ajysV8SoKmP+DxU+pR+fahVyNECHj+jiwEN2VrvxD/34/yHaLUg==", - "requires": { - "arr-flatten": "^1.1.0", - "array-sort": "^0.1.4", - "create-frame": "^1.0.0", - "define-property": "^1.0.0", - "falsey": "^0.3.2", - "for-in": "^1.0.2", - "for-own": "^1.0.0", - "get-object": "^0.2.0", - "get-value": "^2.0.6", - "handlebars": "^4.0.11", - "handlebars-helper-create-frame": "^0.1.0", - "handlebars-utils": "^1.0.6", - "has-value": "^1.0.0", - "helper-date": "^1.0.1", - "helper-markdown": "^1.0.0", - "helper-md": "^0.2.2", - "html-tag": "^2.0.0", - "is-even": "^1.0.0", - "is-glob": "^4.0.0", - "is-number": "^4.0.0", - "kind-of": "^6.0.0", - "lazy-cache": "^2.0.2", - "logging-helpers": "^1.0.0", - "micromatch": "^3.1.4", - "relative": "^3.0.2", - "striptags": "^3.1.0", - "to-gfm-code-block": "^0.1.1", - "year": "^0.2.1" - } - }, - "handlebars-utils": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/handlebars-utils/-/handlebars-utils-1.0.6.tgz", - "integrity": "sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw==", - "requires": { - "kind-of": "^6.0.0", - "typeof-article": "^0.1.1" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "helper-date": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/helper-date/-/helper-date-1.0.1.tgz", - "integrity": "sha512-wU3VOwwTJvGr/w5rZr3cprPHO+hIhlblTJHD6aFBrKLuNbf4lAmkawd2iK3c6NbJEvY7HAmDpqjOFSI5/+Ey2w==", - "requires": { - "date.js": "^0.3.1", - "handlebars-utils": "^1.0.4", - "moment": "^2.18.1" - } - }, - "helper-markdown": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/helper-markdown/-/helper-markdown-1.0.0.tgz", - "integrity": "sha512-AnDqMS4ejkQK0MXze7pA9TM3pu01ZY+XXsES6gEE0RmCGk5/NIfvTn0NmItfyDOjRAzyo9z6X7YHbHX4PzIvOA==", - "requires": { - "handlebars-utils": "^1.0.2", - "highlight.js": "^9.12.0", - "remarkable": "^1.7.1" - } - }, - "helper-md": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/helper-md/-/helper-md-0.2.2.tgz", - "integrity": "sha1-wfWdflW7riM2L9ig6XFgeuxp1B8=", - "requires": { - "ent": "^2.2.0", - "extend-shallow": "^2.0.1", - "fs-exists-sync": "^0.1.0", - "remarkable": "^1.6.2" - } - }, - "highlight.js": { - "version": "9.15.6", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.6.tgz", - "integrity": "sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ==" - }, - "hosted-git-info": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", - "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", - "dev": true - }, - "html-tag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-tag/-/html-tag-2.0.0.tgz", - "integrity": "sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g==", - "requires": { - "is-self-closing": "^1.0.1", - "kind-of": "^6.0.0" - } - }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.2.0.tgz", - "integrity": "sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "http-cache-semantics": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", - "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "http_ece": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz", - "integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==", - "requires": { - "urlsafe-base64": "~1.0.0" - } - }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "image-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/image-extensions/-/image-extensions-1.1.0.tgz", - "integrity": "sha1-uOa/YDnfAFbjM1AqALZjejEF2JQ=" - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "in-publish": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", - "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "info-symbol": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/info-symbol/-/info-symbol-0.1.0.tgz", - "integrity": "sha1-J4QdcoZ920JCzWEtecEGM4gcang=" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "ip-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz", - "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==" - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" - }, - "is-absolute-url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.0.tgz", - "integrity": "sha512-3OkP8XrM2Xq4/IxsJnClfMp3OaM3TAatLPLKPeWcxLBTrpe6hihwtX+XZfJTcXg/FTRi4qjy0y/C5qiyNxY24g==" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-even": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-even/-/is-even-1.0.0.tgz", - "integrity": "sha1-drUFX7rY0pSoa2qUkBXhyXtxfAY=", - "requires": { - "is-odd": "^0.1.2" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", - "dev": true, - "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" - } - }, - "is-nan": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.2.1.tgz", - "integrity": "sha1-n69ltvttskt/XAYoR16nH5iEAeI=", - "requires": { - "define-properties": "^1.1.1" - } - }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-odd": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-0.1.2.tgz", - "integrity": "sha1-vFc7XONx7yqtbm9JeZtyvvE5eKc=", - "requires": { - "is-number": "^3.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, - "is-relative-url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-relative-url/-/is-relative-url-3.0.0.tgz", - "integrity": "sha512-U1iSYRlY2GIMGuZx7gezlB5dp1Kheaym7zKzO1PV06mOihiWTXejLwm4poEJysPyXF+HtK/BEd0DVlcCh30pEA==", - "requires": { - "is-absolute-url": "^3.0.0" - } - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true - }, - "is-self-closing": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-self-closing/-/is-self-closing-1.0.1.tgz", - "integrity": "sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg==", - "requires": { - "self-closing-tags": "^1.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-uri/-/is-uri-1.2.0.tgz", - "integrity": "sha1-uS/yNK9owO2X0u7UZJLQF5O31CA=", - "requires": { - "parse-uri": "~1.0.0", - "punycode2": "~1.0.0" - } - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "iso-639-3": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/iso-639-3/-/iso-639-3-1.2.0.tgz", - "integrity": "sha512-jNvD2P4JHNckQH7pc0R0SQ4oPCpyEtgs0nTtjB+DZCUDdygz0cOAxlcnq5KgNjjsqMHbR4Sbgwz2+DflzAZvlQ==" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "isostring": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isostring/-/isostring-0.0.1.tgz", - "integrity": "sha1-3bYI77/InNqG25yxa+CQp4gTTH8=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "js-base64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", - "dev": true - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "kareem": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.0.tgz", - "integrity": "sha512-6hHxsp9e6zQU8nXsP+02HGWXwTkOEw6IROhF2ZA28cYbUk4eJ6QbtZvdqZOdD9YPKghG3apk5eOCvs+tLl3lRg==" - }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "requires": { - "json-buffer": "3.0.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "latest-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, - "requires": { - "package-json": "^4.0.0" - } - }, - "lazy-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", - "requires": { - "set-getter": "^0.1.0" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" - }, - "lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" - }, - "lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "log-ok": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/log-ok/-/log-ok-0.1.1.tgz", - "integrity": "sha1-vqPdNqzQuKckDXhza1uXxlREozQ=", - "requires": { - "ansi-green": "^0.1.1", - "success-symbol": "^0.1.0" - } - }, - "log-utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/log-utils/-/log-utils-0.2.1.tgz", - "integrity": "sha1-pMIXoN2aUFFdm5ICBgkas9TgMc8=", - "requires": { - "ansi-colors": "^0.2.0", - "error-symbol": "^0.1.0", - "info-symbol": "^0.1.0", - "log-ok": "^0.1.1", - "success-symbol": "^0.1.0", - "time-stamp": "^1.0.1", - "warning-symbol": "^0.1.0" - } - }, - "logging-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/logging-helpers/-/logging-helpers-1.0.0.tgz", - "integrity": "sha512-qyIh2goLt1sOgQQrrIWuwkRjUx4NUcEqEGAcYqD8VOnOC6ItwkrVE8/tA4smGpjzyp4Svhc6RodDp9IO5ghpyA==", - "requires": { - "isobject": "^3.0.0", - "log-utils": "^0.2.1" - } - }, - "long-timeout": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", - "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - }, - "dependencies": { - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - } - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "mem": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-5.0.0.tgz", - "integrity": "sha512-r0kKFSQy7ywj2AHxsCyRlIQqnqiazZBeOx6ZNVvfJtEEzqVMgvgp83tB30B9pcqfGDXXPHLuVgCXLwNE2Un6lQ==", - "requires": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^2.1.0", - "p-is-promise": "^2.1.0" - } - }, - "memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "optional": true - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "metascraper": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/metascraper/-/metascraper-5.4.0.tgz", - "integrity": "sha512-b5HRqRMGnRP7kavSD/2I4cJJwOT6VzIq0pHQ//wOsx7U5Ohb6HqbYMyRzeT099q2GQTmowvTwl4VpPZnR76OZQ==", - "requires": { - "@metascraper/helpers": "^5.4.0", - "cheerio": "~1.0.0-rc.2", - "cheerio-advanced-selectors": "~2.0.1", - "lodash": "~4.17.11", - "whoops": "~4.0.2", - "xss": "~1.0.6" - } - }, - "metascraper-description": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/metascraper-description/-/metascraper-description-5.4.0.tgz", - "integrity": "sha512-42W8GwcyC/TYnIp10z07bO+wYtyUlaRoOXV8Bt5b2VurMLXOunnuZfrGEB6TgI6KsHV8moT5ASL3GQm85mE9uA==", - "requires": { - "@metascraper/helpers": "^5.4.0" - } - }, - "metascraper-image": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/metascraper-image/-/metascraper-image-5.4.0.tgz", - "integrity": "sha512-D4IwfmpfucfSb/MWJ5tEHJP+PZBLXi1dc4skDmlgk6cREQ576xriNgXb4VzmbfhIA9sCpFblM1y4/vAvKcEdXQ==", - "requires": { - "@metascraper/helpers": "^5.4.0" - } - }, - "metascraper-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/metascraper-title/-/metascraper-title-5.4.0.tgz", - "integrity": "sha512-FB1k7cEAmDjiuaA3G5CLsIWrwZ7ngE4oVTGQbkdlbWnfXKcVdaW3rDJRQkYDMnYUfY2ZM6Np6eN8EQqO3oz8bg==", - "requires": { - "@metascraper/helpers": "^5.4.0", - "lodash": "~4.17.11" - } - }, - "metascraper-url": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/metascraper-url/-/metascraper-url-5.4.0.tgz", - "integrity": "sha512-68ePaC3HZ79q4VK31UzOa6TWTLbOri83JEH0Xeu8GAlzE2LO+y+KTpxszPzTogWMkLpB6lb7XiCbsdzhWuyEFQ==", - "requires": { - "@metascraper/helpers": "^5.4.0" - } - }, - "method-override": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/method-override/-/method-override-3.0.0.tgz", - "integrity": "sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==", - "requires": { - "debug": "3.1.0", - "methods": "~1.1.2", - "parseurl": "~1.3.2", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" - }, - "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", - "requires": { - "mime-db": "~1.38.0" - } - }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - }, - "minipass": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.4.0.tgz", - "integrity": "sha512-6PmOuSP4NnZXzs2z6rbwzLJu/c5gdzYg1mRI/WIYdx45iiX7T+a4esOzavD6V/KmBzAaopFSTZPZcUx73bqKWA==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "requires": { - "minipass": "^2.2.1" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, - "moment-timezone": { - "version": "0.5.25", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz", - "integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==", - "requires": { - "moment": ">= 2.9.0" - } - }, - "mongodb": { - "version": "3.1.13", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.13.tgz", - "integrity": "sha512-sz2dhvBZQWf3LRNDhbd30KHVzdjZx9IKC0L+kSZ/gzYquCF5zPOgGqRz6sSCqYZtKP2ekB4nfLxhGtzGHnIKxA==", - "requires": { - "mongodb-core": "3.1.11", - "safe-buffer": "^5.1.2" - } - }, - "mongodb-core": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.11.tgz", - "integrity": "sha512-rD2US2s5qk/ckbiiGFHeu+yKYDXdJ1G87F6CG3YdaZpzdOm5zpoAZd/EKbPmFO6cQZ+XVXBXBJ660sSI0gc6qg==", - "requires": { - "bson": "^1.1.0", - "require_optional": "^1.0.1", - "safe-buffer": "^5.1.2", - "saslprep": "^1.0.0" - } - }, - "mongoose": { - "version": "5.4.19", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.4.19.tgz", - "integrity": "sha512-paRU3nbCrPIUVw1GAlxo11uIIqrYORctUx1kcLj7i2NhkxPQuy5OK2/FYj8+tglsaixycmONSyop2HQp1IUQSA==", - "requires": { - "async": "2.6.1", - "bson": "~1.1.0", - "kareem": "2.3.0", - "mongodb": "3.1.13", - "mongodb-core": "3.1.11", - "mongoose-legacy-pluralize": "1.0.2", - "mpath": "0.5.1", - "mquery": "3.2.0", - "ms": "2.1.1", - "regexp-clone": "0.0.1", - "safe-buffer": "5.1.2", - "sliced": "1.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "mongoose-legacy-pluralize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", - "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" - }, - "morgan": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz", - "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==", - "requires": { - "basic-auth": "~2.0.0", - "debug": "2.6.9", - "depd": "~1.1.2", - "on-finished": "~2.3.0", - "on-headers": "~1.0.1" - } - }, - "mpath": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.1.tgz", - "integrity": "sha512-H8OVQ+QEz82sch4wbODFOz+3YQ61FYz/z3eJ5pIdbMEaUzDqA268Wd+Vt4Paw9TJfvDgVKaayC0gBzMIw2jhsg==" - }, - "mquery": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.0.tgz", - "integrity": "sha512-qPJcdK/yqcbQiKoemAt62Y0BAc0fTEKo1IThodBD+O5meQRJT/2HSe5QpBNwaa4CjskoGrYWsEyjkqgiE0qjhg==", - "requires": { - "bluebird": "3.5.1", - "debug": "3.1.0", - "regexp-clone": "0.0.1", - "safe-buffer": "5.1.2", - "sliced": "1.0.1" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "dev": true - }, - "nanoid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.0.1.tgz", - "integrity": "sha512-k1u2uemjIGsn25zmujKnotgniC/gxQ9sdegdezeDiKdkDW56THUMqlz3urndKCXJxA6yPzSZbXx/QCMe/pxqsA==" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "napi-build-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", - "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" - }, - "node-abi": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.11.0.tgz", - "integrity": "sha512-kuy/aEg75u40v378WRllQ4ZexaXJiCvB68D2scDXclp/I4cRq6togpbOoKhmN07tns9Zldu51NNERo0wehfX9g==", - "requires": { - "semver": "^5.4.1" - } - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "dev": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "dev": true - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - } - } - }, - "node-sass": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", - "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", - "dev": true, - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash": "^4.17.11", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.13.2", - "node-gyp": "^3.8.0", - "npmlog": "^4.0.0", - "request": "^2.88.0", - "sass-graph": "^2.2.4", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" - }, - "dependencies": { - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "node-schedule": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.2.tgz", - "integrity": "sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw==", - "requires": { - "cron-parser": "^2.7.3", - "long-timeout": "0.1.1", - "sorted-array-functions": "^1.0.0" - } - }, - "nodemailer": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.2.1.tgz", - "integrity": "sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==" - }, - "nodemailer-express-handlebars": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/nodemailer-express-handlebars/-/nodemailer-express-handlebars-3.0.0.tgz", - "integrity": "sha1-atkTu6dOGG0pgxzpvuMP00uPmL4=", - "requires": { - "express-handlebars": "^3.0.0" - } - }, - "nodemon": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.1.tgz", - "integrity": "sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg==", - "dev": true, - "requires": { - "chokidar": "^2.1.5", - "debug": "^3.1.0", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.6", - "semver": "^5.5.0", - "supports-color": "^5.2.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-url": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.3.0.tgz", - "integrity": "sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ==" - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "requires": { - "boolbase": "~1.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", - "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" - }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - }, - "dependencies": { - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - } - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-uri": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-uri/-/parse-uri-1.0.0.tgz", - "integrity": "sha1-KHLcwi8aeXrN4Vg9igrClVLdrCA=" - }, - "parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "requires": { - "@types/node": "*" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "passport": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", - "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", - "requires": { - "passport-strategy": "1.x.x", - "pause": "0.0.1" - } - }, - "passport-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", - "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", - "requires": { - "passport-strategy": "1.x.x" - } - }, - "passport-strategy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "postcss": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.14.tgz", - "integrity": "sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==", - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "prebuild-install": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.0.tgz", - "integrity": "sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==", - "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", - "os-homedir": "^1.0.1", - "pump": "^2.0.1", - "rc": "^1.2.7", - "simple-get": "^2.7.0", - "tar-fs": "^1.13.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "simple-get": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", - "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", - "requires": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - } - } - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "promise": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.2.tgz", - "integrity": "sha512-EIyzM39FpVOMbqgzEHhxdrEhtOSDOtjMZQ0M6iVfCE+kWNgCkAyOdnuCWqfmflylftfadU6FkiMgHZA2kUzwRw==", - "requires": { - "asap": "~2.0.6" - } - }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", - "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==" - }, - "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==", - "dev": true - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "punycode2": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/punycode2/-/punycode2-1.0.0.tgz", - "integrity": "sha1-4rS5qaj/FX0LhEOOIDGB7niS39g=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "regexp-clone": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", - "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" - }, - "registry-auth-token": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", - "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", - "dev": true, - "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" - } - }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", - "dev": true, - "requires": { - "rc": "^1.0.1" - } - }, - "relative": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", - "integrity": "sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8=", - "requires": { - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "remarkable": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz", - "integrity": "sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==", - "requires": { - "argparse": "^1.0.10", - "autolinker": "~0.28.0" - }, - "dependencies": { - "autolinker": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz", - "integrity": "sha1-BlK0kYgYefB3XazgzcoyM5QqTkc=", - "requires": { - "gulp-header": "^1.7.1" - } - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.0.0" - } - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "^2.0.0", - "semver": "^5.1.0" - } - }, - "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sanitize-html": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.20.0.tgz", - "integrity": "sha512-BpxXkBoAG+uKCHjoXFmox6kCSYpnulABoGcZ/R3QyY9ndXbIM5S94eOr1IqnzTG8TnbmXaxWoDDzKC5eJv7fEQ==", - "requires": { - "chalk": "^2.4.1", - "htmlparser2": "^3.10.0", - "lodash.clonedeep": "^4.5.0", - "lodash.escaperegexp": "^4.1.2", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.mergewith": "^4.6.1", - "postcss": "^7.0.5", - "srcset": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "saslprep": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", - "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", - "optional": true, - "requires": { - "sparse-bitfield": "^3.0.3" - } - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", - "dev": true, - "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^7.0.0" - } - }, - "scss-tokenizer": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", - "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", - "dev": true, - "requires": { - "js-base64": "^2.1.8", - "source-map": "^0.4.2" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "self-closing-tags": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/self-closing-tags/-/self-closing-tags-1.0.1.tgz", - "integrity": "sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA==" - }, - "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "^5.0.3" - } - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-getter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", - "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", - "requires": { - "to-object-path": "^0.3.0" - } - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "sharp": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.23.0.tgz", - "integrity": "sha512-3+QlktTYDPO9CLmB3DUaBSj729ic3R9TO5Bz318F8WubUW10HR4os0Tm+GdYNcVg0layhMhP4Hf2SILwXVG2ig==", - "requires": { - "color": "^3.1.2", - "detect-libc": "^1.0.3", - "nan": "^2.14.0", - "npmlog": "^4.1.2", - "prebuild-install": "^5.3.0", - "semver": "^6.3.0", - "simple-get": "^3.0.3", - "tar": "^4.4.10", - "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shortid": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.14.tgz", - "integrity": "sha512-4UnZgr9gDdA1kaKj/38IiudfC3KHKhDc1zi/HSxd9FQDR0VLwH3/y79tZJLsVYPsJgIjeHjqIWaWVRJUj9qZOQ==", - "requires": { - "nanoid": "^2.0.0" - } - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" - }, - "simple-get": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.0.3.tgz", - "integrity": "sha512-Wvre/Jq5vgoz31Z9stYWPLn0PqRqmBDpFSdypAnHu5AvRVCYPRYGnvryNLiXu8GOBNDH82J2FRHUGMjjHUpXFw==", - "requires": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - } - }, - "sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" - }, - "smartquotes": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/smartquotes/-/smartquotes-2.3.1.tgz", - "integrity": "sha1-Aeu1ldbHqeJNkOjLlcF9Dhr0lAc=" - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "sorted-array-functions": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz", - "integrity": "sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", - "optional": true, - "requires": { - "memory-pager": "^1.0.2" - } - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "srcset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-1.0.0.tgz", - "integrity": "sha1-pWad4StC87HV6D7QPHEEb8SPQe8=", - "requires": { - "array-uniq": "^1.0.2", - "number-is-nan": "^1.0.0" - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "streamsearch": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", - "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "striptags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz", - "integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0=" - }, - "success-symbol": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/success-symbol/-/success-symbol-0.1.0.tgz", - "integrity": "sha1-JAIuSG878c3KCUKDt2nEctO3KJc=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tar": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", - "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.5", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", - "requires": { - "chownr": "^1.0.1", - "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" - }, - "dependencies": { - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "^0.7.0" - }, - "dependencies": { - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - } - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=" - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "title": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/title/-/title-3.4.1.tgz", - "integrity": "sha512-CoIakPr4Gi91bm4Xp6xTuZ3EENxPcKgOJzHzrhSdUatGtFMeVJa2qxI4WNhJHgz0Jak5Ck6jY2UNFM71/yBIsw==", - "requires": { - "arg": "1.0.0", - "chalk": "2.3.0", - "clipboardy": "1.2.2", - "titleize": "1.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "requires": { - "ansi-styles": "^3.1.0", - "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "requires": { - "has-flag": "^2.0.0" - } - } - } - }, - "titleize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-1.0.0.tgz", - "integrity": "sha1-fTUHIgYYMLpmF2MeDP0+oIOY2Vo=" - }, - "tlds": { - "version": "1.203.1", - "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.203.1.tgz", - "integrity": "sha512-7MUlYyGJ6rSitEZ3r1Q1QNV8uSIzapS8SmmhSusBuIc7uIxPPwsKllEP0GRp1NS6Ik6F+fRZvnjDWm3ecv2hDw==" - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "to-gfm-code-block": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz", - "integrity": "sha1-JdBFpfrlUxielje1kJANpzLYqoI=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - }, - "dependencies": { - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, - "requires": { - "abbrev": "1" - } - } - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "true-case-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", - "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", - "dev": true, - "requires": { - "glob": "^7.1.2" - } - }, - "truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/truncate/-/truncate-2.1.0.tgz", - "integrity": "sha512-em3E3SUDONOjTBcZ36DTm3RvDded3IRU9rX32oHwwXNt3rJD5MVaFlJTQvs8tJoHRoeYP36OuQ1eL/Q7bNEWIQ==" - }, - "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "typeof-article": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/typeof-article/-/typeof-article-0.1.1.tgz", - "integrity": "sha1-nwfnM8P7tkb/qeYcCN66zUYOBq8=", - "requires": { - "kind-of": "^3.1.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "uglify-js": { - "version": "3.5.15", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.15.tgz", - "integrity": "sha512-fe7aYFotptIddkwcm6YuA0HmknBZ52ZzOsUxZEdhhkSsz7RfjHDX2QDxwKTiv4JQ5t5NhfmpgAK+J7LiDhKSqg==", - "optional": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - } - }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "requires": { - "random-bytes": "~1.0.0" - } - }, - "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "requires": { - "prepend-http": "^2.0.0" - } - }, - "url-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-5.0.0.tgz", - "integrity": "sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g==", - "requires": { - "ip-regex": "^4.1.0", - "tlds": "^1.203.0" - } - }, - "urlsafe-base64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz", - "integrity": "sha1-I/iQaabGL0bPOh07ABac77kL4MY=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validator": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", - "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "video-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/video-extensions/-/video-extensions-1.1.0.tgz", - "integrity": "sha1-6qhrRfKahTwrhz6djiO1E3Epl9Y=" - }, - "warning-symbol": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/warning-symbol/-/warning-symbol-0.1.0.tgz", - "integrity": "sha1-uzHdEbeg+dZ6su2V9Fe2WCW7rSE=" - }, - "web-push": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.3.5.tgz", - "integrity": "sha512-sukVBk0chRCL4n+Xwurl2TlD4/JmezNv4L2nwlTZx4KwMeUPfmD9TdGl6A6taayNgjObYzp0k0gubAcQCp7TMg==", - "requires": { - "asn1.js": "^5.0.0", - "http_ece": "1.1.0", - "https-proxy-agent": "^2.2.1", - "jws": "^3.1.3", - "minimist": "^1.2.0", - "urlsafe-base64": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" - }, - "whoops": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/whoops/-/whoops-4.0.2.tgz", - "integrity": "sha512-b1ofth7xMOAkukgzMhAPKBrgieGJAgKVMyu54DXAOVLmkhpQEfNKe4wS0R7LbdxIsm6FD2CFUjBOdN7Sj+zLSg==", - "requires": { - "clean-stack": "~2.0.0", - "mimic-fn": "~2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.0.0.tgz", - "integrity": "sha512-jbex9Yd/3lmICXwYT6gA/j2mNQGU48wCh/VzRd+/Y/PjYQtlg1gLMdZqvu9s/xH7qKvngxRObl56XZR609IMbA==" - } - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "widest-line": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", - "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", - "dev": true, - "requires": { - "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, - "xss": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.6.tgz", - "integrity": "sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A==", - "requires": { - "commander": "^2.9.0", - "cssfilter": "0.0.10" - } - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - } - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "^3.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - } - } - }, - "year": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/year/-/year-0.2.1.tgz", - "integrity": "sha1-QIOuUgoxiyPshgN/MADLiSvfm7A=" - } - } -} diff --git a/package.json b/package.json index 89173abe..c764f594 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "watch-sass": "node-sass --source-map true --output-style compressed scss/index.scss public/css/style.css --watch", "generate-dark-mode": "node-sass --source-map true --output-style compressed scss/dark-mode.scss public/css/dark-mode.css --watch", "start": "nodemon server.js & npm run watch-sass", - "debug": "nodemon --inspect --inspect-brk=5757 server.js & node-sass --source-comments --output-style compressed scss/index.scss public/css/style.css --watch" + "debug": "nodemon --inspect --inspect-brk=5757 server.js & node-sass --source-comments --output-style compressed scss/index.scss public/css/style.css --watch", + "lint": "eslint app --fix" }, "author": "Raphael Kabo", "license": "GPL-3.0-only", @@ -51,6 +52,12 @@ "web-push": "^3.3.5" }, "devDependencies": { + "eslint": "^6.8.0", + "eslint-config-standard": "^14.1.0", + "eslint-plugin-import": "^2.20.0", + "eslint-plugin-node": "^11.0.0", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-standard": "^4.0.1", "node-sass": "^4.12.0", "nodemon": "^1.19.1" } diff --git a/server.js b/server.js index 451ed239..5311f196 100644 --- a/server.js +++ b/server.js @@ -1,93 +1,93 @@ // Initialization ====================================================================== -const express = require('express'); -const handlebars = require('express-handlebars'); -const app = express(); -const port = process.env.PORT || 8686; -const passport = require('passport'); -const flash = require('connect-flash'); -require('handlebars-helpers')(); +const express = require('express') +const handlebars = require('express-handlebars') +const app = express() +const port = process.env.PORT || 8686 +const passport = require('passport') +const flash = require('connect-flash') +require('handlebars-helpers')() -const compression = require('compression'); -app.use(compression()); +const compression = require('compression') +app.use(compression()) -const expressValidator = require('express-validator'); -app.use(expressValidator()); +const expressValidator = require('express-validator') +app.use(expressValidator()) -const morgan = require('morgan'); -const cookieParser = require('cookie-parser'); -const bodyParser = require('body-parser'); -const session = require('express-session'); +const morgan = require('morgan') +const cookieParser = require('cookie-parser') +const bodyParser = require('body-parser') +const session = require('express-session') -const fileUpload = require('express-fileupload'); -app.use(fileUpload()); +const fileUpload = require('express-fileupload') +app.use(fileUpload()) // Set up our Express application -app.use(cookieParser()); // read cookies (needed for auth) -app.use(bodyParser()); // get information from html forms -const mongoSanitize = require('express-mongo-sanitize'); //sanitize information recieved from html forms -app.use(mongoSanitize()); +app.use(cookieParser()) // read cookies (needed for auth) +app.use(bodyParser()) // get information from html forms +const mongoSanitize = require('express-mongo-sanitize') // sanitize information recieved from html forms +app.use(mongoSanitize()) // View engine (Handlebars) hbs = handlebars.create({ defaultLayout: 'main', - partialsDir:['views/partials/','views/partials/scriptPartials/'], + partialsDir: ['views/partials/', 'views/partials/scriptPartials/'], helpers: { plural: function (number, text) { - var singular = number === 1; + var singular = number === 1 // If no text parameter was given, just return a conditional s. - if (typeof text !== 'string') return singular ? '' : 's'; + if (typeof text !== 'string') return singular ? '' : 's' // Split with regex into group1/group2 or group1(group3) - var match = text.match(/^([^()\/]+)(?:\/(.+))?(?:\((\w+)\))?/); + var match = text.match(/^([^()\/]+)(?:\/(.+))?(?:\((\w+)\))?/) // If no match, just append a conditional s. - if (!match) return text + (singular ? '' : 's'); + if (!match) return text + (singular ? '' : 's') // We have a good match, so fire away return singular && match[1] // Singular case || match[2] // Plural case: 'bagel/bagels' --> bagels || - match[1] + (match[3] || 's'); // Plural case: 'bagel(s)' or 'bagel' --> bagels + match[1] + (match[3] || 's') // Plural case: 'bagel(s)' or 'bagel' --> bagels }, - buildComment(comment, depth) { - if (!depth) depth = 1; - var tree = []; + buildComment (comment, depth) { + if (!depth) depth = 1 + var tree = [] tree.push({ comment: comment, depth: depth }) comment.replies.forEach((r) => { depth = depth + 1 - tree.comment.replies.depth = depth; - }); - return tree; + tree.comment.replies.depth = depth + }) + return tree } } -}); -app.engine('handlebars', hbs.engine); -app.set('view engine', 'handlebars'); +}) +app.engine('handlebars', hbs.engine) +app.set('view engine', 'handlebars') // Static files -app.use(express.static('public')); +app.use(express.static('public')) // Database Configuration and Global Variable Creation=============================================================== -const configDatabase = require('./config/database.js'); -mongoose = require('mongoose'); -mongoose.connect(configDatabase.url, {useNewUrlParser: true}); // connect to our database -ObjectId = mongoose.Types.ObjectId; -DBReference = mongoose.Schema.Types.ObjectId; -User = require('./app/models/user'); -Relationship = require('./app/models/relationship'); -Post = require('./app/models/post'); -Tag = require('./app/models/tag'); -Community = require('./app/models/community'); -Vote = require('./app/models/vote'); -Image = require('./app/models/image'); - -//persist sessions across restarts via their storage in mongodb -const MongoStore = require('connect-mongo')(session); - -//set up passport authentication and session storage -require('./config/passport')(passport); // pass passport for configuration -var auth = require('./config/auth.js'); +const configDatabase = require('./config/database.js') +mongoose = require('mongoose') +mongoose.connect(configDatabase.url, { useNewUrlParser: true }) // connect to our database +ObjectId = mongoose.Types.ObjectId +DBReference = mongoose.Schema.Types.ObjectId +User = require('./app/models/user') +Relationship = require('./app/models/relationship') +Post = require('./app/models/post') +Tag = require('./app/models/tag') +Community = require('./app/models/community') +Vote = require('./app/models/vote') +Image = require('./app/models/image') + +// persist sessions across restarts via their storage in mongodb +const MongoStore = require('connect-mongo')(session) + +// set up passport authentication and session storage +require('./config/passport')(passport) // pass passport for configuration +var auth = require('./config/auth.js') app.use(session({ secret: auth.secret, cookie: { @@ -100,61 +100,61 @@ app.use(session({ mongooseConnection: mongoose.connection, secret: auth.secret }) -})); -app.use(passport.initialize()); -app.use(passport.session()); // persistent login sessions -app.use(flash()); // use connect-flash for flash messages stored in session +})) +app.use(passport.initialize()) +app.use(passport.session()) // persistent login sessions +app.use(flash()) // use connect-flash for flash messages stored in session app.use(function (req, res, next) { - res.locals.sessionFlash = req.session.sessionFlash; - delete req.session.sessionFlash; - next(); -}); + res.locals.sessionFlash = req.session.sessionFlash + delete req.session.sessionFlash + next() +}) -//set up webpush to send push notifications for the notifier -webpush = require('web-push'); +// set up webpush to send push notifications for the notifier +webpush = require('web-push') if (!auth.vapidPrivateKey || !auth.vapidPublicKey) { - vapidKeys = webpush.generateVAPIDKeys(); + vapidKeys = webpush.generateVAPIDKeys() webpush.setVapidDetails( 'mailto:support@sweet.sh', vapidKeys.publicKey, vapidKeys.privateKey - ); + ) } else { webpush.setVapidDetails( 'mailto:support@sweet.sh', auth.vapidPublicKey, auth.vapidPrivateKey - ); + ) } -//kill the process when the sigint code is recieved, generally generated by pressing ctrl-c in the console +// kill the process when the sigint code is recieved, generally generated by pressing ctrl-c in the console app.on('SIGINT', function () { db.stop(function (err) { - process.exit(err ? 1 : 0); - }); -}); + process.exit(err ? 1 : 0) + }) +}) // utilized by routes code ================================================================================= -path = require('path'); -global.appRoot = path.resolve(__dirname); -fs = require('fs'); -moment = require('moment'); +path = require('path') +global.appRoot = path.resolve(__dirname) +fs = require('fs') +moment = require('moment') moment.updateLocale('en', { relativeTime: { - future: "in %s", - past: "%s", - s: "just now", - ss: '%ds ago', - m: "1m ago", - mm: "%dm ago", - h: "1h ago", - hh: "%dh ago", - d: "1d ago", - dd: "%dd ago", - M: "1mon ago", - MM: "%dmon ago", - y: "1y ago", - yy: "%dy ago" + future: 'in %s', + past: '%s', + s: 'just now', + ss: '%ds ago', + m: '1m ago', + mm: '%dm ago', + h: '1h ago', + hh: '%dh ago', + d: '1d ago', + dd: '%dd ago', + M: '1mon ago', + MM: '%dmon ago', + y: '1y ago', + yy: '%dy ago' }, calendar: { sameDay: '[today at] h:mm a [UTC]Z', @@ -162,50 +162,50 @@ moment.updateLocale('en', { lastWeek: '[last] dddd [at] h:mm a [UTC]Z', sameElse: 'MMMM Do YYYY, [at] h:mm a [UTC]Z' } -}); -var momentLogFormat = '[[]DD/MM HH:mm:ss.SSS[]]'; -sanitizeHtml = require('sanitize-html'); -sharp = require('sharp'); -shortid = require('shortid'); -bcrypt = require('bcrypt-nodejs'); -Autolinker = require('autolinker'); -schedule = require('node-schedule'); -globals = require('./config/globals'); - -const writeMorganToSeparateFile = true; //change to write full request log to stdout instead of a separate file - -if(writeMorganToSeparateFile){ - var morganOutput = fs.openSync("full request log starting "+moment().format('DD-MM HH[h] MM[m] ss[s]')+".txt",'w'); //colons in the file path not supported by windows :( +}) +var momentLogFormat = '[[]DD/MM HH:mm:ss.SSS[]]' +sanitizeHtml = require('sanitize-html') +sharp = require('sharp') +shortid = require('shortid') +bcrypt = require('bcrypt-nodejs') +Autolinker = require('autolinker') +schedule = require('node-schedule') +globals = require('./config/globals') + +const writeMorganToSeparateFile = true // change to write full request log to stdout instead of a separate file + +if (writeMorganToSeparateFile) { + var morganOutput = fs.openSync('full request log starting ' + moment().format('DD-MM HH[h] MM[m] ss[s]') + '.txt', 'w') // colons in the file path not supported by windows :( var stream = { - write: function(input,encoding){ - fs.writeSync(morganOutput,input,undefined,encoding); + write: function (input, encoding) { + fs.writeSync(morganOutput, input, undefined, encoding) } } -}else{ - var stream = process.stdout; +} else { + var stream = process.stdout } -//log every request to the console w/ timestamp before it can crash the server -app.use(morgan(function(tokens,req,res){return moment().format(momentLogFormat)+" "+req.method.toLowerCase()+" request for "+req.url},{immediate:true,stream:stream})); -//add timestamps to all console logging functions -for(var spicy of ["warn","error","log"]){ - var vanilla = console[spicy]; - console[spicy] = vanilla.bind(console, moment().format(momentLogFormat)); +// log every request to the console w/ timestamp before it can crash the server +app.use(morgan(function (tokens, req, res) { return moment().format(momentLogFormat) + ' ' + req.method.toLowerCase() + ' request for ' + req.url }, { immediate: true, stream: stream })) +// add timestamps to all console logging functions +for (var spicy of ['warn', 'error', 'log']) { + var vanilla = console[spicy] + console[spicy] = vanilla.bind(console, moment().format(momentLogFormat)) } // routes ====================================================================== -helper = require('./app/utilityFunctionsMostlyText.js'); -require('./app/statisticsTracker.js')(app, mongoose); -notifier = require('./app/notifier.js'); -emailer = require('./app/emailer.js'); -require('./app/personalAccountActions.js')(app, passport); -require('./app/inhabitingCommunities.js')(app, passport); -require('./app/viewingSweet.js')(app); -require('./app/postingToSweet.js')(app); +helper = require('./app/utilityFunctionsMostlyText.js') +require('./app/statisticsTracker.js')(app, mongoose) +notifier = require('./app/notifier.js') +emailer = require('./app/emailer.js') +require('./app/personalAccountActions.js')(app, passport) +require('./app/inhabitingCommunities.js')(app, passport) +require('./app/viewingSweet.js')(app) +require('./app/postingToSweet.js')(app) // launch ====================================================================== -app.listen(port); +app.listen(port) -/*var https = require('https'); +/* var https = require('https'); var httpsOptions = { key: fs.readFileSync('../192.168.1.15-key.pem'), cert: fs.readFileSync('../192.168.1.15.pem') @@ -213,6 +213,6 @@ var httpsOptions = { https.createServer(httpsOptions, app) .listen(3000, function () { console.log('app listening on port 3000! Go to https://localhost:3000/') -})*/ +}) */ -console.log('Server booting on default port: ' + port); +console.log('Server booting on default port: ' + port) diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..c9e9ae63 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,5444 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@metascraper/helpers@^5.10.3": + version "5.10.3" + resolved "https://registry.yarnpkg.com/@metascraper/helpers/-/helpers-5.10.3.tgz#38abd95ffbcfaf0bd6afbaa85853d4b9aa780c87" + integrity sha512-ZiDS3aGIK3OSGBMaYORvUVZAVFKmjh1KZaaFnmjIPi6vWPFOrtLK8r4OrkwroJ0IK6Hg6ZiuDjQo63LDcmLqdA== + dependencies: + audio-extensions "0.0.0" + chrono-node "~1.4.2" + condense-whitespace "~2.0.0" + entities "~2.0.0" + file-extension "~4.0.5" + has-values "~2.0.1" + image-extensions "~1.1.0" + is-relative-url "~3.0.0" + is-uri "~1.2.0" + iso-639-3 "~1.2.0" + isostring "0.0.1" + lodash "~4.17.15" + memoize-one "~5.1.1" + mime-types "~2.1.26" + normalize-url "~4.5.0" + smartquotes "~2.3.1" + title "~3.4.1" + truncate "~2.1.0" + url-regex "~5.0.0" + video-extensions "~1.1.0" + +"@sendgrid/client@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-6.5.0.tgz#d3c254738623fe43be8627e262ba698c5f3b9be7" + integrity sha512-gbA5GkZI5FWBJXgrtVPLHE2YV16WkjO0P2/jHt+m0CBQnfNV9FjGSXY/0r6ob+Ka9sw6Xj/N7gKtGLtrINCiBg== + dependencies: + "@sendgrid/helpers" "^6.5.0" + request "^2.88.0" + +"@sendgrid/helpers@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@sendgrid/helpers/-/helpers-6.5.0.tgz#2143558011f31750fef075967a43052133d2d4ea" + integrity sha512-W5rgGDA6pz+XA5TTThNy/l0iV35zTFup+Y/u5hCGoFNZZy9oZLsSg0Ooumw/Kdc/wEVHuUyQesEnRyNrX+qyoQ== + dependencies: + chalk "^2.0.1" + deepmerge "^2.1.1" + +"@sendgrid/mail@^6.3.1": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@sendgrid/mail/-/mail-6.5.0.tgz#6630b9ed775725dec3b598d9719b4ddd6428c56d" + integrity sha512-KH8fh08gAWrEdY0XwOfRpjgf0vCj1vpzEfA2p0QRgMKbPyni6uZQm4oxuJasPdZVNmy2W16F33uG1mDRWLCJKw== + dependencies: + "@sendgrid/client" "^6.5.0" + "@sendgrid/helpers" "^6.5.0" + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@types/node@*": + version "13.1.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b" + integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-jsx@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" + integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== + +acorn@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +ajv@^6.10.0, ajv@^6.10.2: + version "6.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^6.5.5: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= + +ansi-align@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" + integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= + dependencies: + string-width "^2.0.0" + +ansi-bgblack@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bgblack/-/ansi-bgblack-0.1.1.tgz#a68ba5007887701b6aafbe3fa0dadfdfa8ee3ca2" + integrity sha1-poulAHiHcBtqr74/oNrf36juPKI= + dependencies: + ansi-wrap "0.1.0" + +ansi-bgblue@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bgblue/-/ansi-bgblue-0.1.1.tgz#67bdc04edc9b9b5278969da196dea3d75c8c3613" + integrity sha1-Z73ATtybm1J4lp2hlt6j11yMNhM= + dependencies: + ansi-wrap "0.1.0" + +ansi-bgcyan@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bgcyan/-/ansi-bgcyan-0.1.1.tgz#58489425600bde9f5507068dd969ebfdb50fe768" + integrity sha1-WEiUJWAL3p9VBwaN2Wnr/bUP52g= + dependencies: + ansi-wrap "0.1.0" + +ansi-bggreen@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bggreen/-/ansi-bggreen-0.1.1.tgz#4e3191248529943f4321e96bf131d1c13816af49" + integrity sha1-TjGRJIUplD9DIelr8THRwTgWr0k= + dependencies: + ansi-wrap "0.1.0" + +ansi-bgmagenta@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bgmagenta/-/ansi-bgmagenta-0.1.1.tgz#9b28432c076eaa999418672a3efbe19391c2c7a1" + integrity sha1-myhDLAduqpmUGGcqPvvhk5HCx6E= + dependencies: + ansi-wrap "0.1.0" + +ansi-bgred@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bgred/-/ansi-bgred-0.1.1.tgz#a76f92838382ba43290a6c1778424f984d6f1041" + integrity sha1-p2+Sg4OCukMpCmwXeEJPmE1vEEE= + dependencies: + ansi-wrap "0.1.0" + +ansi-bgwhite@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bgwhite/-/ansi-bgwhite-0.1.1.tgz#6504651377a58a6ececd0331994e480258e11ba8" + integrity sha1-ZQRlE3elim7OzQMxmU5IAljhG6g= + dependencies: + ansi-wrap "0.1.0" + +ansi-bgyellow@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bgyellow/-/ansi-bgyellow-0.1.1.tgz#c3fe2eb08cd476648029e6874d15a0b38f61d44f" + integrity sha1-w/4usIzUdmSAKeaHTRWgs49h1E8= + dependencies: + ansi-wrap "0.1.0" + +ansi-black@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-black/-/ansi-black-0.1.1.tgz#f6185e889360b2545a1ec50c0bf063fc43032453" + integrity sha1-9hheiJNgslRaHsUMC/Bj/EMDJFM= + dependencies: + ansi-wrap "0.1.0" + +ansi-blue@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-blue/-/ansi-blue-0.1.1.tgz#15b804990e92fc9ca8c5476ce8f699777c21edbf" + integrity sha1-FbgEmQ6S/JyoxUds6PaZd3wh7b8= + dependencies: + ansi-wrap "0.1.0" + +ansi-bold@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-bold/-/ansi-bold-0.1.1.tgz#3e63950af5acc2ae2e670e6f67deb115d1a5f505" + integrity sha1-PmOVCvWswq4uZw5vZ96xFdGl9QU= + dependencies: + ansi-wrap "0.1.0" + +ansi-colors@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-0.2.0.tgz#72c31de2a0d9a2ccd0cac30cc9823eeb2f6434b5" + integrity sha1-csMd4qDZoszQysMMyYI+6y9kNLU= + dependencies: + ansi-bgblack "^0.1.1" + ansi-bgblue "^0.1.1" + ansi-bgcyan "^0.1.1" + ansi-bggreen "^0.1.1" + ansi-bgmagenta "^0.1.1" + ansi-bgred "^0.1.1" + ansi-bgwhite "^0.1.1" + ansi-bgyellow "^0.1.1" + ansi-black "^0.1.1" + ansi-blue "^0.1.1" + ansi-bold "^0.1.1" + ansi-cyan "^0.1.1" + ansi-dim "^0.1.1" + ansi-gray "^0.1.1" + ansi-green "^0.1.1" + ansi-grey "^0.1.1" + ansi-hidden "^0.1.1" + ansi-inverse "^0.1.1" + ansi-italic "^0.1.1" + ansi-magenta "^0.1.1" + ansi-red "^0.1.1" + ansi-reset "^0.1.1" + ansi-strikethrough "^0.1.1" + ansi-underline "^0.1.1" + ansi-white "^0.1.1" + ansi-yellow "^0.1.1" + lazy-cache "^2.0.1" + +ansi-cyan@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" + integrity sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= + dependencies: + ansi-wrap "0.1.0" + +ansi-dim@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-dim/-/ansi-dim-0.1.1.tgz#40de4c603aa8086d8e7a86b8ff998d5c36eefd6c" + integrity sha1-QN5MYDqoCG2Oeoa4/5mNXDbu/Ww= + dependencies: + ansi-wrap "0.1.0" + +ansi-escapes@^4.2.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d" + integrity sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg== + dependencies: + type-fest "^0.8.1" + +ansi-gray@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" + integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= + dependencies: + ansi-wrap "0.1.0" + +ansi-green@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-green/-/ansi-green-0.1.1.tgz#8a5d9a979e458d57c40e33580b37390b8e10d0f7" + integrity sha1-il2al55FjVfEDjNYCzc5C44Q0Pc= + dependencies: + ansi-wrap "0.1.0" + +ansi-grey@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-grey/-/ansi-grey-0.1.1.tgz#59d98b6ac2ba19f8a51798e9853fba78339a33c1" + integrity sha1-WdmLasK6GfilF5jphT+6eDOaM8E= + dependencies: + ansi-wrap "0.1.0" + +ansi-hidden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-hidden/-/ansi-hidden-0.1.1.tgz#ed6a4c498d2bb7cbb289dbf2a8d1dcc8567fae0f" + integrity sha1-7WpMSY0rt8uyidvyqNHcyFZ/rg8= + dependencies: + ansi-wrap "0.1.0" + +ansi-inverse@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-inverse/-/ansi-inverse-0.1.1.tgz#b6af45826fe826bfb528a6c79885794355ccd269" + integrity sha1-tq9Fgm/oJr+1KKbHmIV5Q1XM0mk= + dependencies: + ansi-wrap "0.1.0" + +ansi-italic@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-italic/-/ansi-italic-0.1.1.tgz#104743463f625c142a036739cf85eda688986f23" + integrity sha1-EEdDRj9iXBQqA2c5z4XtpoiYbyM= + dependencies: + ansi-wrap "0.1.0" + +ansi-magenta@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-magenta/-/ansi-magenta-0.1.1.tgz#063b5ba16fb3f23e1cfda2b07c0a89de11e430ae" + integrity sha1-BjtboW+z8j4c/aKwfAqJ3hHkMK4= + dependencies: + ansi-wrap "0.1.0" + +ansi-red@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" + integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= + dependencies: + ansi-wrap "0.1.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-reset@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-reset/-/ansi-reset-0.1.1.tgz#e7e71292c3c7ddcd4d62ef4a6c7c05980911c3b7" + integrity sha1-5+cSksPH3c1NYu9KbHwFmAkRw7c= + dependencies: + ansi-wrap "0.1.0" + +ansi-strikethrough@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-strikethrough/-/ansi-strikethrough-0.1.1.tgz#d84877140b2cff07d1c93ebce69904f68885e568" + integrity sha1-2Eh3FAss/wfRyT685pkE9oiF5Wg= + dependencies: + ansi-wrap "0.1.0" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.1.0, ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-underline@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-underline/-/ansi-underline-0.1.1.tgz#dfc920f4c97b5977ea162df8ffb988308aaa71a4" + integrity sha1-38kg9Ml7WXfqFi34/7mIMIqqcaQ= + dependencies: + ansi-wrap "0.1.0" + +ansi-white@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-white/-/ansi-white-0.1.1.tgz#9c77b7c193c5ee992e6011d36ec4c921b4578944" + integrity sha1-nHe3wZPF7pkuYBHTbsTJIbRXiUQ= + dependencies: + ansi-wrap "0.1.0" + +ansi-wrap@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + +ansi-yellow@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-yellow/-/ansi-yellow-0.1.1.tgz#cb9356f2f46c732f0e3199e6102955a77da83c1d" + integrity sha1-y5NW8vRscy8OMZnmEClVp32oPB0= + dependencies: + ansi-wrap "0.1.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +arch@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" + integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arg@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/arg/-/arg-1.0.0.tgz#444d885a4e25b121640b55155ef7cd03975d6050" + integrity sha512-Wk7TEzl1KqvTGs/uyhmHO/3XLd3t1UeU4IstvPXVzGPM522cTjqjNZ99esCkcL52sjqjo8e8CTBcWhkxvGzoAw== + +argparse@^1.0.10, argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-sort@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-0.1.4.tgz#662855eaeb671b4188df4451b2f24a0753992b23" + integrity sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ== + dependencies: + default-compare "^1.0.0" + get-value "^2.0.6" + kind-of "^5.0.2" + +array-uniq@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1.js@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.3.0.tgz#439099fe9174e09cff5a54a9dda70260517e8689" + integrity sha512-WHnQJFcOrIWT1RLOkFFBQkFVvyt9BPOOrH+Dp152Zk4R993rSzXUGPmkybIcUFhHE2d/iHH+nCaOWVCDbO8fgA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +audio-extensions@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/audio-extensions/-/audio-extensions-0.0.0.tgz#d0eefe077fb9eb625898eed9985890548cf1f8d2" + integrity sha1-0O7+B3+562JYmO7ZmFiQVIzx+NI= + +autolinker@^3.0.5: + version "3.11.1" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-3.11.1.tgz#aa4f910371df091b0d714d8d6e700d53f357ce95" + integrity sha512-6sAmetStorjXvwmV8MBxI5DGICHKD1B5EjdkIrq34X6YBDN6jj54EUHnoHgNqmNCclcf8c409zuVMNy449u80g== + dependencies: + tslib "^1.9.3" + +autolinker@~0.28.0: + version "0.28.1" + resolved "https://registry.yarnpkg.com/autolinker/-/autolinker-0.28.1.tgz#0652b491881879f0775dace0cdca3233942a4e47" + integrity sha1-BlK0kYgYefB3XazgzcoyM5QqTkc= + dependencies: + gulp-header "^1.7.1" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +basic-auth@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +bcrypt-nodejs@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/bcrypt-nodejs/-/bcrypt-nodejs-0.0.3.tgz#c60917f26dc235661566c681061c303c2b28842b" + integrity sha1-xgkX8m3CNWYVZsaBBhwwPCsohCs= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bl@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" + integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + dependencies: + readable-stream "^3.0.1" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= + dependencies: + inherits "~2.0.0" + +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + +bn.js@^4.0.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + +body-parser@1.19.0, body-parser@^1.18.3: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +boxen@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" + integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== + dependencies: + ansi-align "^2.0.0" + camelcase "^4.0.0" + chalk "^2.0.1" + cli-boxes "^1.0.0" + string-width "^2.0.0" + term-size "^1.2.0" + widest-line "^2.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +bson@^1.1.1, bson@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" + integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== + +bson@~1.0.4: + version "1.0.9" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.0.9.tgz#12319f8323b1254739b7c6bef8d3e89ae05a2f57" + integrity sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg== + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-shims@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" + integrity sha1-mXjOMXOIxkmth5MCjDR37wRKi1E= + +busboy@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" + integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== + dependencies: + dicer "0.3.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= + +camelcase@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + +capture-stack-trace@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" + integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + integrity sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q== + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + +chalk@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +cheerio-advanced-selectors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/cheerio-advanced-selectors/-/cheerio-advanced-selectors-2.0.1.tgz#fb5ec70a4599e8cec1cf669c6d9b90a3fa969c48" + integrity sha512-5wHR8bpiD5pdUtaS81A6hnJezzoDzL1TLWfK6bxnLkIgEKPV26BlOdMCcvuj3fTE7JSalsTUeNU7AOD/u6bYhw== + +cheerio@~1.0.0-rc.3: + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" + integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.1" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1, chownr@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" + integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== + +chrono-node@~1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/chrono-node/-/chrono-node-1.4.2.tgz#0c7fc1f264e60a660c2b2dab753a3f285dbfd8c9" + integrity sha512-fsb82wPDHVZl3xtche8k4ZZtNwf81/ZMueil2ANpSfogUAEa3BuzZAar7ObLXi1ptMjBzdzA6ys/bFq1oBjO8w== + dependencies: + dayjs "^1.8.19" + +ci-info@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" + integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +clean-stack@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-boxes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" + integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +clipboardy@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-1.2.2.tgz#2ce320b9ed9be1514f79878b53ff9765420903e2" + integrity sha512-16KrBOV7bHmHdxcQiCvfUFYVFyEah4FI8vYT1Fr7CGSA4G+xBWMEfUEQJS1hxeHGtI9ju1Bzs9uXSbj5HZKArw== + dependencies: + arch "^2.1.0" + execa "^0.8.0" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" + integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-with-sourcemaps@*: + version "1.1.0" + resolved "https://registry.yarnpkg.com/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz#d4ea93f05ae25790951b99e7b3b09e3908a4082e" + integrity sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg== + dependencies: + source-map "^0.6.1" + +condense-whitespace@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/condense-whitespace/-/condense-whitespace-2.0.0.tgz#94e9644938f66aa7be4b8849f8f0b3cec97d6b3a" + integrity sha512-Ath9o58/0rxZXbyoy3zZgrVMoIemi30sukG/btuMKCLyqfQt3dNOWc9N3EHEMa2Q3i0tXQPDJluYFLwy7pJuQw== + +configstore@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" + integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw== + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +connect-flash@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/connect-flash/-/connect-flash-0.1.1.tgz#d8630f26d95a7f851f9956b1e8cc6732f3b6aa30" + integrity sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA= + +connect-mongo@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/connect-mongo/-/connect-mongo-2.0.3.tgz#db6cabe1e3add5acc9ef9c486681027675a58ee5" + integrity sha512-Vs+QZ/6X6gbCrP1Ls7Oh/wlyY6pgpbPSrUKF5yRT+zd+4GZPNbjNquxquZ+Clv2+03HBXE7T4lVM0PUcaBhihg== + dependencies: + mongodb "^2.0.36" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-parser@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.4.tgz#e6363de4ea98c3def9697b93421c09f30cf5d188" + integrity sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw== + dependencies: + cookie "0.3.1" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +create-error-class@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= + dependencies: + capture-stack-trace "^1.0.0" + +create-frame@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/create-frame/-/create-frame-1.0.0.tgz#8b95f2691e3249b6080443e33d0bad9f8f6975aa" + integrity sha1-i5XyaR4ySbYIBEPjPQutn49pdao= + dependencies: + define-property "^0.2.5" + extend-shallow "^2.0.1" + isobject "^3.0.0" + lazy-cache "^2.0.2" + +cron-parser@^2.7.3: + version "2.13.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.13.0.tgz#6f930bb6f2931790d2a9eec83b3ec276e27a6725" + integrity sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ== + dependencies: + is-nan "^1.2.1" + moment-timezone "^0.5.25" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +date.js@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/date.js/-/date.js-0.3.3.tgz#ef1e92332f507a638795dbb985e951882e50bbda" + integrity sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw== + dependencies: + debug "~3.1.0" + +dayjs@^1.8.19: + version "1.8.19" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.19.tgz#5117dc390d8f8e586d53891dbff3fa308f51abfe" + integrity sha512-7kqOoj3oQSmqbvtvGFLU5iYqies+SqUiEGNT0UtUPPxcPYgY1BrkXR0Cq2R9HYSimBXN+xHkEN4Hi399W+Ovlg== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +deepmerge@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" + integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== + +default-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" + integrity sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ== + dependencies: + kind-of "^5.0.2" + +defer-to-connect@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.1.tgz#88ae694b93f67b81815a2c8c769aef6574ac8f2f" + integrity sha512-J7thop4u3mRTkYRQ+Vpfwy2G5Ehoy82I14+14W4YMDLKdWloI9gSzRbV30s/NckQGVJtPkWNcW4oMAUigTdqiQ== + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +dicer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" + integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== + dependencies: + streamsearch "0.1.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== + dependencies: + is-obj "^1.0.0" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= + +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0, entities@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" + integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +error-symbol@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/error-symbol/-/error-symbol-0.1.0.tgz#0a4dae37d600d15a29ba453d8ef920f1844333f6" + integrity sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y= + +es6-promise@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.2.1.tgz#ec56233868032909207170c39448e24449dd1fc4" + integrity sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q= + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +eslint@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.3" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.2.tgz#6c272650932b4f91c3714e5e7b5f5e2ecf47262d" + integrity sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA== + dependencies: + acorn "^7.1.0" + acorn-jsx "^5.1.0" + eslint-visitor-keys "^1.1.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" + integrity sha1-2NdrvBtVIX7RkP1t1J08d07PyNo= + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +express-fileupload@^1.1.3-alpha.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.1.6.tgz#0ac2659ad8c1128c92c8580fd6e15b8b15343db0" + integrity sha512-w24zPWT8DkoIxSVkbxYPo9hkTiLpCQQzNsLRTCnecBhfbYv+IkIC5uLw2MIUAxBZ+7UMmXPjGxlhzUXo4RcbZw== + dependencies: + busboy "^0.3.1" + +express-handlebars@^3.0.0, express-handlebars@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/express-handlebars/-/express-handlebars-3.1.0.tgz#c177ee9a81f6a2abada6b550b77b3e30c6bc0796" + integrity sha512-7QlaXnSREMmN5P2o4gmpUZDfJlLtfBka9d6r7/ccXaU7rPp76odw9YYtwZYdIiha2JqwiaG6o2Wu6NZJQ0u7Fg== + dependencies: + glob "^7.1.3" + graceful-fs "^4.1.2" + handlebars "^4.1.2" + object.assign "^4.1.0" + promise "^8.0.2" + +express-messages@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/express-messages/-/express-messages-1.0.1.tgz#9981a85a5d2b118c79fc33f52b41834ffee9685a" + integrity sha1-mYGoWl0rEYx5/DP1K0GDT/7paFo= + +express-mongo-sanitize@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/express-mongo-sanitize/-/express-mongo-sanitize-1.3.2.tgz#fba404f6c041577cbeeec4dd9057cefbb439de5a" + integrity sha1-+6QE9sBBV3y+7sTdkFfO+7Q53lo= + +express-session@^1.15.6: + version "1.17.0" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.0.tgz#9b50dbb5e8a03c3537368138f072736150b7f9b3" + integrity sha512-t4oX2z7uoSqATbMfsxWMbNjAL0T5zpvcJCk3Z9wnPPN7ibddhnmDZXHfEcoBMG2ojKXZoCyPMc5FbtK+G7SoDg== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.0" + uid-safe "~2.1.5" + +express-validator@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-5.3.1.tgz#6f42c6d52554441b0360c40ccfb555b1770affe2" + integrity sha512-g8xkipBF6VxHbO1+ksC7nxUU7+pWif0+OZXjZTybKJ/V0aTVhuCoHbyhIPgSYVldwQLocGExPtB2pE0DqK4jsw== + dependencies: + lodash "^4.17.10" + validator "^10.4.0" + +express@^4.16.4: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +"falsey@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/falsey/-/falsey-0.3.2.tgz#b21c90c5c34660fc192bf909575db95b6880d597" + integrity sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg== + dependencies: + kind-of "^5.0.2" + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +figures@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.1.0.tgz#4b198dd07d8d71530642864af2d45dd9e459c4ec" + integrity sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +file-extension@~4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/file-extension/-/file-extension-4.0.5.tgz#ae6cef34c28e7313a92baa4aa955755cacdf0ce3" + integrity sha512-l0rOL3aKkoi6ea7MNZe6OHgqYYpn48Qfflr8Pe9G9JPPTx5A+sfboK91ZufzIs59/lPqh351l0eb6iKU9J5oGg== + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" + integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + integrity sha1-mC1ok6+RjnLQjeyehnP/K1qNat0= + +fs-minipass@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.0.0.tgz#a6415edab02fae4b9e9230bc87ee2e4472003cd1" + integrity sha512-40Qz+LFXmd9tzYVnnBmZvFfvAADfUA14TXPK1s7IfElJTIZ97rA8w4Kin7Wt5JBrC3ShnnFJO/5vPjPEeJIq9A== + dependencies: + minipass "^3.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.11" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3" + integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +fstream@^1.0.0, fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== + dependencies: + globule "^1.0.0" + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-object@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/get-object/-/get-object-0.2.0.tgz#d92ff7d5190c64530cda0543dac63a3d47fe8c0c" + integrity sha1-2S/31RkMZFMM2gVD2sY6PUf+jAw= + dependencies: + is-number "^2.0.2" + isobject "^0.2.0" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" + integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== + dependencies: + is-glob "^4.0.1" + +glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= + dependencies: + ini "^1.3.4" + +globals@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.3.0.tgz#1e564ee5c4dded2ab098b0f88f24702a3c56be13" + integrity sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw== + dependencies: + type-fest "^0.8.1" + +globule@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.0.tgz#41d0e9fb44afd4b80d93a23263714f90b3dec904" + integrity sha512-YlD4kdMqRCQHrhVdonet4TdRtv1/sZKepvoxNT4Nrhrp5HI8XFfc8kFlGlBn2myBo80aGp8Eft259mbcUJhgSg== + dependencies: + glob "~7.1.1" + lodash "~4.17.10" + minimatch "~3.0.2" + +got@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" + integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= + dependencies: + create-error-class "^3.0.0" + duplexer3 "^0.1.4" + get-stream "^3.0.0" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + safe-buffer "^5.0.1" + timed-out "^4.0.0" + unzip-response "^2.0.1" + url-parse-lax "^1.0.0" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +gulp-header@^1.7.1: + version "1.8.12" + resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" + integrity sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ== + dependencies: + concat-with-sourcemaps "*" + lodash.template "^4.4.0" + through2 "^2.0.0" + +handlebars-helper-create-frame@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/handlebars-helper-create-frame/-/handlebars-helper-create-frame-0.1.0.tgz#8aa51d10aeb6408fcc6605d40d77356288487a03" + integrity sha1-iqUdEK62QI/MZgXUDXc1YohIegM= + dependencies: + create-frame "^1.0.0" + isobject "^3.0.0" + +handlebars-helpers@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/handlebars-helpers/-/handlebars-helpers-0.10.0.tgz#663d49e718928eafbead1473419ed7bc24bcd45a" + integrity sha512-QiyhQz58u/DbuV41VnfpE0nhy6YCH4vB514ajysV8SoKmP+DxU+pR+fahVyNECHj+jiwEN2VrvxD/34/yHaLUg== + dependencies: + arr-flatten "^1.1.0" + array-sort "^0.1.4" + create-frame "^1.0.0" + define-property "^1.0.0" + "falsey" "^0.3.2" + for-in "^1.0.2" + for-own "^1.0.0" + get-object "^0.2.0" + get-value "^2.0.6" + handlebars "^4.0.11" + handlebars-helper-create-frame "^0.1.0" + handlebars-utils "^1.0.6" + has-value "^1.0.0" + helper-date "^1.0.1" + helper-markdown "^1.0.0" + helper-md "^0.2.2" + html-tag "^2.0.0" + is-even "^1.0.0" + is-glob "^4.0.0" + is-number "^4.0.0" + kind-of "^6.0.0" + lazy-cache "^2.0.2" + logging-helpers "^1.0.0" + micromatch "^3.1.4" + relative "^3.0.2" + striptags "^3.1.0" + to-gfm-code-block "^0.1.1" + year "^0.2.1" + +handlebars-utils@^1.0.2, handlebars-utils@^1.0.4, handlebars-utils@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/handlebars-utils/-/handlebars-utils-1.0.6.tgz#cb9db43362479054782d86ffe10f47abc76357f9" + integrity sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw== + dependencies: + kind-of "^6.0.0" + typeof-article "^0.1.1" + +handlebars@^4.0.11, handlebars@^4.1.2, handlebars@^4.5.3: + version "4.7.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.2.tgz#01127b3840156a0927058779482031afe0e730d7" + integrity sha512-4PwqDL2laXtTWZghzzCtunQUTLbo31pcCJrd/B/9JP8XbhVzpS5ZXuKqlOzsd1rtcaLo4KqAn8nl8mkknS4MHw== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has-values@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-2.0.1.tgz#3876200ff86d8a8546a9264a952c17d5fc17579d" + integrity sha512-+QdH3jOmq9P8GfdjFg0eJudqx1FqU62NQJ4P16rOEHeRdl7ckgwn6uqQjzYE0ZoHVV/e5E2esuJ5Gl5+HUW19w== + dependencies: + kind-of "^6.0.2" + +helper-date@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/helper-date/-/helper-date-1.0.1.tgz#12fedea3ad8e44a7ca4c4efb0ff4104a5120cffb" + integrity sha512-wU3VOwwTJvGr/w5rZr3cprPHO+hIhlblTJHD6aFBrKLuNbf4lAmkawd2iK3c6NbJEvY7HAmDpqjOFSI5/+Ey2w== + dependencies: + date.js "^0.3.1" + handlebars-utils "^1.0.4" + moment "^2.18.1" + +helper-markdown@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/helper-markdown/-/helper-markdown-1.0.0.tgz#ee7e9fc554675007d37eb90f7853b13ce74f3e10" + integrity sha512-AnDqMS4ejkQK0MXze7pA9TM3pu01ZY+XXsES6gEE0RmCGk5/NIfvTn0NmItfyDOjRAzyo9z6X7YHbHX4PzIvOA== + dependencies: + handlebars-utils "^1.0.2" + highlight.js "^9.12.0" + remarkable "^1.7.1" + +helper-md@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/helper-md/-/helper-md-0.2.2.tgz#c1f59d7e55bbae23362fd8a0e971607aec69d41f" + integrity sha1-wfWdflW7riM2L9ig6XFgeuxp1B8= + dependencies: + ent "^2.2.0" + extend-shallow "^2.0.1" + fs-exists-sync "^0.1.0" + remarkable "^1.6.2" + +highlight.js@^9.12.0: + version "9.17.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.17.1.tgz#14a4eded23fd314b05886758bb906e39dd627f9a" + integrity sha512-TA2/doAur5Ol8+iM3Ov7qy3jYcr/QiJ2eDTdRF4dfbjG7AaaB99J5G+zSl11ljbl6cIcahgPY6SKb3sC3EJ0fw== + dependencies: + handlebars "^4.5.3" + +hosted-git-info@^2.1.4: + version "2.8.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" + integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== + +html-tag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tag/-/html-tag-2.0.0.tgz#36c3bc8d816fd30b570d5764a497a641640c2fed" + integrity sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g== + dependencies: + is-self-closing "^1.0.1" + kind-of "^6.0.0" + +htmlparser2@^3.10.0, htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + +http-cache-semantics@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#495704773277eeef6e43f9ab2c2c7d259dda25c5" + integrity sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http_ece@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75" + integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA== + dependencies: + urlsafe-base64 "~1.0.0" + +https-proxy-agent@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" + integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +image-extensions@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/image-extensions/-/image-extensions-1.1.0.tgz#b8e6bf6039df0056e333502a00b6637a3105d894" + integrity sha1-uOa/YDnfAFbjM1AqALZjejEF2JQ= + +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +info-symbol@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/info-symbol/-/info-symbol-0.1.0.tgz#27841d72867ddb4242cd612d79c10633881c6a78" + integrity sha1-J4QdcoZ920JCzWEtecEGM4gcang= + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.4, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +inquirer@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.3.tgz#f9b4cd2dff58b9f73e8d43759436ace15bed4567" + integrity sha512-+OiOVeVydu4hnCGLCSX+wedovR/Yzskv9BFqUNNKq9uU2qg7LCcCo3R86S2E7WLo0y/x2pnEZfZe1CoYnORUAw== + dependencies: + ansi-escapes "^4.2.1" + chalk "^2.4.2" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.2.0" + rxjs "^6.5.3" + string-width "^4.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= + +ip-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.1.0.tgz#5ad62f685a14edb421abebc2fff8db94df67b455" + integrity sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA== + +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + +is-absolute-url@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-ci@^1.0.10: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== + dependencies: + ci-info "^1.5.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-even@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-even/-/is-even-1.0.0.tgz#76b5055fbad8d294a86b6a949015e1c97b717c06" + integrity sha1-drUFX7rY0pSoa2qUkBXhyXtxfAY= + dependencies: + is-odd "^0.1.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + +is-nan@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" + integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ== + dependencies: + define-properties "^1.1.3" + +is-npm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" + integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= + +is-number@^2.0.2: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-odd@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-0.1.2.tgz#bc573b5ce371ef2aad6e6f49799b72bef13978a7" + integrity sha1-vFc7XONx7yqtbm9JeZtyvvE5eKc= + dependencies: + is-number "^3.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= + +is-relative-url@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-relative-url/-/is-relative-url-3.0.0.tgz#f623c8e26baa5bd3742b3b7ec074f50f3b45b3f3" + integrity sha512-U1iSYRlY2GIMGuZx7gezlB5dp1Kheaym7zKzO1PV06mOihiWTXejLwm4poEJysPyXF+HtK/BEd0DVlcCh30pEA== + dependencies: + is-absolute-url "^3.0.0" + +is-retry-allowed@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + +is-self-closing@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-self-closing/-/is-self-closing-1.0.1.tgz#5f406b527c7b12610176320338af0fa3896416e4" + integrity sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg== + dependencies: + self-closing-tags "^1.0.1" + +is-stream@^1.0.0, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-uri@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-uri/-/is-uri-1.2.0.tgz#b92ff234af68c0ed97d2eed46492d01793b7d420" + integrity sha1-uS/yNK9owO2X0u7UZJLQF5O31CA= + dependencies: + parse-uri "~1.0.0" + punycode2 "~1.0.0" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +iso-639-3@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/iso-639-3/-/iso-639-3-1.2.0.tgz#eee1f5e6ca2bbb33e3ecc910857c1c12e8b295be" + integrity sha512-jNvD2P4JHNckQH7pc0R0SQ4oPCpyEtgs0nTtjB+DZCUDdygz0cOAxlcnq5KgNjjsqMHbR4Sbgwz2+DflzAZvlQ== + +isobject@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-0.2.0.tgz#a3432192f39b910b5f02cc989487836ec70aa85e" + integrity sha1-o0MhkvObkQtfAsyYlIeDbscKqF4= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isostring@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isostring/-/isostring-0.0.1.tgz#ddb608efbfc89cda86db9cb16be090a788134c7f" + integrity sha1-3bYI77/InNqG25yxa+CQp4gTTH8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +js-base64@^2.1.8: + version "2.5.1" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.1.tgz#1efa39ef2c5f7980bb1784ade4a8af2de3291121" + integrity sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.1.3: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +kareem@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" + integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.1.0, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0, kind-of@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +latest-version@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" + integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU= + dependencies: + package-json "^4.0.0" + +lazy-cache@^2.0.1, lazy-cache@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" + integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ= + dependencies: + set-getter "^0.1.0" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= + dependencies: + invert-kv "^1.0.0" + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.mergewith@^4.6.1: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + +lodash.template@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@~4.17.10, lodash@~4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +log-ok@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" + integrity sha1-vqPdNqzQuKckDXhza1uXxlREozQ= + dependencies: + ansi-green "^0.1.1" + success-symbol "^0.1.0" + +log-utils@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/log-utils/-/log-utils-0.2.1.tgz#a4c217a0dd9a50515d9b920206091ab3d4e031cf" + integrity sha1-pMIXoN2aUFFdm5ICBgkas9TgMc8= + dependencies: + ansi-colors "^0.2.0" + error-symbol "^0.1.0" + info-symbol "^0.1.0" + log-ok "^0.1.1" + success-symbol "^0.1.0" + time-stamp "^1.0.1" + warning-symbol "^0.1.0" + +logging-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/logging-helpers/-/logging-helpers-1.0.0.tgz#b5a37b32ad53eb0137c58c7898a47b175ddb7c36" + integrity sha512-qyIh2goLt1sOgQQrrIWuwkRjUx4NUcEqEGAcYqD8VOnOC6ItwkrVE8/tA4smGpjzyp4Svhc6RodDp9IO5ghpyA== + dependencies: + isobject "^3.0.0" + log-utils "^0.2.1" + +long-timeout@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + integrity sha1-lyHXiLR+C8taJMLivuGg2lXatRQ= + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memoize-one@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +metascraper-description@^5.4.0: + version "5.10.3" + resolved "https://registry.yarnpkg.com/metascraper-description/-/metascraper-description-5.10.3.tgz#55288e47becbb6f81bbb08f94dcc696486c2e05a" + integrity sha512-2jclyI20U5nKBwI/lGN/TEv8GzoL4wLYLW3wfMcFSQnwIABjcfJ5+W9hBQf3dE0eejWocQiRjZbXIvfWtO0WNA== + dependencies: + "@metascraper/helpers" "^5.10.3" + +metascraper-image@^5.4.0: + version "5.10.3" + resolved "https://registry.yarnpkg.com/metascraper-image/-/metascraper-image-5.10.3.tgz#853eb464c3b717f8fe4ac8bf31c4b2bc39b83216" + integrity sha512-22/WPf9A+jUc/Unvpfursi6FAfo/s71Pq/N9jiTNxBgOjfrUWjrzzw8qUKBxTc7Dwz1mERfDn2B0f/tKdsBAFA== + dependencies: + "@metascraper/helpers" "^5.10.3" + +metascraper-title@^5.4.0: + version "5.10.3" + resolved "https://registry.yarnpkg.com/metascraper-title/-/metascraper-title-5.10.3.tgz#549e35616f3fbc3ecae5fff8f5adc677f0e43a2d" + integrity sha512-HrYrZ/jOarFFvxUjuefBT9q4KDUqZnkvDhQM7WQ/8qE1Eht5WsijBxqWsDvONxIW/VhELhXendvt6MU9t+y2yA== + dependencies: + "@metascraper/helpers" "^5.10.3" + lodash "~4.17.15" + +metascraper-url@^5.4.0: + version "5.10.3" + resolved "https://registry.yarnpkg.com/metascraper-url/-/metascraper-url-5.10.3.tgz#95be7d8d27fae9b9e945dd0121669cd726b23618" + integrity sha512-38K6yodNfQRnrACXTZ76rE17WalwdW0ov1wjpPO0FP8LKJ/3I/GcBW8LOgwdMeyzlBBuF7UzPYPlLQzp1RZpMQ== + dependencies: + "@metascraper/helpers" "^5.10.3" + +metascraper@^5.4.0: + version "5.10.3" + resolved "https://registry.yarnpkg.com/metascraper/-/metascraper-5.10.3.tgz#0a914bc2d0cfebc7e81d00505fc113bd0c409113" + integrity sha512-F7VIGtApjHGdH/itxL+mCGMfJBo4TXzBio29vb+8y9Fam7cD3n8OrPFVInFOahOe1sBWeXgKP28cDFRkqM0yKQ== + dependencies: + "@metascraper/helpers" "^5.10.3" + cheerio "~1.0.0-rc.3" + cheerio-advanced-selectors "~2.0.1" + lodash "~4.17.15" + whoops "~4.1.0" + +method-override@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/method-override/-/method-override-3.0.0.tgz#6ab0d5d574e3208f15b0c9cf45ab52000468d7a2" + integrity sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA== + dependencies: + debug "3.1.0" + methods "~1.1.2" + parseurl "~1.3.2" + vary "~1.1.2" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.26: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.0.0.tgz#76044cfa8818bbf6999c5c9acadf2d3649b14b4b" + integrity sha512-PiVO95TKvhiwgSwg1IdLYlCTdul38yZxZMIcnDSFIBUm4BNZha2qpQ4GpJ++15bHoKDtrW2D69lMfFwdFYtNZQ== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" + integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +minipass@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5" + integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w== + dependencies: + yallist "^4.0.0" + +minizlib@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3" + integrity sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +moment-timezone@^0.5.25: + version "0.5.27" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.27.tgz#73adec8139b6fe30452e78f210f27b1f346b8877" + integrity sha512-EIKQs7h5sAsjhPCqN6ggx6cEbs94GK050254TIJySD1bzoM5JTYDwAU1IoVOeTOL6Gm27kYJ51/uuvq1kIlrbw== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0", moment@^2.18.1, moment@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + +mongodb-core@2.1.20: + version "2.1.20" + resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-2.1.20.tgz#fece8dd76b59ee7d7f2d313b65322c160492d8f1" + integrity sha512-IN57CX5/Q1bhDq6ShAR6gIv4koFsZP7L8WOK1S0lR0pVDQaScffSMV5jxubLsmZ7J+UdqmykKw4r9hG3XQEGgQ== + dependencies: + bson "~1.0.4" + require_optional "~1.0.0" + +mongodb@3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.4.1.tgz#0d15e57e0ea0fc85b7a4fb9291b374c2e71652dc" + integrity sha512-juqt5/Z42J4DcE7tG7UdVaTKmUC6zinF4yioPfpeOSNBieWSK6qCY+0tfGQcHLKrauWPDdMZVROHJOa8q2pWsA== + dependencies: + bson "^1.1.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +mongodb@^2.0.36: + version "2.2.36" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-2.2.36.tgz#1c573680b2849fb0f47acbba3dc5fa228de975f5" + integrity sha512-P2SBLQ8Z0PVx71ngoXwo12+FiSfbNfGOClAao03/bant5DgLNkOPAck5IaJcEk4gKlQhDEURzfR3xuBG1/B+IA== + dependencies: + es6-promise "3.2.1" + mongodb-core "2.1.20" + readable-stream "2.2.7" + +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== + +mongoose@^5.4.19: + version "5.8.7" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.8.7.tgz#d175f8ac4c8ed7bf57b71aa4980aaa3525b923b2" + integrity sha512-PCCuTrdxpUmO86L1geXWE+9AvJRFuneIrMUT2hB/LXZ+5HIGOIfE6OSMU7cd8wFU7JRINP9V73zZ9YsmAbt+Iw== + dependencies: + bson "~1.1.1" + kareem "2.3.1" + mongodb "3.4.1" + mongoose-legacy-pluralize "1.0.2" + mpath "0.6.0" + mquery "3.2.2" + ms "2.1.2" + regexp-clone "1.0.0" + safe-buffer "5.1.2" + sift "7.0.1" + sliced "1.0.1" + +morgan@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.1.tgz#0a8d16734a1d9afbc824b99df87e738e58e2da59" + integrity sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA== + dependencies: + basic-auth "~2.0.0" + debug "2.6.9" + depd "~1.1.2" + on-finished "~2.3.0" + on-headers "~1.0.1" + +mpath@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" + integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== + +mquery@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" + integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "^1.0.0" + safe-buffer "5.1.2" + sliced "1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nan@^2.12.1, nan@^2.13.2, nan@^2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanoid@^2.1.0: + version "2.1.9" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.9.tgz#edc71de7b16fc367bbb447c7a638ccebe07a17a1" + integrity sha512-J2X7aUpdmTlkAuSe9WaQ5DsTZZPW1r/zmEWKsGhbADO6Gm9FMd2ZzJ8NhsmP4OtA9oFhXfxNqPlreHEDOGB4sg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +napi-build-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508" + integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-abi@^2.7.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.13.0.tgz#e2f2ec444d0aca3ea1b3874b6de41d1665828f63" + integrity sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA== + dependencies: + semver "^5.4.1" + +node-gyp@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" + integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "^2.87.0" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-sass@^4.12.0: + version "4.13.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" + integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash "^4.17.15" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.13.2" + node-gyp "^3.8.0" + npmlog "^4.0.0" + request "^2.88.0" + sass-graph "^2.2.4" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" + +node-schedule@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.2.tgz#d774b383e2a6f6ade59eecc62254aea07cd758cb" + integrity sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw== + dependencies: + cron-parser "^2.7.3" + long-timeout "0.1.1" + sorted-array-functions "^1.0.0" + +nodemailer-express-handlebars@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nodemailer-express-handlebars/-/nodemailer-express-handlebars-3.1.0.tgz#e80da7528cfd30a8dd49c002642854edcab72c68" + integrity sha512-JLt+lAlo/D3TfpVnDY+imLHAAonX5L9FEHaczyiPxWCH259bsfxEjPSWl/rWgnvcoFT8uC/hwHi/I0ScFmHlIQ== + dependencies: + express-handlebars "^3.0.0" + +nodemailer@^6.2.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.2.tgz#7147550e32cdc37453380ab78d2074533966090a" + integrity sha512-g0n4nH1ONGvqYo1v72uSWvF/MRNnnq1LzmSzXb/6EPF3LFb51akOhgG3K2+aETAsJx90/Q5eFNTntu4vBCwyQQ== + +nodemon@^1.19.1: + version "1.19.4" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.4.tgz#56db5c607408e0fdf8920d2b444819af1aae0971" + integrity sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ== + dependencies: + chokidar "^2.1.8" + debug "^3.2.6" + ignore-by-default "^1.0.1" + minimatch "^3.0.4" + pstree.remy "^1.1.7" + semver "^5.7.1" + supports-color "^5.5.0" + touch "^3.1.0" + undefsafe "^2.0.2" + update-notifier "^2.5.0" + +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= + +"nopt@2 || 3": + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= + dependencies: + abbrev "1" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-url@^4.1.0, normalize-url@~4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.11, object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1, on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= + dependencies: + lcid "^1.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +package-json@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" + integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0= + dependencies: + got "^6.7.1" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parse-uri@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-uri/-/parse-uri-1.0.0.tgz#2872dcc22f1a797acde1583d8a0ac29552ddac20" + integrity sha1-KHLcwi8aeXrN4Vg9igrClVLdrCA= + +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== + dependencies: + "@types/node" "*" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +passport-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee" + integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4= + dependencies: + passport-strategy "1.x.x" + +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= + +passport@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" + integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss@^7.0.5: + version "7.0.26" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.26.tgz#5ed615cfcab35ba9bbb82414a4fa88ea10429587" + integrity sha512-IY4oRjpXWYshuTDFxMVkJDtWIk2LhsTlu8bZnbEJA4+bYT16Lvpo8Qv6EvDumhYRgzjZl489pmsY3qVgJQ08nA== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +prebuild-install@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.3.tgz#ef4052baac60d465f5ba6bf003c9c1de79b9da8e" + integrity sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.0" + mkdirp "^0.5.1" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise@^8.0.2: + version "8.0.3" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6" + integrity sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw== + dependencies: + asap "~2.0.6" + +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +psl@^1.1.24: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + +pstree.remy@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3" + integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode2@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/punycode2/-/punycode2-1.0.0.tgz#e2b4b9a9a8ff157d0b84438e203181ee7892dfd8" + integrity sha1-4rS5qaj/FX0LhEOOIDGB7niS39g= + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs= + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +readable-stream@2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.7.tgz#07057acbe2467b22042d36f98c5ad507054e95b1" + integrity sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE= + dependencies: + buffer-shims "~1.0.0" + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~1.0.0" + util-deprecate "~1.0.1" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.1, readable-stream@^3.1.1: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" + integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +registry-auth-token@^3.0.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" + integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= + dependencies: + rc "^1.0.1" + +relative@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" + integrity sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8= + dependencies: + isobject "^2.0.0" + +remarkable@^1.6.2, remarkable@^1.7.1: + version "1.7.4" + resolved "https://registry.yarnpkg.com/remarkable/-/remarkable-1.7.4.tgz#19073cb960398c87a7d6546eaa5e50d2022fcd00" + integrity sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg== + dependencies: + argparse "^1.0.10" + autolinker "~0.28.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +request@^2.87.0, request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require_optional@^1.0.1, require_optional@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.10.0: + version "1.14.2" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.14.2.tgz#dbf31d0fa98b1f29aa5169783b9c290cb865fea2" + integrity sha512-EjlOBLBO1kxsUxsKjLt7TAECyKW6fOh1VRkykQkKGzcBbjjPIxBqGh0jf7GJ3k/f5mxMqW3htMD3WdTUVtW8HQ== + dependencies: + path-parse "^1.0.6" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rimraf@2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +rxjs@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" + integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.0, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sanitize-html@^1.20.0: + version "1.21.1" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.21.1.tgz#1647d15c0c672901aa41eac1b86d0c38146d30ce" + integrity sha512-W6enXSVphVaVbmVbzVngBthR5f5sMmhq3EfPfBlzBzp2WnX8Rnk7NGpP7KmHUc0Y3MVk9tv/+CbpdHchX9ai7g== + dependencies: + chalk "^2.4.1" + htmlparser2 "^3.10.0" + lodash.clonedeep "^4.5.0" + lodash.escaperegexp "^4.1.2" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.mergewith "^4.6.1" + postcss "^7.0.5" + srcset "^1.0.0" + xtend "^4.0.1" + +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +sass-graph@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +self-closing-tags@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/self-closing-tags/-/self-closing-tags-1.0.1.tgz#6c5fa497994bb826b484216916371accee490a5d" + integrity sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA== + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= + dependencies: + semver "^5.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5.0, semver@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-getter@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" + integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y= + dependencies: + to-object-path "^0.3.0" + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +sharp@^0.23.0: + version "0.23.4" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.23.4.tgz#ca36067cb6ff7067fa6c77b01651cb9a890f8eb3" + integrity sha512-fJMagt6cT0UDy9XCsgyLi0eiwWWhQRxbwGmqQT6sY8Av4s0SVsT/deg8fobBQCTDU5iXRgz0rAeXoE2LBZ8g+Q== + dependencies: + color "^3.1.2" + detect-libc "^1.0.3" + nan "^2.14.0" + npmlog "^4.1.2" + prebuild-install "^5.3.3" + semver "^6.3.0" + simple-get "^3.1.0" + tar "^5.0.5" + tunnel-agent "^0.6.0" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shortid@^2.2.14: + version "2.2.15" + resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122" + integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw== + dependencies: + nanoid "^2.1.0" + +sift@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" + integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" + integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY= + +simple-get@^3.0.3, simple-get@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= + +smartquotes@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/smartquotes/-/smartquotes-2.3.1.tgz#01ebb595d6c7a9e24d90e8cb95c17d0e1af49407" + integrity sha1-Aeu1ldbHqeJNkOjLlcF9Dhr0lAc= + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sorted-array-functions@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz#43265b21d6e985b7df31621b1c11cc68d8efc7c3" + integrity sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg== + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + integrity sha1-66T12pwNyZneaAMti092FzZSA2s= + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +srcset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" + integrity sha1-pWad4StC87HV6D7QPHEEb8SPQe8= + dependencies: + array-uniq "^1.0.2" + number-is-nan "^1.0.0" + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stdout-stream@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" + integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== + dependencies: + readable-stream "^2.0.1" + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + integrity sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +striptags@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.1.1.tgz#c8c3e7fdd6fb4bb3a32a3b752e5b5e3e38093ebd" + integrity sha1-yMPn/db7S7OjKjt1LltePjgJPr0= + +success-symbol@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/success-symbol/-/success-symbol-0.1.0.tgz#24022e486f3bf1cdca094283b769c472d3b72897" + integrity sha1-JAIuSG878c3KCUKDt2nEctO3KJc= + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + integrity sha1-vnoN5ITexcXN34s9WRJQRJEvY1s= + dependencies: + has-flag "^2.0.0" + +supports-color@^5.3.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +tar-fs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.0.tgz#677700fc0c8b337a78bee3623fdc235f21d7afad" + integrity sha512-vaY0obB6Om/fso8a8vakQBzwholQ7v5+uy+tF3Ozvxv1KNezmVQAiWtcNmMHFSFPqL3dJA8ha6gdtFbfX9mcxA== + dependencies: + chownr "^1.1.1" + mkdirp "^0.5.1" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3" + integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw== + dependencies: + bl "^3.0.0" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== + dependencies: + block-stream "*" + fstream "^1.0.12" + inherits "2" + +tar@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/tar/-/tar-5.0.5.tgz#03fcdb7105bc8ea3ce6c86642b9c942495b04f93" + integrity sha512-MNIgJddrV2TkuwChwcSNds/5E9VijOiw7kAc1y5hTNJoLDSuIyid2QtLYiCYNnICebpuvjhPQZsXwUL0O3l7OQ== + dependencies: + chownr "^1.1.3" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.0" + mkdirp "^0.5.0" + yallist "^4.0.0" + +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= + dependencies: + execa "^0.7.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +time-stamp@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= + +timed-out@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" + integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= + +title@~3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/title/-/title-3.4.1.tgz#166177c48f52ae9b73afe9901a183f0b6c97b2e6" + integrity sha512-CoIakPr4Gi91bm4Xp6xTuZ3EENxPcKgOJzHzrhSdUatGtFMeVJa2qxI4WNhJHgz0Jak5Ck6jY2UNFM71/yBIsw== + dependencies: + arg "1.0.0" + chalk "2.3.0" + clipboardy "1.2.2" + titleize "1.0.0" + +titleize@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/titleize/-/titleize-1.0.0.tgz#7d350722061830ba6617631e0cfd3ea08398d95a" + integrity sha1-fTUHIgYYMLpmF2MeDP0+oIOY2Vo= + +tlds@^1.203.0: + version "1.207.0" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.207.0.tgz#459264e644cf63ddc0965fece3898913286b1afd" + integrity sha512-k7d7Q1LqjtAvhtEOs3yN14EabsNO8ZCoY6RESSJDB9lst3bTx3as/m1UuAeCKzYxiyhR1qq72ZPhpSf+qlqiwg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-gfm-code-block@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz#25d045a5fae553189e9637b590900da732d8aa82" + integrity sha1-JdBFpfrlUxielje1kJANpzLYqoI= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +"true-case-path@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" + integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== + dependencies: + glob "^7.1.2" + +truncate@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/truncate/-/truncate-2.1.0.tgz#391183563a25cffbd4d613a1d00ae5844c9e55d3" + integrity sha512-em3E3SUDONOjTBcZ36DTm3RvDded3IRU9rX32oHwwXNt3rJD5MVaFlJTQvs8tJoHRoeYP36OuQ1eL/Q7bNEWIQ== + +tslib@^1.9.0, tslib@^1.9.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typeof-article@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/typeof-article/-/typeof-article-0.1.1.tgz#9f07e733c3fbb646ffa9e61c08debacd460e06af" + integrity sha1-nwfnM8P7tkb/qeYcCN66zUYOBq8= + dependencies: + kind-of "^3.1.0" + +uglify-js@^3.1.4: + version "3.7.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.7.5.tgz#278c7c24927ac5a32d3336fc68fd4ae1177a486a" + integrity sha512-GFZ3EXRptKGvb/C1Sq6nO1iI7AGcjyqmIyOw0DrD0675e+NNbGO72xmMM2iEBdFbxaTLo70NbjM/Wy54uZIlsg== + dependencies: + commander "~2.20.3" + source-map "~0.6.1" + +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + +undefsafe@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" + integrity sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY= + dependencies: + debug "^2.2.0" + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= + dependencies: + crypto-random-string "^1.0.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +unzip-response@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" + integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= + +upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-notifier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" + integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== + dependencies: + boxen "^1.2.1" + chalk "^2.0.1" + configstore "^3.0.0" + import-lazy "^2.1.0" + is-ci "^1.0.10" + is-installed-globally "^0.1.0" + is-npm "^1.0.0" + latest-version "^3.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= + dependencies: + prepend-http "^1.0.1" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +url-regex@~5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-5.0.0.tgz#8f5456ab83d898d18b2f91753a702649b873273a" + integrity sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g== + dependencies: + ip-regex "^4.1.0" + tlds "^1.203.0" + +urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" + integrity sha1-I/iQaabGL0bPOh07ABac77kL4MY= + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validator@^10.4.0: + version "10.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228" + integrity sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +video-extensions@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/video-extensions/-/video-extensions-1.1.0.tgz#eaa86b45f29a853c2b873e9d8e23b513712997d6" + integrity sha1-6qhrRfKahTwrhz6djiO1E3Epl9Y= + +warning-symbol@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/warning-symbol/-/warning-symbol-0.1.0.tgz#bb31dd11b7a0f9d67ab2ed95f457b65825bbad21" + integrity sha1-uzHdEbeg+dZ6su2V9Fe2WCW7rSE= + +web-push@^3.3.5: + version "3.4.3" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.4.3.tgz#88bdf8a8079b24efbc569da7d1f2c3556126f407" + integrity sha512-nt/hRSlfRDTwvem//7jle1+cy62lBoxFshad8ai2Q4SlHZS00oHnrw5Dul3jSWXR+bOcnZkwnRs3tW+daNTuyA== + dependencies: + asn1.js "^5.0.0" + http_ece "1.1.0" + https-proxy-agent "^3.0.0" + jws "^3.1.3" + minimist "^1.2.0" + urlsafe-base64 "^1.0.0" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= + +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + +which@1, which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +whoops@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/whoops/-/whoops-4.1.0.tgz#f42e51514c7af19a9491a44cabf2712292c6a8e1" + integrity sha512-42soctqvFs9FaU1r4ZadCy2F6A9dUc4SN3ud+tbDEdmyZDTeYBgKKqtIdo6NiQlnZnJegWRCyKLk2edYH9DsHA== + dependencies: + clean-stack "~2.2.0" + mimic-fn "~3.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +widest-line@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" + integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== + dependencies: + string-width "^2.1.1" + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.0.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= + +xtend@^4.0.1, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= + dependencies: + camelcase "^3.0.0" + +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +year@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/year/-/year-0.2.1.tgz#4083ae520a318b23ec86037f3000cb892bdf9bb0" + integrity sha1-QIOuUgoxiyPshgN/MADLiSvfm7A=