Programmer une publication sur Reddit avec Spring

Planifier la publication sur Reddit avec Spring

1. Vue d'ensemble

Dans lesearlierparts de cette étude de cas, nous avons mis en place une application simple et un processus d'authentification OAuth avec l'API Reddit.

Construisons maintenant quelque chose d'utile avec Reddit - support pourscheduling Posts pour ce dernier.

2. LesUser et la poste

Commençons par créer les 2 entités principales - lesUser et lesPost. LesUser garderont une trace du nom d'utilisateur ainsi que des informations Oauth supplémentaires:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private String username;

    private String accessToken;
    private String refreshToken;
    private Date tokenExpiration;

    private boolean needCaptcha;

    // standard setters and getters
}

Ensuite - l'entitéPost - contenant les informations nécessaires pour soumettre un lien vers Reddit:title,URL,subreddit,… etc.

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false) private String title;
    @Column(nullable = false) private String subreddit;
    @Column(nullable = false) private String url;
    private boolean sendReplies;

    @Column(nullable = false) private Date submissionDate;

    private boolean isSent;

    private String submissionResponse;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    // standard setters and getters
}

3. La couche de persistance

Nous allons utiliserSpring Data JPA for persistence, il n'y a donc pas grand chose à regarder ici, à part les définitions d'interface bien connues de nos dépôts:

  • UserRepository:

public interface UserRepository extends JpaRepository {

    User findByUsername(String username);

    User findByAccessToken(String token);
}
  • PostRepository:

public interface PostRepository extends JpaRepository {

    List findBySubmissionDateBeforeAndIsSent(Date date, boolean isSent);

    List findByUser(User user);
}

4. Un planificateur

Pour les aspects de planification de l'application, nous allons également faire bon usage du support Spring prêt à l'emploi.

Nous définissons une tâche à exécuter toutes les minutes; cela cherchera simplementPosts that are due to be submitted sur Reddit:

public class ScheduledTasks {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private OAuth2RestTemplate redditRestTemplate;

    @Autowired
    private PostRepository postReopsitory;

    @Scheduled(fixedRate = 1 * 60 * 1000)
    public void reportCurrentTime() {
        List posts =
          postReopsitory.findBySubmissionDateBeforeAndIsSent(new Date(), false);
        for (Post post : posts) {
            submitPost(post);
        }
    }

    private void submitPost(Post post) {
        try {
            User user = post.getUser();
            DefaultOAuth2AccessToken token =
              new DefaultOAuth2AccessToken(user.getAccessToken());
            token.setRefreshToken(new DefaultOAuth2RefreshToken((user.getRefreshToken())));
            token.setExpiration(user.getTokenExpiration());
            redditRestTemplate.getOAuth2ClientContext().setAccessToken(token);

            UsernamePasswordAuthenticationToken userAuthToken =
              new UsernamePasswordAuthenticationToken(
              user.getUsername(), token.getValue(),
              Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
            SecurityContextHolder.getContext().setAuthentication(userAuthToken);

            MultiValueMap param = new LinkedMultiValueMap();
            param.add("api_type", "json");
            param.add("kind", "link");
            param.add("resubmit", "true");
            param.add("then", "comments");
            param.add("title", post.getTitle());
            param.add("sr", post.getSubreddit());
            param.add("url", post.getUrl());
            if (post.isSendReplies()) {
                param.add(RedditApiConstants.SENDREPLIES, "true");
            }

            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.setSent(true);
                post.setSubmissionResponse("Successfully sent");
                postReopsitory.save(post);
            } else {
                post.setSubmissionResponse(errorNode.toString());
                postReopsitory.save(post);
            }
        } catch (Exception e) {
            logger.error("Error occurred", e);
        }
    }
}

Notez qu'en cas de problème, le message ne sera pas marqué commesent - doncthe next cycle will try to submit it again after one minute.

5. Le processus de connexion

Avec la nouvelle entité Utilisateur, contenant des informations spécifiques à la sécurité, nous devronsmodify our simple login process to store that information:

@RequestMapping("/login")
public String redditLogin() {
    JsonNode node = redditRestTemplate.getForObject(
      "https://oauth.reddit.com/api/v1/me", JsonNode.class);
    loadAuthentication(node.get("name").asText(), redditRestTemplate.getAccessToken());
    return "redirect:home.html";
}

EtloadAuthentication():

