知識庫 / Spring RSS 訂閱

使用Spring發佈Reddit帖子

Security,Spring
HongKong
4
02:56 PM · Dec 06 ,2025

1. 概述

在本文檔的早期部分,我們已經搭建了一個簡單的應用程序和一個使用 Reddit API 的 OAuth 認證流程。

現在,讓我們使用 Reddit 功能構建一個有用的應用程序——支持為稍後安排發佈帖子的功能。

2. 用户與帖子

首先,讓我們創建兩個主要實體:用户帖子用户 將跟蹤用户名以及一些額外的 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所需的信息:標題URL子版塊……等。

@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);
}
<ul>
  <li><strong <em>PostRepository:</em></strong></li>
</ul>
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);
        }
    }
}

請注意,如果出現任何問題,帖子將不會被標記為已發送——因此下一次循環將在一分鐘後嘗試重新提交

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

並且加載身份驗證:

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

請注意,如果用户不存在,系統會自動創建該用户。這使得“使用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。這是因為——如果用户Karma值小於10——他們無法在填寫 Captcha 之前安排帖子。

7. 發送請求 (POSTing)

當日程表表單提交時,請求信息僅會被驗證並持久化,以便調度器稍後獲取:

@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 構建了第一個非 trivial 的功能——定時發佈帖子 (Scheduling Posts)。

對於一個認真使用 Reddit 的用户來説,這個功能非常實用,尤其考慮到 Reddit 提交內容的時效性。

接下來,我們將構建一個更有用的功能,幫助用户在 Reddit 上獲得更多點贊——機器學習建議。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.