606 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			606 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
var Rx = require('rx'),
 | 
						|
    nodemailer = require('nodemailer'),
 | 
						|
    assign = require('object.assign'),
 | 
						|
    sanitizeHtml = require('sanitize-html'),
 | 
						|
    moment = require('moment'),
 | 
						|
    mongodb = require('mongodb'),
 | 
						|
    debug = require('debug')('freecc:cntr:story'),
 | 
						|
    utils = require('../utils'),
 | 
						|
    observeMethod = require('../utils/rx').observeMethod,
 | 
						|
    saveUser = require('../utils/rx').saveUser,
 | 
						|
    saveInstance = require('../utils/rx').saveInstance,
 | 
						|
    MongoClient = mongodb.MongoClient,
 | 
						|
    secrets = require('../../config/secrets');
 | 
						|
 | 
						|
var foundationDate = 1413298800000;
 | 
						|
var time48Hours = 172800000;
 | 
						|
 | 
						|
var unDasherize = utils.unDasherize;
 | 
						|
var dasherize = utils.dasherize;
 | 
						|
var getURLTitle = utils.getURLTitle;
 | 
						|
 | 
						|
var transporter = nodemailer.createTransport({
 | 
						|
  service: 'Mandrill',
 | 
						|
  auth: {
 | 
						|
    user: secrets.mandrill.user,
 | 
						|
    pass: secrets.mandrill.password
 | 
						|
  }
 | 
						|
});
 | 
						|
 | 
						|