private void loadAuthentication(String name, OAuth2AccessToken token) {
    User user = userReopsitory.findByUsername(name);
    if (user == null) {
        user = new User();
        user.setUsername(name);
    }

    if (needsCaptcha().equalsIgnoreCase("true")) {
        user.setNeedCaptcha(true);
    } else {
        user.setNeedCaptcha(false);
    }

    user.setAccessToken(token.getValue());
    user.setRefreshToken(token.getRefreshToken().getValue());
    user.setTokenExpiration(token.getExpiration());
    userReopsitory.save(user);

    UsernamePasswordAuthenticationToken auth =
      new UsernamePasswordAuthenticationToken(user, token.getValue(),
      Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
    SecurityContextHolder.getContext().setAuthentication(auth);
}

Notez comment l'utilisateur est automatiquement créé s'il n'existe pas déjà. Cela permet au processus «Connexion avec Reddit» de créer un utilisateur local dans le système lors de la première connexion.

6. La page Calendrier

Ensuite, jetons un œil à la page qui permet la planification de nouveaux messages:

@RequestMapping("/postSchedule")
public String showSchedulePostForm(Model model) {
    boolean isCaptchaNeeded = getCurrentUser().isCaptchaNeeded();
    if (isCaptchaNeeded) {
        model.addAttribute("msg", "Sorry, You do not have enought karma");
        return "submissionResponse";
    }
    return "schedulePostForm";
}
private User getCurrentUser() {
    return (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

schedulePostForm.html:

Notez comment nous devons vérifier le Captcha. C'est parce que -if the user has less than 10 karma - ils ne peuvent pas planifier une publication sans remplir le Captcha.

7. Affectation

Lorsque le formulaire de planification est soumis,the post info is simply validated and persisted à récupérer ultérieurement par le planificateur:

@RequestMapping(value = "/api/scheduledPosts", method = RequestMethod.POST)
@ResponseBody
public Post schedule(@RequestBody Post post) {
    if (submissionDate.before(new Date())) {
        throw new InvalidDateException("Scheduling Date already passed");
    }

    post.setUser(getCurrentUser());
    post.setSubmissionResponse("Not sent yet");
    return postReopsitory.save(post);
}

8. La liste des messages programmés

Implémentons maintenant une API REST simple pour récupérer les messages programmés que nous avons:

@RequestMapping(value = "/api/scheduledPosts")
@ResponseBody
public List getScheduledPosts() {
    User user = getCurrentUser();
    return postReopsitory.findByUser(user);
}

Et un moyen simple et rapide d'accéder àdisplay these scheduled posts on the front end:

Post titleSubmission Date

9. Modifier une publication programmée

Ensuite, voyons comment nous pouvons modifier un message programmé.

Nous allons commencer par le front-end - tout d'abord, l'opération MVC très simple:

@RequestMapping(value = "/editPost/{id}", method = RequestMethod.GET)
public String showEditPostForm() {
    return "editPostForm";
}

Après l'API simple, voici le frontal qui la consomme:

Voyons maintenant lesREST API:

@RequestMapping(value = "/api/scheduledPosts/{id}", method = RequestMethod.GET)
@ResponseBody
public Post getPost(@PathVariable("id") Long id) {
    return postReopsitory.findOne(id);
}

@RequestMapping(value = "/api/scheduledPosts/{id}", method = RequestMethod.PUT)
@ResponseStatus(HttpStatus.OK)
public void updatePost(@RequestBody Post post, @PathVariable Long id) {
    if (post.getSubmissionDate().before(new Date())) {
        throw new InvalidDateException("Scheduling Date already passed");
    }
    post.setUser(getCurrentUser());
    postReopsitory.save(post);
}

10. Unschedule/Delete a Post

Nous allons également proposer une opération de suppression simple pour l'un des posts programmés:

@RequestMapping(value = "/api/scheduledPosts/{id}", method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus.OK)
public void deletePost(@PathVariable("id") Long id) {
    postReopsitory.delete(id);
}

Voici comment nous l'appelons du côté client:

Delete

11. Conclusion

Dans cette partie de notre étude de cas Reddit, nous avons créé le premier élément de fonctionnalité non trivial à l'aide de l'API Reddit - la planification de publications.

C'est une fonctionnalité très utile pour un utilisateur sérieux de Reddit, en particulier compte tenu dehow time-sensitive Reddit submissions are.

Ensuite, nous allons créer une fonctionnalité encore plus utile pour vous aider à faire voter le contenu sur Reddit - des suggestions d'apprentissage automatique.

Lesfull implementation de ce didacticiel se trouvent dansthe github project - il s'agit d'un projet basé sur Eclipse, il devrait donc être facile à importer et à exécuter tel quel.