Skip to content

08. Following and blocking other users

Nick Doiron edited this page Dec 6, 2017 · 4 revisions

In a social network, there are two kinds of follows:

  • a symmetric follow (such as a Facebook friendship) where both sides must agree and share a common following-status
  • an asymmetric follow (such as a Twitter follow) where one user follows another (the other user decides whether to follow back)

I'm going to implement an asymmetric follow on 1batch.

There are databases such as Neo4j and OrientDB which are designed around networks and connections between objects, but MongoDB and other databases work in a different way. Instead I create a separate Follow object model.

const mongoose = require('mongoose');

var followSchema = mongoose.Schema({
  start_user_id: String, // Person A
  end_user_id: String,  // follows Person B (and all their photos, by default)
  unfollowed: [String], // array of Person B's photos that I don't care about
  blocked: Boolean
});

module.exports = mongoose.model('Follow', followSchema);

I'm considering a "block" to be a special type of Follow, so that it comes up in my Follow.find({}) search, and that I can easily prevent a new Follow from being created between users who block each other.

I first implemented the Follow button as a mini form which refreshed the page, but refreshing the page is annoying in a mobile web app. It can easily be an AJAX button which sends the follow in the background. Here's what the Pug template has:

input.csrf(type="hidden", value=csrfToken)
input.username(type="hidden", value=user.username)
button.btn.btn-success.follow Follow

I also added the jQuery library so I could make a simple AJAX request:

$(".follow").click(function(e) {
  // is this following or unfollowing?
  // double-check on the click so that this button can toggle
  var makeFollow = ($(e.currentTarget).text() === 'Follow');

  $.post("/follow/" + $(".username").val(), {
    // use that hidden input to fake the form POST
    _csrf: $(".csrf").val(),
    // makeFollow = false gets sent as makeFollow = 'false' .. watch out
    makeFollow: makeFollow
  }, function(response) {
    if (response.status && response.status === 'success') {
      if (makeFollow) {
        $(e.currentTarget).removeClass("btn-success")
          .addClass("btn-danger")
          .text("Unfollow");
      } else {
        $(e.currentTarget).removeClass("btn-danger")
          .addClass("btn-success")
          .text("Follow");
      }
    } else {
      alert("An error occurred");
    }
  });
});

Now the actual follow has to happen on the server-side. We need to make several checks here, too, to make sure we're not following a user who blocked me, or that I'm currently blocking, to make sure I'm not following myself or a user that doesn't exist, or a private user (someone whose name is still their email). Never trust the request from the client.

// follow another user
router.post('/follow/:end_user', async function (ctx) {
  var requser = ctx.req.user;
  if (!requser) {
    // must log in to follow
    return ctx.redirect('/login');
  }
  if (requser.name === ctx.params.end_user) {
    throw 'you can\'t follow yourself';
  }
  if (req.params.end_user.indexOf('@') > -1) {
    throw 'user does not exist';
  }
  // look for a follow object in this direction
  var existing = await Follow.findOne({ start_user_id: req.user.name, end_user_id: req.params.end_user }).exec();
  if (ctx.request.body.makeFollow === 'true') {
    // trying to create follow
    if (existing) {
      // follow already exists
      return printError('you already follow', ctx);
    }

    // looks good for a new follow-ing
    var f = new Follow({
      start_user_id: requser.name,
      end_user_id: req.params.end_user,
      blocked: false
    });
    await f.save();
    ctx.body = { status: 'success' };
  } else {
    // trying to unfollow
    if (!existing) {
      return printError('you already don\'t follow', ctx);
    }
    await Follow.remove({ start_user_id: req.user.name, end_user_id: req.params.end_user, blocked: false });
    ctx.body = { status: 'success' };
  }
});

Clone this wiki locally