function sendMailWhillyNilly(mailOptions) {
 | 
						|
  transporter.sendMail(mailOptions, function(err) {
 | 
						|
    if (err) {
 | 
						|
      console.log('err sending mail whilly nilly', err);
 | 
						|
      console.log('logging err but not carring');
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
function hotRank(timeValue, rank) {
 | 
						|
  /*
 | 
						|
  * Hotness ranking algorithm: http://amix.dk/blog/post/19588
 | 
						|
  * tMS = postedOnDate - foundationTime;
 | 
						|
  * Ranking...
 | 
						|
  * f(ts, 1, rank) = log(10)z + (ts)/45000;
 | 
						|
  */
 | 
						|
  var z = Math.log(rank) / Math.log(10);
 | 
						|
  var hotness = z + (timeValue / time48Hours);
 | 
						|
  return hotness;
 | 
						|
}
 | 
						|
 | 
						|
function sortByRank(a, b) {
 | 
						|
  return hotRank(b.timePosted - foundationDate, b.rank) -
 | 
						|
    hotRank(a.timePosted - foundationDate, a.rank);
 | 
						|
}
 | 
						|
 | 
						|
function cleanData(data, opts) {
 | 
						|
  var options = assign(
 | 
						|
    {},
 | 
						|
    {
 | 
						|
      allowedTags: [],
 | 
						|
      allowedAttributes: []
 | 
						|
    },
 | 
						|
    opts || {}
 | 
						|
  );
 | 
						|
  return sanitizeHtml(data, options).replace(/";/g, '"');
 | 
						|
}
 | 
						|
 | 
						|
module.exports = function(app) {
 | 
						|
  var router = app.loopback.Router();
 | 
						|
  var User = app.models.User;
 | 
						|
  var findUserById = observeMethod(User, 'findById');
 | 
						|
  var findOneUser = observeMethod(User, 'findOne');
 | 
						|
 | 
						|
  var Story = app.models.Story;
 | 
						|
  var findStory = observeMethod(Story, 'find');
 | 
						|
  var findOneStory = observeMethod(Story, 'findOne');
 | 
						|
  var findStoryById = observeMethod(Story, 'findById');
 | 
						|
  var countStories = observeMethod(Story, 'count');
 | 
						|
 | 
						|
  var Comment = app.models.Comment;
 | 
						|
  var findCommentById = observeMethod(Comment, 'findById');
 | 
						|
 | 
						|
  router.get('/stories/hotStories', hotJSON);
 | 
						|
  router.get('/stories/comments/:id', comments);
 | 
						|
  router.post('/stories/comment/', commentSubmit);
 | 
						|
  router.post('/stories/comment/:id/comment', commentOnCommentSubmit);
 | 
						|
  router.put('/stories/comment/:id/edit', commentEdit);
 | 
						|
  router.get('/stories/submit', submitNew);
 | 
						|
  router.get('/stories/submit/new-story', preSubmit);
 | 
						|
  router.post('/stories/preliminary', newStory);
 | 
						|
  router.post('/stories/', storySubmission);
 | 
						|
  router.get('/news/', hot);
 | 
						|
  router.post('/stories/search', getStories);
 | 
						|
  router.get('/news/:storyName', returnIndividualStory);
 | 
						|
  router.post('/stories/upvote/', upvote);
 | 
						|
  router.get('/stories/:storyName', redirectToNews);
 | 
						|
 | 
						|
  app.use(router);
 | 
						|
 | 
						|
  function redirectToNews(req, res) {
 | 
						|
    var url = req.originalUrl.replace(/^\/stories/, '/news');
 | 
						|
    return res.redirect(url);
 | 
						|
  }
 | 
						|
 | 
						|
  function hotJSON(req, res, next) {
 | 
						|
    var query = {
 | 
						|
      order: 'timePosted DESC',
 | 
						|
      limit: 1000
 | 
						|
    };
 | 
						|
    findStory(query).subscribe(
 | 
						|
      function(stories) {
 | 
						|
        var sliceVal = stories.length >= 100 ? 100 : stories.length;
 | 
						|
        var data = stories.sort(sortByRank).slice(0, sliceVal);
 | 
						|
        res.json(data);
 | 
						|
      },
 | 
						|
      next
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  function hot(req, res) {
 | 
						|
    return res.render('stories/index', {
 | 
						|
      title: 'Hot stories currently trending on Camper News',
 | 
						|
      page: 'hot'
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function submitNew(req, res) {
 | 
						|
    return res.render('stories/index', {
 | 
						|
      title: 'Submit a new story to Camper News',
 | 
						|
      page: 'submit'
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function preSubmit(req, res) {
 | 
						|
    var data = req.query;
 | 
						|
    var cleanedData = cleanData(data.url);
 | 
						|
 | 
						|
    if (data.url.replace(/&/g, '&') !== cleanedData) {
 | 
						|
      req.flash('errors', {
 | 
						|
        msg: 'The data for this post is malformed'
 | 
						|
      });
 | 
						|
      return res.render('stories/index', {
 | 
						|
        page: 'stories/submit'
 | 
						|
      });
 | 
						|
    }
 | 
						|
 | 
						|
    var title = data.title || '';
 | 
						|
    var image = data.image || '';
 | 
						|
    var description = data.description || '';
 | 
						|
    return res.render('stories/index', {
 | 
						|
      title: 'Confirm your Camper News story submission',
 | 
						|
      page: 'storySubmission',
 | 
						|
      storyURL: data.url,
 | 
						|
      storyTitle: title,
 | 
						|
      storyImage: image,
 | 
						|
      storyMetaDescription: description
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function returnIndividualStory(req, res, next) {
 | 
						|
    var dashedName = req.params.storyName;
 | 
						|
    var storyName = unDasherize(dashedName);
 | 
						|
 | 
						|
    findOneStory({ where: { storyLink: storyName } }).subscribe(
 | 
						|
      function(story) {
 | 
						|
        if (!story) {
 | 
						|
          req.flash('errors', {
 | 
						|
            msg: "404: We couldn't find a story with that name. " +
 | 
						|
            'Please double check the name.'
 | 
						|
          });
 | 
						|
          return res.redirect('/news');
 | 
						|
        }
 | 
						|
 | 
						|
        var dashedNameFull = story.storyLink.toLowerCase()
 | 
						|
          .replace(/\s+/g, ' ')
 | 
						|
          .replace(/\s/g, '-');
 | 
						|
 | 
						|
        if (dashedNameFull !== dashedName) {
 | 
						|
          return res.redirect('../stories/' + dashedNameFull);
 | 
						|
        }
 | 
						|
 | 
						|
        var username = req.user ? req.user.username : '';
 | 
						|
        // true if any of votes are made by user
 | 
						|
        var userVoted = story.upVotes.some(function(upvote) {
 | 
						|
          return upvote.upVotedByUsername === username;
 | 
						|
        });
 | 
						|
 | 
						|
        res.render('stories/index', {
 | 
						|
          title: story.headline,
 | 
						|
          link: story.link,
 | 
						|
          originalStoryLink: dashedName,
 | 
						|
          originalStoryAuthorEmail: story.author.email || '',
 | 
						|
          author: story.author,
 | 
						|
          description: story.description,
 | 
						|
          rank: story.upVotes.length,
 | 
						|
          upVotes: story.upVotes,
 | 
						|
          comments: story.comments,
 | 
						|
          id: story.id,
 | 
						|
          timeAgo: moment(story.timePosted).fromNow(),
 | 
						|
          image: story.image,
 | 
						|
          page: 'show',
 | 
						|
          storyMetaDescription: story.metaDescription,
 | 
						|
          hasUserVoted: userVoted
 | 
						|
        });
 | 
						|
      },
 | 
						|
      next
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  function getStories(req, res, next) {
 | 
						|
    MongoClient.connect(secrets.db, function(err, database) {
 | 
						|
      if (err) {
 | 
						|
        return next(err);
 | 
						|
      }
 | 
						|
      database.collection('stories').find({
 | 
						|
        '$text': {
 | 
						|
          '$search': req.body.data ? req.body.data.searchValue : ''
 | 
						|
        }
 | 
						|
      }, {
 | 
						|
        headline: 1,
 | 
						|
        timePosted: 1,
 | 
						|
        link: 1,
 | 
						|
        description: 1,
 | 
						|
        rank: 1,
 | 
						|
        upVotes: 1,
 | 
						|
        author: 1,
 | 
						|
        comments: 1,
 | 
						|
        image: 1,
 | 
						|
        storyLink: 1,
 | 
						|
        metaDescription: 1,
 | 
						|
        textScore: {
 | 
						|
          $meta: 'textScore'
 | 
						|
        }
 | 
						|
      }, {
 | 
						|
        sort: {
 | 
						|
          textScore: {
 | 
						|
            $meta: 'textScore'
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }).toArray(function(err, items) {
 | 
						|
        if (err) {
 | 
						|
          return next(err);
 | 
						|
        }
 | 
						|
        if (items !== null && items.length !== 0) {
 | 
						|
          return res.json(items);
 | 
						|
        }
 | 
						|
        return res.sendStatus(404);
 | 
						|
      });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  function upvote(req, res, next) {
 | 
						|
    var id = req.body.data.id;
 | 
						|
    var story$ = findStoryById(id).shareReplay();
 | 
						|
 | 
						|
    story$.flatMap(function(story) {
 | 
						|
        // find story author
 | 
						|
        return findUserById(story.author.userId);
 | 
						|
      })
 | 
						|
      .flatMap(function(user) {
 | 
						|
        // if user deletes account then this will not exist
 | 
						|
        if (user) {
 | 
						|
          user.progressTimestamps.push(Date.now());
 | 
						|
        }
 | 
						|
        return saveUser(user);
 | 
						|
      })
 | 
						|
      .flatMap(function() {
 | 
						|
        req.user.progressTimestamps.push(Date.now());
 | 
						|
        return saveUser(req.user);
 | 
						|
      })
 | 
						|
      .flatMap(function() {
 | 
						|
        return story$;
 | 
						|
      })
 | 
						|
      .flatMap(function(story) {
 | 
						|
        debug('upvoting');
 | 
						|
        story.rank += 1;
 | 
						|
        story.upVotes.push({
 | 
						|
          upVotedBy: req.user.id,
 | 
						|
          upVotedByUsername: req.user.username
 | 
						|
        });
 | 
						|
        return saveInstance(story);
 | 
						|
      })
 | 
						|
      .subscribe(
 | 
						|
        function(story) {
 | 
						|
          return res.send(story);
 | 
						|
        },
 | 
						|
        next
 | 
						|
      );
 | 
						|
  }
 | 
						|
 | 
						|
  function comments(req, res, next) {
 | 
						|
    var id = req.params.id;
 | 
						|
    findCommentById(id).subscribe(
 | 
						|
      function(comment) {
 | 
						|
        res.send(comment);
 | 
						|
      },
 | 
						|
      next
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  function newStory(req, res, next) {
 | 
						|
    if (!req.user) {
 | 
						|
      return next(new Error('Must be logged in'));
 | 
						|
    }
 | 
						|
    var url = req.body.data.url;
 | 
						|
    var cleanURL = cleanData(url);
 | 
						|
 | 
						|
    if (cleanURL !== url) {
 | 
						|
      req.flash('errors', {
 | 
						|
        msg: "The URL you submitted doesn't appear valid"
 | 
						|
      });
 | 
						|
      return res.json({
 | 
						|
        alreadyPosted: true,
 | 
						|
        storyURL: '/stories/submit'
 | 
						|
      });
 | 
						|
 | 
						|
    }
 | 
						|
    if (url.search(/^https?:\/\//g) === -1) {
 | 
						|
      url = 'http://' + url;
 | 
						|
    }
 | 
						|
 | 
						|
    findStory({ where: { link: url } })
 | 
						|
      .map(function(stories) {
 | 
						|
        if (stories.length) {
 | 
						|
          return {
 | 
						|
            alreadyPosted: true,
 | 
						|
            storyURL: '/stories/' + stories.pop().storyLink
 | 
						|
          };
 | 
						|
        }
 | 
						|
        return {
 | 
						|
          alreadyPosted: false,
 | 
						|
          storyURL: url
 | 
						|
        };
 | 
						|
      })
 | 
						|
      .flatMap(function(data) {
 | 
						|
        if (data.alreadyPosted) {
 | 
						|
          return Rx.Observable.just(data);
 | 
						|
        }
 | 
						|
        return Rx.Observable.fromNodeCallback(getURLTitle)(data.storyURL)
 | 
						|
          .map(function(story) {
 | 
						|
            return {
 | 
						|
              alreadyPosted: false,
 | 
						|
              storyURL: data.storyURL,
 | 
						|
              storyTitle: story.title,
 | 
						|
              storyImage: story.image,
 | 
						|
              storyMetaDescription: story.description
 | 
						|
            };
 | 
						|
          });
 | 
						|
      })
 | 
						|
      .subscribe(
 | 
						|
        function(story) {
 | 
						|
          if (story.alreadyPosted) {
 | 
						|
            req.flash('errors', {
 | 
						|
              msg: "Someone's already posted that link. Here's the discussion."
 | 
						|
            });
 | 
						|
          }
 | 
						|
          res.json(story);
 | 
						|
        },
 | 
						|
        next
 | 
						|
      );
 | 
						|
  }
 | 
						|
 | 
						|
  function storySubmission(req, res, next) {
 | 
						|
    var data = req.body.data;
 | 
						|
    if (!req.user) {
 | 
						|
      return next(new Error('Not authorized'));
 | 
						|
    }
 | 
						|
    var storyLink = data.headline
 | 
						|
      .replace(/[^a-z0-9\s]/gi, '')
 | 
						|
      .replace(/\s+/g, ' ')
 | 
						|
      .toLowerCase()
 | 
						|
      .trim();
 | 
						|
 | 
						|
    var link = data.link;
 | 
						|
 | 
						|
    if (link.search(/^https?:\/\//g) === -1) {
 | 
						|
      link = 'http://' + link;
 | 
						|
    }
 | 
						|
 | 
						|
    var query = {
 | 
						|
      storyLink: {
 | 
						|
        like: ('^' + storyLink + '(?: [0-9]+)?$'),
 | 
						|
        options: 'i'
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    var savedStory = countStories(query)
 | 
						|
      .flatMap(function(storyCount) {
 | 
						|
        // if duplicate storyLink add unique number
 | 
						|
        storyLink = (storyCount === 0) ?
 | 
						|
          storyLink :
 | 
						|
          storyLink + ' ' + storyCount;
 | 
						|
 | 
						|
        var link = data.link;
 | 
						|
        if (link.search(/^https?:\/\//g) === -1) {
 | 
						|
          link = 'http://' + link;
 | 
						|
        }
 | 
						|
        var newStory = new Story({
 | 
						|
          headline: cleanData(data.headline),
 | 
						|
          timePosted: Date.now(),
 | 
						|
          link: link,
 | 
						|
          description: cleanData(data.description),
 | 
						|
          rank: 1,
 | 
						|
          upVotes: [({
 | 
						|
            upVotedBy: req.user.id,
 | 
						|
            upVotedByUsername: req.user.username
 | 
						|
          })],
 | 
						|
          author: {
 | 
						|
            picture: req.user.picture,
 | 
						|
            userId: req.user.id,
 | 
						|
            username: req.user.username,
 | 
						|
            email: req.user.email
 | 
						|
          },
 | 
						|
          comments: [],
 | 
						|
          image: data.image,
 | 
						|
          storyLink: storyLink,
 | 
						|
          metaDescription: data.storyMetaDescription,
 | 
						|
          originalStoryAuthorEmail: req.user.email
 | 
						|
        });
 | 
						|
        return saveInstance(newStory);
 | 
						|
      });
 | 
						|
 | 
						|
    req.user.progressTimestamps.push(Date.now());
 | 
						|
    return saveUser(req.user)
 | 
						|
      .flatMap(savedStory)
 | 
						|
      .subscribe(
 | 
						|
        function(story) {
 | 
						|
          res.json({
 | 
						|
            storyLink: dasherize(story.storyLink)
 | 
						|
          });
 | 
						|
        },
 | 
						|
        next
 | 
						|
      );
 | 
						|
  }
 | 
						|
 | 
						|
  function commentSubmit(req, res, next) {
 | 
						|
    var data = req.body.data;
 | 
						|
    if (!req.user) {
 | 
						|
      return next(new Error('Not authorized'));
 | 
						|
    }
 | 
						|
    var sanitizedBody = cleanData(data.body);
 | 
						|
 | 
						|
    if (data.body !== sanitizedBody) {
 | 
						|
      req.flash('errors', {
 | 
						|
        msg: 'HTML is not allowed'
 | 
						|
      });
 | 
						|
      return res.send(true);
 | 
						|
    }
 | 
						|
    var comment = new Comment({
 | 
						|
      associatedPost: data.associatedPost,
 | 
						|
      originalStoryLink: data.originalStoryLink,
 | 
						|
      originalStoryAuthorEmail: data.originalStoryAuthorEmail,
 | 
						|
      body: sanitizedBody,
 | 
						|
      rank: 0,
 | 
						|
      upvotes: 0,
 | 
						|
      author: {
 | 
						|
        picture: req.user.picture,
 | 
						|
        userId: req.user.id,
 | 
						|
        username: req.user.username,
 | 
						|
        email: req.user.email
 | 
						|
      },
 | 
						|
      comments: [],
 | 
						|
      topLevel: true,
 | 
						|
      commentOn: Date.now()
 | 
						|
    });
 | 
						|
 | 
						|
    commentSave(comment, findStoryById).subscribe(
 | 
						|
      function() {},
 | 
						|
      next,
 | 
						|
      function() {
 | 
						|
        res.send(true);
 | 
						|
      }
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  function commentOnCommentSubmit(req, res, next) {
 | 
						|
    var data = req.body.data;
 | 
						|
    if (!req.user) {
 | 
						|
      return next(new Error('Not authorized'));
 | 
						|
    }
 | 
						|
 | 
						|
    var sanitizedBody = cleanData(data.body);
 | 
						|
 | 
						|
    if (data.body !== sanitizedBody) {
 | 
						|
      req.flash('errors', {
 | 
						|
        msg: 'HTML is not allowed'
 | 
						|
      });
 | 
						|
      return res.send(true);
 | 
						|
    }
 | 
						|
 | 
						|
    var comment = new Comment({
 | 
						|
      associatedPost: data.associatedPost,
 | 
						|
      body: sanitizedBody,
 | 
						|
      rank: 0,
 | 
						|
      upvotes: 0,
 | 
						|
      originalStoryLink: data.originalStoryLink,
 | 
						|
      originalStoryAuthorEmail: data.originalStoryAuthorEmail,
 | 
						|
      author: {
 | 
						|
        picture: req.user.picture,
 | 
						|
        userId: req.user.id,
 | 
						|
        username: req.user.username,
 | 
						|
        email: req.user.email
 | 
						|
      },
 | 
						|
      comments: [],
 | 
						|
      topLevel: false,
 | 
						|
      commentOn: Date.now()
 | 
						|
    });
 | 
						|
    commentSave(comment, findCommentById).subscribe(
 | 
						|
      function() {},
 | 
						|
      next,
 | 
						|
      function() {
 | 
						|
        res.send(true);
 | 
						|
      }
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  function commentEdit(req, res, next) {
 | 
						|
    findCommentById(req.params.id)
 | 
						|
      .doOnNext(function(comment) {
 | 
						|
        if (!req.user && comment.author.userId !== req.user.id) {
 | 
						|
          throw new Error('Not authorized');
 | 
						|
        }
 | 
						|
      })
 | 
						|
      .flatMap(function(comment) {
 | 
						|
        var sanitizedBody = cleanData(req.body.body);
 | 
						|
        if (req.body.body !== sanitizedBody) {
 | 
						|
          req.flash('errors', {
 | 
						|
            msg: 'HTML is not allowed'
 | 
						|
          });
 | 
						|
        }
 | 
						|
        comment.body = sanitizedBody;
 | 
						|
        comment.commentOn = Date.now();
 | 
						|
        return saveInstance(comment);
 | 
						|
      })
 | 
						|
      .subscribe(
 | 
						|
        function() {
 | 
						|
          res.send(true);
 | 
						|
        },
 | 
						|
        next
 | 
						|
      );
 | 
						|
  }
 | 
						|
 | 
						|
  function commentSave(comment, findContextById) {
 | 
						|
    return saveInstance(comment)
 | 
						|
      .flatMap(function(comment) {
 | 
						|
        // Based on the context retrieve the parent
 | 
						|
        // object of the comment (Story/Comment)
 | 
						|
        return findContextById(comment.associatedPost);
 | 
						|
      })
 | 
						|
      .flatMap(function(associatedContext) {
 | 
						|
        if (associatedContext) {
 | 
						|
          associatedContext.comments.push(comment.id);
 | 
						|
        }
 | 
						|
        // NOTE(berks): saveInstance is safe
 | 
						|
        // it will automatically call onNext with null and onCompleted if
 | 
						|
        // argument is falsey or has no method save
 | 
						|
        return saveInstance(associatedContext);
 | 
						|
      })
 | 
						|
      .flatMap(function(associatedContext) {
 | 
						|
        // Find the author of the parent object
 | 
						|
        // if no username
 | 
						|
        var username = associatedContext && associatedContext.author ?
 | 
						|
          associatedContext.author.username :
 | 
						|
          null;
 | 
						|
 | 
						|
        var query = { where: { username: username } };
 | 
						|
        return findOneUser(query);
 | 
						|
      })
 | 
						|
      // if no user is found we don't want to hit the doOnNext
 | 
						|
      // filter here will call onCompleted without running through the following
 | 
						|
      // steps
 | 
						|
      .filter(function(user) {
 | 
						|
        return !!user;
 | 
						|
      })
 | 
						|
      // if this is called user is guarenteed to exits
 | 
						|
      // this is a side effect, hence we use do/tap observable methods
 | 
						|
      .doOnNext(function(user) {
 | 
						|
        // If the emails of both authors differ,
 | 
						|
        // only then proceed with email notification
 | 
						|
        if (
 | 
						|
          comment.author &&
 | 
						|
          comment.author.email &&
 | 
						|
          user.email &&
 | 
						|
          (comment.author.email !== user.email)
 | 
						|
        ) {
 | 
						|
          sendMailWhillyNilly({
 | 
						|
            to: user.email,
 | 
						|
            from: 'Team@freecodecamp.com',
 | 
						|
            subject: comment.author.username +
 | 
						|
            ' replied to your post on Camper News',
 | 
						|
            text: [
 | 
						|
              'Just a quick heads-up: ',
 | 
						|
              comment.author.username,
 | 
						|
              ' replied to you on Camper News.',
 | 
						|
              'You can keep this conversation going.',
 | 
						|
              'Just head back to the discussion here: ',
 | 
						|
              'http://freecodecamp.com/stories/',
 | 
						|
              comment.originalStoryLink,
 | 
						|
              '- the Free Code Camp Volunteer Team'
 | 
						|
            ].join('\n')
 | 
						|
          });
 | 
						|
        }
 | 
						|
      });
 | 
						|
  }
 | 
						|
};
 |