I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

Posting to Reddit is a crap shoot. One post may do great and get a whole lot of attention while another, maybe better post will get no love at all. What about keeping an eye on these posts early on and – if they’re not getting enough traction – deleting them quickly and resubmitting them.

In this quick article we’re continuing the Reddit case study by implementing an interesting functionality – delete and resubmit a post if it’s not getting enough attention immediatly.

The simple goal is to allow the user to configure how many votes on Reddit are enough to consider the Post is getting enough traction to leave it up – inside a certain time interval.

2. More Reddit Permissions

First – we’ll need to ask for additional permissions from the Reddit API – specifically we need to edit posts.

So we’ll add “edit” scope to our Reddit Resource:

@Bean
public OAuth2ProtectedResourceDetails reddit() {
    AuthorizationCodeResourceDetails details = 
      new AuthorizationCodeResourceDetails();
    details.setScope(Arrays.asList("identity", "read", "submit", "edit"));
    ...
}

3. The Entity and The Repository

Now – let’s add the extra information in our Post entity:

@Entity
public class Post {
    ...
    private String redditID;
    private int noOfAttempts;
    private int timeInterval;
    private int minScoreRequired;
}

The fields:

  • redditID: Post ID on Reddit, to be used when checking the score and when deleting the post
  • noOfAttempts: Maximum number of resubmit tries (delete Post and resubmit it)
  • timeInterval: Time interval to check if the post is getting enough traction
  • minScoreRequired: Minimum score required to consider it successful enough to leave up

Next – let’s add some new operations into our PostRepository interface – in order to easily retrieve posts when we need to check check them:

public interface PostRepository extends JpaRepository<Post, Long> {

    List<Post> findBySubmissionDateBeforeAndIsSent(Date date, boolean sent);

    List<Post> findByUser(User user);

    List<Post> findByRedditIDNotNullAndNoOfAttemptsGreaterThan(int attempts);
}

4. A New Scheduled Task

Now – let’s define a new task – the re-submit task – into the scheduler:

@Scheduled(fixedRate = 3 * 60 * 1000)
public void checkAndReSubmitPosts() {
    List<Post> submitted = 
      postReopsitory.findByRedditIDNotNullAndNoOfAttemptsGreaterThan(0);
    for (Post post : submitted) {
        checkAndReSubmit(post);
    }
}

Each few minutes, this is simply iterating over the posts that are still in play – and, if they’re not getting enough traction, it deletes them and submits them again.

And here is checkAndReSubmit() method:

private void checkAndReSubmit(Post post) {
    try {
        checkAndReSubmitInternal(post);
    } catch (final Exception e) {
        logger.error("Error occurred while check post " + post.toString(), e);
    }
}
private void checkAndReSubmitInternal(Post post) {
    if (didIntervalPassed(post.getSubmissionDate(), post.getTimeInterval())) {
        int score = getPostScore(post.getRedditID());
        if (score < post.getMinScoreRequired()) {
            deletePost(post.getRedditID());
            resetPost(post);
        } else {
            post.setNoOfAttempts(0);
            postReopsitory.save(post);
        }
    }
}
private boolean didIntervalPassed(Date submissonDate, int postInterval) {
    long currentTime = new Date().getTime();
    long interval = currentTime - submissonDate.getTime();
    long intervalInMinutes = TimeUnit.MINUTES.convert(interval, TimeUnit.MILLISECONDS);
    return intervalInMinutes > postInterval;
}
private void resetPost(Post post) {
    long time = new Date().getTime();
    time += TimeUnit.MILLISECONDS.convert(post.getTimeInterval(), TimeUnit.MINUTES);
    post.setRedditID(null);
    post.setSubmissionDate(new Date(time));
    post.setSent(false);
    post.setSubmissionResponse("Not sent yet");
    postReopsitory.save(post);
}

We’ll also need to keep track of the redditID when first submitting it to Reddit:

private void submitPostInternal(Post post) {
    ...
    JsonNode node = redditRestTemplate.postForObject(
      "https://oauth.reddit.com/api/submit", param, JsonNode.class);
    JsonNode errorNode = node.get("json").get("errors").get(0);
    if (errorNode == null) {
        post.setRedditID(node.get("json").get("data").get("id").asText());
        post.setNoOfAttempts(post.getNoOfAttempts() - 1);
        ...
}

The logic here is also quite simple – we just save the id and decrease the number of attempts counter.

5. Get Reddit Post score

Now – let’s see how we can get the current score of the post from Reddit:

private int getPostScore(String redditId) {
    JsonNode node = redditRestTemplate.getForObject(
      "https://oauth.reddit.com/api/info?id=t3_" + redditId, JsonNode.class);
    int score = node.get("data").get("children").get(0).get("data").get("score").asInt();
    return score;
}

Note that:

  • We need the “read” scope when retrieving the post information from Reddit
  • We add “t3_” to the reddit id to obtain full name of the post

6. Delete The Reddit Post

Next – let’s see how delete Reddit post using its id:

private void deletePost(String redditId) {
    MultiValueMap<String, String> param = new LinkedMultiValueMap<String, String>();
    param.add("id", "t3_" + redditId);
    redditRestTemplate.postForObject(
      "https://oauth.reddit.com/api/del.json", param, JsonNode.class);
}

7. The RedditController

Now – let’s add the new info to the controller:

@RequestMapping(value = "/schedule", method = RequestMethod.POST)
public String schedule(Model model, 
  @RequestParam Map<String, String> formParams) throws ParseException {
    Post post = new Post();
    post.setTitle(formParams.get("title"));
    post.setSubreddit(formParams.get("sr"));
    post.setUrl(formParams.get("url"));
    post.setNoOfAttempts(Integer.parseInt(formParams.get("attempt")));
    post.setTimeInterval(Integer.parseInt(formParams.get("interval")));
    post.setMinScoreRequired(Integer.parseInt(formParams.get("score")));
    ....
}

8. The UI – Configure the Rules

Finally – let’s modify our very simple schedule form to add resubmit the new settings:

<label class="col-sm-3">Resubmit Settings</label>

<label>Number of Attempts</label> 
<select name="attempt">
    <option value="0" selected>None</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
</select>

<label>Time interval</label>
<select name="interval">
    <option value="0" selected >None</option>
    <option value="45">45 minutes</option>
    <option value="60">1 hour</option>
    <option value="120">2 hours</option>
</select>

<label>Min score</label>
<input type="number"value="0" name="score" required/>

9. Conclusion

We’re continuing to improve what this simple app can do – we can now post to Reddit and – if the post isn’t getting enough traction quickly – we can have the system delete it and re-post to give it a better chance of performing.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS