Building a Dating site with Neo4j – Part Six

Without posts, we can’t have High Fives and that defeats the purpose of our dating site, so it’s time to let our users post things. We want to allow two types of posts, text posts and image posts. Today we’re going to focus on text posts and getting them working and we’ll deal with images in another post. The first thing we want to do is prevent users from posting bad things. So we’re going to create a PostValidator to deal with the user input:

 
    @POST
    public Response createPost(String body, @PathParam("username") final String username,
                               @Context GraphDatabaseService db) throws IOException {
        Map<String, Object> results;
        HashMap<String, Object> input = PostValidator.validate(body);

In our validate method we will check for the usual things and then use Jsoup.clean to only allow simpleText for now. We are preventing XSS type attack and also don’t want to allow any outbound links. It’s not a publishing/advertising platform like Twitter, it’s meant to keep content within.

 
        if (!input.containsKey(STATUS)) {
            throw PostExceptions.missingStatusParameter;
        } else {
            String status = (String)input.get(STATUS);
            if (status.equals("")) {
                throw PostExceptions.emptyStatusParameter;
            } else {
                input.put(STATUS, Jsoup.clean(status, Whitelist.simpleText()));
            }
        }

After our post is validated, we begin a transaction, find the posting user, create the post, get combined properties of the post and poster and return that as our result. Since the post is brand new, it has no high fives, low fives so we don’t bother checking.

 
        ZonedDateTime dateTime = ZonedDateTime.now(utc);

        try (Transaction tx = db.beginTx()) {
            Node user = Users.findUser(username, db);
            Node post = createPost(db, input, user, dateTime);
            results = post.getAllProperties();
            results.put(USERNAME, username);
            results.put(NAME, user.getProperty(NAME));
            results.put(HASH, user.getProperty(HASH));
            results.put(HIGH_FIVED, false);
            results.put(LOW_FIVED, false);
            results.put(HIGH_FIVES, 0);
            results.put(LOW_FIVES, 0);

            tx.success();
        }
        return Response.ok().entity(objectMapper.writeValueAsString(results)).build();

The createPost method is below. We are setting the status and post creating time, as well as connecting the post to the user by a “dated” relationship type that is generated dynamically from the date it was created. Re-read this blog post on building a Twitter clone if you forgot why we do this.

 
    private Node createPost(@Context GraphDatabaseService db, HashMap input, Node user, ZonedDateTime dateTime) {
        Node post = db.createNode(Labels.Post);
        post.setProperty(STATUS, input.get("status"));
        post.setProperty(TIME, dateTime);
        Relationship r1 = user.createRelationshipTo(post, RelationshipType.withName("POSTED_ON_" +
                        dateTime.format(dateFormatter)));
        r1.setProperty(TIME, dateTime);
        Tags.createTags(post, input, dateTime, db);
        Mentions.createMentions(post, input, dateTime, db);
        return post;
    }

In this method we are also creating tags. This is mimicking the Twitter HashTag functionality which I’m not sure if we really need but once a UX pattern has been established by a popular site you kind of want to continue it so your users feel an instant familiarity with your site as well. Any word starting with a “#” will be found by our pattern. We will be using this method on the creating and updating of posts, so we will go ahead and clean any existing tags it may have:

 
private static final Pattern hashtagPattern = Pattern.compile("#(\\S+)");

public static void createTags(Node post, HashMap<String, Object> input, ZonedDateTime dateTime, GraphDatabaseService db) {
    Matcher mat = hashtagPattern.matcher(((String)input.get(STATUS)).toLowerCase());
    for (Relationship r1 : post.getRelationships(Direction.OUTGOING, RelationshipType.withName("TAGGED_ON_" +
            dateTime.format(dateFormatter)))) {
        r1.delete();
    }

With our Post node now tag-free, will iterate over any found tags, find or create the tag nodes and create dated relationships once again to our post. One benefit of the dated relationships on tags is that it lets us quickly see which tags are/were trending on any particular day.

 
        Set<Node> tagged = new HashSet<>();
        while (mat.find()) {
            String tag = mat.group(1);
            Node hashtag = db.findNode(Labels.Tag, NAME, tag);
            if (hashtag == null) {
                hashtag = db.createNode(Labels.Tag);
                hashtag.setProperty(NAME, tag);
                hashtag.setProperty(TIME, dateTime);
            }
            if (!tagged.contains(hashtag)) {
                post.createRelationshipTo(hashtag, RelationshipType.withName("TAGGED_ON_" +
                        dateTime.format(dateFormatter)));
                tagged.add(hashtag);
            }
        }

In our createPost method we also checked for Mentions. It follows a similar pattern as tags, except we don’t create users if they don’t already exist. The code is here if you want to take a look. I’m not sure what users will do with Mentions, it could be for things like going on a double date with someone or complaining that someone is a bad kisser.

Now that our users can create posts, we need to see them. But we are using dated relationship types, so we can’t just ask for the “POSTED” relationship, so what do we do instead? We have to generate the relationship types dynamically. We start our method checking a parameter to see if we want current posts or from some previous time.

 
    @GET
    public Response getPosts(@PathParam("username") final String username,
                             @QueryParam("limit") @DefaultValue("25") final Integer limit,
                             @QueryParam("since") final String since,
                             @QueryParam("username2") final String username2,
                             @Context GraphDatabaseService db) throws IOException {
        ArrayList<Map<String, Object>> results = new ArrayList<>();
        ZonedDateTime latest;
        if (since == null) {
            latest = ZonedDateTime.now(utc);
        } else {
            latest = ZonedDateTime.parse(since);
        }

Next, we find our user node in the graph and ask Neo4j for that node’s relationship types. One of the neat things about Neo4j is that every node knows what types of relationships are connected to it at all times. From this list, we want to just get the relationship types that start with POSTED_ON and we want to sort them in reverse order.

 
Node user = Users.findUser(username, db);
ArrayList<RelationshipType> types = new ArrayList<>();
user.getRelationshipTypes().forEach(t-> {
if (t.name().startsWith("POSTED_ON")) {
    types.add(t);
}
        });
types.sort(relTypeComparator);

Neo4j doesn’t know how to sort relationship types, but we can create a Comparator to do just that:

 
private static final Comparator<RelationshipType> relTypeComparator =
     Comparator.comparing((Function<RelationshipType, Object>) RelationshipType::name, reverseOrder());

Now we can get our posts and return them to the user.

 
            for (RelationshipType relType : types) {
                if (count >= limit) { break;}
                for (Relationship r1 : user.getRelationships(Direction.OUTGOING, relType)) {
                    Node post = r1.getEndNode();
                    Map<String, Object> result = post.getAllProperties();
                    ZonedDateTime time = (ZonedDateTime)r1.getProperty("time");
                    if(time.isBefore(latest)) {
                        result.put(TIME, time);
                        result.put(USERNAME, username);
                        result.put(NAME, userProperties.get(NAME));
                        result.put(HASH, userProperties.get(HASH));
                        result.put(HIGH_FIVED, highFived.contains(post));
                        result.put(LOW_FIVED, lowFived.contains(post));
                        result.put(HIGH_FIVES, post.getDegree(RelationshipTypes.HIGH_FIVED, Direction.INCOMING));
                        result.put(LOW_FIVES, post.getDegree(RelationshipTypes.LOW_FIVED, Direction.INCOMING));

                        results.add(result);
                        count++;
                    }
                }
            }
            tx.success();

We can integrate this endpoint into our API with simply:

 
    @GET("users/{username}/posts")
    Call<List<Post>> getPosts(@Path("username") String username);

…and request the posts on our user page:

 
      get("/user/{username}", req -> {
          String requested_by = req.get("requested_by");
          if (requested_by.equals("anonymous")) requested_by = null;
          User authenticated = getUserProfile(requested_by);

          Response<User> userResponse = api.getProfile(req.param("username").value(), requested_by).execute();
          if (userResponse.isSuccessful()) {
              User user = userResponse.body();

              Response<List<Post>> postsResponse = api.getPosts(req.param("username").value()).execute();
              List<Post> posts = new ArrayList<>();
              if (postsResponse.isSuccessful()) {
                  posts = postsResponse.body();
              }

              return views.user.template(authenticated, user, posts, getTags());
          } else {
              throw new Err(Status.BAD_REQUEST);
          }
      });

I’ll spare you the html and css, but it ends up looking like this:

So our users can see what they themselves have posted, or if they known another username they can see their posts, but that’s not super useful. What our users want to see is a streaming timeline of posts from the people they are interested in dating. We will tackle that next.

Tagged , , , , , , , ,

One thought on “Building a Dating site with Neo4j – Part Six

  1. […] De Marzi continues with his Building a Dating Site series of blog posts. In part 6 he builds the functionality that lets users post things to the site and in part 7 creates the user […]

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 )

Facebook photo

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

Connecting to %s

%d bloggers like this: