Building a Dating site with Neo4j – Part Eight

Up to this point we have a timeline of posts from people we want to date, but no way to interact with those people. The first step begins today as we will allow users to High Five and Low Five posts. Recall that once a user has high fived your post, you will be able to message them for up to 5 days when the high five expires. If you do not wish to message them, that’s fine, their high five gives you an additional high five to give to someone else in the hopes they message you. Remember that all users get 5 “free” High Fives a day, if they want more they have to earn them. You can get a High Five on a post that is older than 5 days, it still counts. This is needed to create the opportunity to bring back a user who hasn’t been to the dating site in a while with a High Five to an old Post. Otherwise after 5 days of inactivity, those users would be practically deleted.

Let’s start our method off. We’ll need the two users interacting as well as the post that is getting the high five. I tried using just the time of the post instead of leaking the post id, but theoretically somebody could have two posts at the same time and I had to deal with converting from a ZonedDateTime to String and back and decided it was easier to just use the node id.

 
@Path("/users/{username}/high_fives")
public class HighFives {
    private static final ObjectMapper objectMapper = CustomObjectMapper.getInstance();

    @POST
    @Path("/{username2}/{postId}")
    public Response createFive(String body, @PathParam("username") final String username,
                                @PathParam("username2") final String username2,
                                @PathParam("postId") final Long postId,
                                @Context GraphDatabaseService db) throws IOException {

Now we find the user, and the timezone they are in. We want to reset the 5 free high fives at the start of their day, wherever they are. We are adding timezone as a property of the user node when they register, by pulling it from the location they choose. We could have just traversed to it via their location, but we may want to allow users to have more than one location in the future.

 
            Node user = Users.findUser(username, db);
            ZoneId zoneId = ZoneId.of((String)user.getProperty(TIMEZONE));
            ZonedDateTime startOfDay = ZonedDateTime.now(zoneId).with(LocalTime.MIN);

We next find all of their posts and check for high fives that came in after the start of their day. We can do this by getting all the relationship types connected to the user node, collecting just the POSTED_ON and passing that in to getRelationships in an array.

 
            int high5received = 0;
            ArrayList<RelationshipType> types = new ArrayList<>();
            for (RelationshipType t : user.getRelationshipTypes()) {
                if (t.name().startsWith("POSTED_ON")) {
                    types.add(t);
                }
            }
            for (Relationship r1 : user.getRelationships(types.toArray(new RelationshipType[0]))) {
                Node post = r1.getEndNode();
                for (Relationship r : post.getRelationships(RelationshipTypes.HIGH_FIVED, Direction.INCOMING)) {
                    ZonedDateTime when = (ZonedDateTime) r.getProperty(TIME);
                    if (when.isAfter(startOfDay)) {
                        high5received++;
                    }
                }
            }

Next we need to know how many high fives they have already given out. They can’t high five a post twice, so we check for that as well. Unlike a swipe right/swipe left from Tinder, a user doesn’t have unlimited High Fives, so we don’t have to worry too much about users with a million high fives since they only get and earn a handful per day. If things did get out of control, we could expire the high fives by either deleting them or changing the relationship type to something else like “PAST_HIGH_FIVE”. This effectively deletes the old relationship and creates a new one, since there is no real way to change the type of a relationship in Neo4j due to the way they are stored in dual linked lists by type.

 
            int high5given = 0;
            Node user2 = Users.findUser(username2, db);
            for (Relationship r : user.getRelationships(RelationshipTypes.HIGH_FIVED, Direction.OUTGOING)) {
                if (r.getEndNodeId() == postId) {
                    throw FiveExceptions.alreadyHighFivedPost;
                }
                ZonedDateTime when = (ZonedDateTime)r.getProperty(TIME);
                if (when.isAfter(startOfDay)) {
                    high5given++;
                }
            }

Now that we have the counts, we can check to see if they are over the limit. We could change the “5” to be a property of the user, say we wanted to have paid accounts that get 55 high fives instead of just 5 per day. We haven’t talked about a monetization strategy yet, but it could be one of them.

 
            if (high5given - 5 >= high5received) {
                throw FiveExceptions.overHighFiveLimit;
            }

Finally we find the post and create the High Five relationship. Here I’m deciding to return the updated post, but we could just return a “201” http return code.

 
            Node post = db.getNodeById(postId);
            Relationship r2 = user.createRelationshipTo(post, RelationshipTypes.HIGH_FIVED);
            r2.setProperty(TIME, dateTime);

            results = post.getAllProperties();
            results.put(TIME, dateTime);
            results.put(USERNAME, username2);
            results.put(NAME, user2.getProperty(NAME));
            results.put(HIGH_FIVED, true);
            results.put(LOW_FIVED, false);
            results.put(HIGH_FIVES, post.getDegree(RelationshipTypes.HIGH_FIVED, Direction.INCOMING));
            results.put(LOW_FIVES, post.getDegree(RelationshipTypes.LOW_FIVED, Direction.INCOMING));

That’s the back-end, now moving to the front end. First we will need to add this to our API:

 
    @POST("users/{username}/high_fives/{username2}/{id}")
    Call<Post> createHighFive(@Path("username") String username,
                           @Path("username2") String username2,
                           @Path("id") Long id);

We can now call this api method in our application:

 
        post("/high_five", req -> {
            CommonProfile profile = require(CommonProfile.class);
            String username = profile.getUsername();
            Response<Post> response = App.api.createHighFive(username, req.param("username2").value(), req.param("id").longValue()).execute();

            if (response.isSuccessful()) {
                return response.body();
            } else {
                throw new Err(Status.BAD_REQUEST);
            }
        }).produces("json");

We’ll use a little html and javascript magic to put it all together and now our users can high five:

The low fives are exactly the same so I won’t repeat all that. While we’re here, we also want to let users Block people who post acrid remarks and eventually “penis portraits” or the like. To create this functionality we will find the users in question in the graph.

 
    @POST
    @Path("/{username2}")
    public Response createBlocks(@PathParam("username") final String username,
                                  @PathParam("username2") final String username2,
                                  @Context GraphDatabaseService db) throws IOException {
        Map<String, Object> results =  new HashMap<>();
        try (Transaction tx = db.beginTx()) {
            Node user = findUser(username, db);
            Node user2 = findUser(username2, db);

Before we create the BLOCKS relationship we want to check to see if the user is already blocked. We do this by finding the node with the smallest BLOCKS degree, traversing those relationships and comparing them to the other user.

 
            if (user.getDegree(RelationshipTypes.BLOCKS, Direction.OUTGOING)
                    < user2.getDegree(RelationshipTypes.BLOCKS, Direction.INCOMING) ) {
                for (Relationship r1 : user.getRelationships(Direction.OUTGOING, RelationshipTypes.BLOCKS) ) {
                    if (r1.getEndNode().equals(user2)) {
                        throw BlockExceptions.alreadyBlockingUser;
                    }
                }
            } else {
                for (Relationship r1 : user2.getRelationships(Direction.INCOMING, RelationshipTypes.BLOCKS)) {
                    if (r1.getStartNode().equals(user)) {
                        throw BlockExceptions.alreadyBlockingUser;
                    }
                }
           }

Assuming they aren’t already blocked, we can create the BLOCKS relationship, set the time and return our results.

 
            Relationship blocks = user.createRelationshipTo(user2, RelationshipTypes.BLOCKS);
            blocks.setProperty(TIME, ZonedDateTime.now(utc));

            results.put(USERNAME, username2);

On our front end, we add this to our API:

 
    @POST("users/{username}/blocks/{username2}/")
    Call<User> createBlocks(@Path("username") String username,
                            @Path("username2") String username2);

…and include it in our application.

 
        post("block", req -> {
            CommonProfile profile = require(CommonProfile.class);
            String username = profile.getUsername();
            Response<User> response = App.api.createBlocks(username, req.param("username2").value()).execute();

            if (response.isSuccessful()) {
                return response.body();
            } else {
                throw new Err(Status.BAD_REQUEST);
            }
        }).produces("json");

We’ll use a very old school technique of using an “onclick” event on the button to call a function that posts this request and hides the bad post from our timeline.

 
function block(id) {
    $("#block_"+id).submit(function(e){
        e.preventDefault(e);
    });

    $.post('block', $("#block_"+id).serialize());
    $("#post_"+id).hide();
}

It feels like things are moving along, but the person receiving the high fives and low fives has no idea they received one. We should probably work on that next.

Tagged , , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: