├── src ├── test │ ├── resources │ │ └── pull-readhub.properties │ └── java │ │ └── me │ │ └── tianshuang │ │ └── task │ │ └── ScheduleTaskTest.java └── main │ └── java │ └── me │ └── tianshuang │ ├── domain │ ├── MarkdownDO.java │ ├── NewsDO.java │ └── TopicDO.java │ ├── dto │ └── DingTalkDTO.java │ ├── vo │ └── PageVO.java │ ├── PullReadhubApplication.java │ └── task │ └── ScheduleTask.java ├── README.md ├── .gitignore └── pom.xml /src/test/resources/pull-readhub.properties: -------------------------------------------------------------------------------- 1 | dingtalk.robot.url= 2 | cron=0 0 9 * * * -------------------------------------------------------------------------------- /src/main/java/me/tianshuang/domain/MarkdownDO.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang.domain; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Created by Poison on 12/04/2017. 7 | */ 8 | @Data 9 | public class MarkdownDO { 10 | 11 | private String title; 12 | 13 | private String text; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/tianshuang/domain/NewsDO.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang.domain; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Created by Poison on 12/04/2017. 7 | */ 8 | @Data 9 | public class NewsDO { 10 | 11 | private String url; 12 | 13 | public String getUrl() { 14 | return url == null ? null : url.trim(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/tianshuang/dto/DingTalkDTO.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang.dto; 2 | 3 | import lombok.Data; 4 | import me.tianshuang.domain.MarkdownDO; 5 | 6 | /** 7 | * Created by Poison on 12/04/2017. 8 | */ 9 | @Data 10 | public class DingTalkDTO { 11 | 12 | private String msgtype = "markdown"; 13 | 14 | private MarkdownDO markdown; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/tianshuang/vo/PageVO.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang.vo; 2 | 3 | import lombok.Data; 4 | import me.tianshuang.domain.TopicDO; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created by Poison on 12/04/2017. 10 | */ 11 | @Data 12 | public class PageVO { 13 | 14 | private List data; 15 | 16 | private int pageSize; 17 | 18 | private int totalItems; 19 | 20 | private int totalPages; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/me/tianshuang/domain/TopicDO.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang.domain; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | /** 8 | * Created by Poison on 12/04/2017. 9 | */ 10 | @Data 11 | public class TopicDO { 12 | 13 | private String id; 14 | 15 | private int order; 16 | 17 | private String title; 18 | 19 | private NewsDO[] newsArray; 20 | 21 | private LocalDateTime createdAt; 22 | 23 | public String getTitle() { 24 | return title == null ? null : title.trim(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pull-readhub.cn 2 | 3 | ### 简介 4 | 无码科技近日发布了 [Readhub](https://readhub.cn/),由于不想每天手动登录站点去看,所以随手写了一个小工具,每天早上定时通过钉钉机器人推送到群里。 5 | 6 | 效果如图: 7 | ![dingding](https://storage.tianshuang.me/pull-readhub/dingtalk.jpg) 8 | 9 | 环境要求: 10 | JDK8 + 11 | 12 | 使用方法: 13 | 14 | 下载 [pull-readhub-1.0.2.jar](https://storage.tianshuang.me/pull-readhub/pull-readhub-1.0.2.jar) 15 | 16 | 下载 [pull-readhub.properties](https://storage.tianshuang.me/pull-readhub/pull-readhub.properties) 17 | 18 | 修改配置文件中的日志路径及钉钉机器人 URL 及 cron 配置并将以上两个文件放置于同一目录下。 19 | 20 | ```bash 21 | chmod u+x pull-readhub-1.0.2.jar 22 | ./pull-readhub-1.0.2.jar & 23 | ``` 24 | -------------------------------------------------------------------------------- /src/main/java/me/tianshuang/PullReadhubApplication.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | import org.springframework.boot.builder.SpringApplicationBuilder; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | @SpringBootApplication 8 | @EnableScheduling 9 | public class PullReadhubApplication { 10 | 11 | public static void main(String[] args) { 12 | new SpringApplicationBuilder(PullReadhubApplication.class) 13 | .properties("spring.config.name:pull-readhub") 14 | .build() 15 | .run(args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/me/tianshuang/task/ScheduleTaskTest.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang.task; 2 | 3 | import net.steppschuh.markdowngenerator.MarkdownSerializationException; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.test.context.TestPropertySource; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * Created by Poison on 12/04/2017. 13 | */ 14 | @SpringBootTest 15 | @TestPropertySource(locations = "classpath:pull-readhub.properties") 16 | public class ScheduleTaskTest { 17 | 18 | @Autowired 19 | private ScheduleTask scheduleTask; 20 | 21 | @Test 22 | public void pullNewsFromReadhubTest() throws MarkdownSerializationException, IOException { 23 | scheduleTask.pullNewsFromReadhubAndPushToDingtalk(); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | ### JetBrains template 26 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 27 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 28 | 29 | # User-specific stuff: 30 | .idea/**/workspace.xml 31 | .idea/**/tasks.xml 32 | .idea/dictionaries 33 | 34 | # Sensitive or high-churn files: 35 | .idea/**/dataSources/ 36 | .idea/**/dataSources.ids 37 | .idea/**/dataSources.xml 38 | .idea/**/dataSources.local.xml 39 | .idea/**/sqlDataSources.xml 40 | .idea/**/dynamic.xml 41 | .idea/**/uiDesigner.xml 42 | 43 | # Gradle: 44 | .idea/**/gradle.xml 45 | .idea/**/libraries 46 | 47 | # Mongo Explorer plugin: 48 | .idea/**/mongoSettings.xml 49 | 50 | ## File-based project format: 51 | *.iws 52 | 53 | ## Plugin-specific files: 54 | 55 | # IntelliJ 56 | /out/ 57 | 58 | # mpeltonen/sbt-idea plugin 59 | .idea_modules/ 60 | 61 | # JIRA plugin 62 | atlassian-ide-plugin.xml 63 | 64 | # Crashlytics plugin (for Android Studio and IntelliJ) 65 | com_crashlytics_export_strings.xml 66 | crashlytics.properties 67 | crashlytics-build.properties 68 | fabric.properties 69 | .idea/ 70 | .mvn/ 71 | pull-readhubme.iml 72 | target/ 73 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | me.tianshuang 7 | pull-readhub 8 | 1.0.2 9 | jar 10 | 11 | pull-readhub 12 | pull readhub.cn 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.7.0 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 2.14.0-rc2 26 | 4.10.0 27 | 1.3.1.1 28 | 29 | 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-test 39 | test 40 | 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | true 46 | 47 | 48 | 49 | net.steppschuh.markdowngenerator 50 | markdowngenerator 51 | ${markdowngenerator.version} 52 | 53 | 54 | 55 | com.fasterxml.jackson.core 56 | jackson-databind 57 | ${jackson.version} 58 | 59 | 60 | com.fasterxml.jackson.datatype 61 | jackson-datatype-jsr310 62 | ${jackson.version} 63 | 64 | 65 | 66 | com.squareup.okhttp3 67 | okhttp 68 | ${okhttp.version} 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-maven-plugin 77 | 78 | true 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/java/me/tianshuang/task/ScheduleTask.java: -------------------------------------------------------------------------------- 1 | package me.tianshuang.task; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import lombok.extern.slf4j.Slf4j; 6 | import me.tianshuang.domain.MarkdownDO; 7 | import me.tianshuang.domain.TopicDO; 8 | import me.tianshuang.dto.DingTalkDTO; 9 | import me.tianshuang.vo.PageVO; 10 | import net.steppschuh.markdowngenerator.MarkdownSerializationException; 11 | import net.steppschuh.markdowngenerator.link.Link; 12 | import net.steppschuh.markdowngenerator.list.UnorderedList; 13 | import okhttp3.*; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.scheduling.annotation.Scheduled; 16 | import org.springframework.stereotype.Service; 17 | 18 | import java.io.IOException; 19 | import java.time.LocalDateTime; 20 | import java.time.ZoneId; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Objects; 24 | 25 | /** 26 | * Created by Poison on 12/04/2017. 27 | */ 28 | @Slf4j 29 | @Service 30 | public class ScheduleTask { 31 | 32 | private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient(); 33 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 34 | 35 | static { 36 | OBJECT_MAPPER.findAndRegisterModules(); 37 | OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 38 | } 39 | 40 | @Value("${dingtalk.robot.url}") 41 | private String dingtalkRobotUrl; 42 | 43 | private Integer lastCursor; 44 | 45 | @Scheduled(cron = "${cron}") 46 | void pullNewsFromReadhubAndPushToDingtalk() throws IOException, MarkdownSerializationException { 47 | List linkList = new ArrayList<>(80); 48 | LocalDateTime yesterday = LocalDateTime.now(ZoneId.of("UTC")).minusDays(1); 49 | for (int i = 0; i < 4; i++) { 50 | pullNewsFromReadhubSince(linkList, yesterday); 51 | } 52 | lastCursor = null; 53 | sendNewsToDingTalk(linkList); 54 | } 55 | 56 | private void pullNewsFromReadhubSince(List linkList, LocalDateTime yesterday) throws IOException { 57 | Request urlRequest = new Request.Builder() 58 | .url("https://api.readhub.cn/topic?pageSize=20" + (lastCursor != null ? "&lastCursor=" + lastCursor : "")) 59 | .build(); 60 | try (Response response = OK_HTTP_CLIENT.newCall(urlRequest).execute()) { 61 | PageVO pageVO = OBJECT_MAPPER.readValue(Objects.requireNonNull(response.body()).string(), PageVO.class); 62 | for (TopicDO topicDO : pageVO.getData()) { 63 | if (topicDO.getCreatedAt().isAfter(yesterday)) { 64 | linkList.add(new Link(topicDO.getTitle(), "https://readhub.cn/topic/" + topicDO.getId())); 65 | } 66 | } 67 | 68 | lastCursor = pageVO.getData().get(pageVO.getData().size() - 1).getOrder(); 69 | } 70 | } 71 | 72 | private void sendNewsToDingTalk(List linkList) throws MarkdownSerializationException, IOException { 73 | if (linkList.isEmpty()) { 74 | return; 75 | } 76 | 77 | MarkdownDO markdownDO = new MarkdownDO(); 78 | markdownDO.setTitle("Readhub 热门话题"); 79 | markdownDO.setText(new UnorderedList<>(linkList).serialize()); 80 | 81 | DingTalkDTO dingTalkMessage = new DingTalkDTO(); 82 | dingTalkMessage.setMarkdown(markdownDO); 83 | 84 | RequestBody requestBody = RequestBody.create(OBJECT_MAPPER.writeValueAsString(dingTalkMessage), MediaType.parse("application/json; charset=utf-8")); 85 | Request urlRequest = new Request.Builder() 86 | .url(dingtalkRobotUrl) 87 | .header("Content-Type", "application/json") 88 | .post(requestBody) 89 | .build(); 90 | try (Response response = OK_HTTP_CLIENT.newCall(urlRequest).execute()) { 91 | log.info("Dingtalk HTTP response: " + Objects.requireNonNull(response.body()).string()); 92 | } 93 | } 94 | 95 | } 96 | --------------------------------------------------------------------------------