Перейти к основному содержимому

Запланировать публикацию на Reddit с помощью Spring

· 7 мин. чтения

1. Обзор

В предыдущих частях этого тематического исследования мы настроили простое приложение и процесс аутентификации OAuth с API Reddit.

Давайте теперь создадим что-то полезное с Reddit — поддержку планирования публикаций для последнего.

2. Пользователь и пост

Во-первых, давайте создадим 2 основных объекта — User и Post . Пользователь будет отслеживать имя пользователя и некоторую дополнительную информацию 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 для сохранения состояния, поэтому здесь особо не на что смотреть, кроме хорошо известных определений интерфейса для наших репозиториев:

  • Пользовательский репозиторий:
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. Планировщик

Для аспектов планирования приложения мы также собираемся эффективно использовать стандартную поддержку 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. Процесс входа в систему

С новой сущностью пользователя, содержащей некоторую информацию о безопасности, нам нужно изменить наш простой процесс входа в систему, чтобы сохранить эту информацию :

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

Обратите внимание, как автоматически создается пользователь, если он еще не существует. Это заставляет процесс «Войти с 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();
}

расписаниеPostForm.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 кармы , он не может запланировать публикацию, не заполнив капчу.

7. Публикация

Когда форма расписания отправляется, информация о публикации просто проверяется и сохраняется для последующего использования планировщиком:

@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 мы создали первую нетривиальную часть функциональности с использованием API Reddit — планирование публикаций.

Это очень полезная функция для серьезного пользователя Reddit, особенно если учесть, насколько чувствительными ко времени являются отправки Reddit .

Далее — мы создадим еще более полезную функциональность, которая поможет получить одобрение контента на Reddit — предложения по машинному обучению.

Полную реализацию этого руководства можно найти в проекте github — это проект на основе Eclipse, поэтому его легко импортировать и запускать как есть.