Schedule Post to Reddit with Spring – 在Reddit上安排发帖与Spring

最后修改: 2015年 3月 21日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In the earlier parts of this case study, we set up a simple app and an OAuth authentication process with the Reddit API.

在本案例研究的前面的部分中,我们设置了一个简单的应用程序和一个使用 Reddit API 的 OAuth 认证过程。

Let’s now build something useful with Reddit – support for scheduling Posts for latter.

现在让我们用Reddit建立一些有用的东西–支持为后者排定帖子

2. The User and the Post

2.用户和职位

First, let’s create the 2 main entities – the User and the Post. The User will keep track of the username plus some additional Oauth info:

首先,让我们创建两个主要实体–UserPostUser将记录用户名和一些额外的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
}

Next – the Post entity – holding the information necessary for submitting a link to Reddit: title, URL, subreddit, … etc.

接下来 – Post实体 – 持有提交链接到Reddit的必要信息。title, URL, subreddit, …等等。

@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. The Persistence Layer

3.持久层

We’re going to use Spring Data JPA for persistence, so there’s not a whole lot to look at here, other than the well-known interface definitions for our repositories:

我们将使用Spring Data JPA来实现持久化,所以除了众所周知的存储库的接口定义外,这里没有太多可看的东西。

  • UserRepository:
public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(String username);

    User findByAccessToken(String token);
}
  • PostRepository:
public interface PostRepository extends JpaRepository<Post, Long> {

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

    List<Post> findByUser(User user);
}

4. A Scheduler

4.一个调度器

For the scheduling aspects of the app, we’re also going to make good use of the out-of-the-box Spring support.

对于应用程序的日程安排方面,我们也将很好地利用开箱即用的Spring支持。

We’re defining a task to run every minute; this will simply look for Posts that are due to be submitted to Reddit:

我们正在定义一个任务,每分钟运行一次;这将简单地寻找应提交给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);
        }
    }
}

Note that, in case of anything going wrong, the Post will not be marked as sent – so the next cycle will try to submit it again after one minute.

请注意,如果出现任何问题,帖子将不会被标记为发送 – 所以下一个周期将尝试在一分钟后再次提交

5. The Login Process

5.登录过程

With the new User entity, holding some security specific information, we’ll need to modify 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";
}

And loadAuthentication():

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

Note how the user is automatically created if it doesn’t already exist. This makes the “Login with Reddit” process create a local user in the system on the first login.

请注意,如果用户还不存在,它是如何被自动创建的。这使得 “用Reddit登录 “过程在第一次登录时在系统中创建一个本地用户。

6. The Schedule Page

6.日程表页面

Next – let’s take a look at the page that allows scheduling of new Posts:

接下来–让我们看一下允许安排新帖子的页面。

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

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>

Note how we need to check the Captcha. This is because – if the user has less than 10 karma – they can’t schedule a post without filling in the Captcha.

请注意我们需要检查验证码的情况。这是因为–如果用户的业力低于10–他们不填写验证码就不能安排帖子。

7. POSTing

7.发布

When the Schedule form is submitted, the post info is simply validated and persisted to be picked up by the scheduler later on:

当日程表被提交时,帖子信息被简单地验证和持久化,以便稍后被日程安排者接收。

@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. The List of Scheduled Posts

8.计划中的帖子列表

Let’s now implement a simple REST API for retrieving the scheduled posts we have:

现在让我们实现一个简单的REST API,以检索我们所拥有的预定帖子。

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

And a simple, quick way to display these scheduled posts on the front end:

还有一个简单、快速的方法来在前端显示这些预定的帖子

<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. Edit a Scheduled Post

9.编辑一个预定的帖子

Next – let’s see how we can edit a scheduled post.

接下来–让我们看看如何编辑一个预定的帖子。

We’ll start with the front-end – first, the very simple MVC operation:

我们将从前端开始–首先是非常简单的MVC操作。

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

After the simple API, here’s the front end consuming it:

在简单的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>

Now, let’s look at the REST API:

现在,让我们看看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. Unschedule/Delete a Post

10.取消安排/删除一个帖子

We’ll also provide a simple delete operation for any of the scheduled Posts:

我们还将为任何一个预定的帖子提供一个简单的删除操作。

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

Here’s how we call it from client side:

下面是我们如何从客户端调用它。

<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. Conclusion

11.结论

In this part of our Reddit case-study, we built the first non-trivial bit of functionality using the Reddit API – scheduling Posts.

在我们的Reddit案例研究的这一部分,我们使用Reddit API建立了第一个非琐碎的功能–调度帖子。

This is a super-useful feature for a serious Reddit user, especially considering how time-sensitive Reddit submissions are.

对于一个严肃的Reddit用户来说,这是一个超级有用的功能,特别是考虑到Reddit提交的时间是多么的敏感

Next – we’ll build out an even more helpful functionality to help with getting content upvoted on Reddit – machine learning suggestions.

下一步–我们将建立一个更有用的功能,以帮助在Reddit上获得内容的支持–机器学习建议。

The full implementation of this tutorial can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.

本教程的完整实现可以在github 项目中找到 – 这是一个基于 Eclipse 的项目,因此应该可以轻松导入并按原样运行。