SpringでRedditに投稿をスケジュールする

1概要

このケーススタディの/spring-security-oauth2-authentication-with-reddit[前へ]リンク:/spring-security-oauth-post-to-reddit[部分]では、簡単なアプリとOAuthを設定しました。 Reddit APIによる認証プロセス。

今度はRedditで何か便利なものを作りましょう - 後者のための ポストスケジューリング のサポート。

2 ユーザー と投稿

まず、 User Post の2つの主要エンティティを作成しましょう。 User は、ユーザー名と追加のOauth情報を追跡します。

@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
}

次に - Post エンティティ - Redditへのリンクの送信に必要な情報を保持します。

@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持続層

私たちは永続性のためにSpring Data JPAを使用するつもりです、それで私たちのリポジトリのためのよく知られているインターフェース定義以外に、ここで見ることは全然多くありません:

  • UserRepository:

public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(String username);

    User findByAccessToken(String token);
}
  • 投稿リポジトリ:

public interface PostRepository extends JpaRepository<Post, Long> {

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

    List<Post> findByUser(User user);
}

4スケジューラ

アプリのスケジューリングの面では、すぐに使えるSpringサポートも活用します。

毎分実行するタスクを定義しています。これは単に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<Post> 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<String, String> param = new LinkedMultiValueMap<String, String>();
            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);
        }
    }
}

何か問題が発生した場合、Postは sent としてマークされないので、次のサイクルは1分後にもう一度送信しようとします。

5ログインプロセス

セキュリティ固有の情報を保持している新しいUserエンティティで、その情報を保存するように** 簡単なログインプロセスを変更する必要があります。

@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";
}

そして loadAuthentication() :

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);
}

ユーザーが存在しない場合は、ユーザーが自動的に作成される方法に注意してください。

これにより、「Login with Reddit」プロセスは最初のログイン時にシステムにローカルユーザーを作成します。

6. スケジュールページ

次に、新しい投稿のスケジュールを設定できるページを見てみましょう。

@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 :**

<form>
    <input name="title"/>
    <input name="url"/>
    <input name="subreddit"/>
    <input type="checkbox" name="sendreplies" value="true"/>
    <input name="submissionDate">
    <button type="submit" onclick="schedulePost()">Schedule</button>
</form>

<script>
function schedulePost(){
    var data = {};
    $('form').serializeArray().map(function(x){data[x.name]= x.value;});
    $.ajax({
        url: 'api/scheduledPosts',
        data: JSON.stringify(data),
        type: 'POST',
        contentType:'application/json',
        success: function(result) { window.location.href="scheduledPosts"; },
        error: function(error) { alert(error.responseText); }
    });
}
</script>
</body>
</html>

Captchaをチェックする必要があることに注意してください。これは、 - ユーザーのカルマ数が10未満の場合 - Captchaに入力しないと投稿をスケジュールできないためです。

7. POST

Scheduleフォームが送信されると、 投稿情報は単に検証されて永続化され、 後でスケジューラによって取得されます。

@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予定投稿一覧

スケジュールされた投稿を取得するための簡単なREST APIを実装しましょう。

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

そして、これらの予定されている投稿をフロントエンドに** 表示するための簡単で素早い方法:

<table>
    <thead><tr><th>Post title</th><th>Submission Date</th></tr></thead>
</table>

<script>
$(function(){
    $.get("api/scheduledPosts", function(data){
        $.each(data, function( index, post ) {
            $('.table').append('<tr><td>'+post.title+'</td><td>'+
              post.submissionDate+'</td></tr>');
        });
    });
});
</script>

9予定投稿を編集する

次に - スケジュールされた投稿を編集する方法を見てみましょう。

まずフロントエンドから始めましょう - まず、非常に単純なMVC操作です。

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

単純なAPIの後に、フロントエンドがそれを使います:

<form>
    <input type="hidden" name="id"/>
    <input name="title"/>
    <input name="url"/>
    <input name="subreddit"/>
    <input type="checkbox" name="sendReplies" value="true"/>
    <input name="submissionDate">
    <button type="submit" onclick="editPost()">Save Changes</button>
</form>

<script>
$(function() {
   loadPost();
});

function loadPost(){
    var arr = window.location.href.split("/");
    var id = arr[arr.length-1];
    $.get("../api/scheduledPosts/"+id, function (data){
        $.each(data, function(key, value) {
            $('** [name="'+key+'"]').val(value);
        });
    });
}
function editPost(){
    var id = $("#id").val();
    var data = {};
    $('form').serializeArray().map(function(x){data[x.name]= x.value;});
    $.ajax({
            url: "../api/scheduledPosts/"+id,
            data: JSON.stringify(data),
            type: 'PUT',
            contentType:'application/json'
            }).done(function() {
                window.location.href="../scheduledPosts";
            }).fail(function(error) {
                alert(error.responseText);
        });
}
</script>

それでは、 REST 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投稿のスケジュール解除/削除

また、スケジュールされた投稿のいずれに対しても簡単な削除操作を提供します。

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

クライアント側から呼び出す方法は次のとおりです。

<a href="#" onclick="confirmDelete(${post.getId()})">Delete</a>

<script>
function confirmDelete(id) {
    if (confirm("Do you really want to delete this post?") == true) {
        deletePost(id);
    }
}

function deletePost(id){
    $.ajax({
        url: 'api/scheduledPosts/'+id,
        type: 'DELETE',
        success: function(result) {
            window.location.href="scheduledPosts"
        }
    });
}
</script>

11結論

Redditのケーススタディのこの部分では、Reddit API - スケジューリング投稿を使用して、最初の重要でない機能を構築しました。

これは、深刻なRedditユーザにとっては非常に便利な機能です。特に、時間に敏感なRedditによる投稿がどれほどあるのかを考えると**

次に、Redditで機械学習の提案についてコンテンツを支持するのに役立つ、さらに役立つ機能を構築します。

このチュートリアルの 完全な実装 はhttps://github.com/eugenp/reddit-app/[githubプロジェクト]にあります - これはEclipseベースのプロジェクトなので、インポートおよび実行は簡単ですです。

前の投稿:Mockito 2による遅延検証
次の投稿:グアバテーブルへのガイド