Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edd12b6488 | |||
| 239bbf84f3 | |||
| 611db0d977 | |||
| 656ee0bcb4 | |||
| 3e9d0b4441 |
@@ -2,6 +2,20 @@
|
||||
|
||||
> 基于原作者v1.4.3代码修改
|
||||
|
||||
#### v1.4.3-5
|
||||
1. 补充ui/package.json内"@rsbuild/core": "^1.4.3"依赖
|
||||
2. 新增单条朋友圈页面(前端模板friend-post.html;访问地址/friends/friend-post-xxxxx)
|
||||
3. 朋友圈内容可搜索并返回单条朋友圈页面链接
|
||||
4. 内容(description)为空的项目将不再存入数据库
|
||||
|
||||
#### v1.4.3-4
|
||||
1. 控制台新增并行处理数量、图片下载开关、图片下载位置、图片下载代理等配置
|
||||
2. 修改图片列表字段为imageFiles(不再覆盖title)
|
||||
|
||||
#### v1.4.3-3
|
||||
1. title字段改为微博图片列表
|
||||
2. 图片列表内文件将自动下载至/upload/image-host
|
||||
|
||||
#### v1.4.3-2
|
||||
1. 新增URL参数author用于查询作者
|
||||
2. 模板参数新增${authors}返回所有作者
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
version=1.4.3-2
|
||||
version=1.4.3-5
|
||||
|
||||
Generated
+33
-1
@@ -87,7 +87,7 @@ importers:
|
||||
specifier: ^2.21.0
|
||||
version: 2.22.0(@tiptap/extension-collaboration@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0)(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29))(@tiptap/extension-node-range@3.20.0(@tiptap/core@3.20.0(@tiptap/pm@3.20.0))(@tiptap/pm@3.20.0))(@tiptap/y-tiptap@3.0.2(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)(y-protocols@1.0.7(yjs@13.6.29))(yjs@13.6.29))(axios@1.13.6)(vue-router@4.6.4(vue@3.5.29(typescript@5.5.4)))(vue@3.5.29(typescript@5.5.4))
|
||||
'@kunkunyu/fridends-rss':
|
||||
specifier: file:../packages/fridends-rss
|
||||
specifier: workspace:*
|
||||
version: link:../packages/fridends-rss
|
||||
'@tanstack/vue-query':
|
||||
specifier: ^4.26.1
|
||||
@@ -126,6 +126,9 @@ importers:
|
||||
'@iconify/json':
|
||||
specifier: ^2.2.224
|
||||
version: 2.2.446
|
||||
'@rsbuild/core':
|
||||
specifier: ^1.4.3
|
||||
version: 1.7.3
|
||||
'@rspack/cli':
|
||||
specifier: 1.4.0-beta.0
|
||||
version: 1.4.0-beta.0(@rspack/core@1.4.0-beta.0(@swc/helpers@0.5.19))(@types/express@4.17.25)(tslib@2.8.1)(webpack@5.105.4)
|
||||
@@ -917,48 +920,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.115.0':
|
||||
resolution: {integrity: sha512-HjsZbJPH9mMd4swJRywVMsDZsJX0hyKb1iNHo5ijRl5yhtbO3lj7ImSrrL1oZ1VEg0te4iKmDGGz/6YPLd1G8w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-ppc64-gnu@0.115.0':
|
||||
resolution: {integrity: sha512-zhhePoBrd7kQx3oClX/W6NldsuCbuMqaN9rRsY+6/WoorAb4j490PG/FjqgAXscWp2uSW2WV9L+ksn0wHrvsrg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.115.0':
|
||||
resolution: {integrity: sha512-t/IRojvUE9XrKu+/H1b8YINug+7Q6FLls5rsm2lxB5mnS8GN/eYAYrPgHkcg9/1SueRDSzGpDYu3lGWTObk1zw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-musl@0.115.0':
|
||||
resolution: {integrity: sha512-79jBHSSh/YpQRAmvYoaCfpyToRbJ/HBrdB7hxK2ku2JMehjopTVo+xMJss/RV7/ZYqeezgjvKDQzapJbgcjVZA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.115.0':
|
||||
resolution: {integrity: sha512-nA1TpxkhNTIOMMyiSSsa7XIVJVoOU/SsVrHIz3gHvWweB5PHCQfO7w+Lb2EP0lBWokv7HtA/KbF7aLDoXzmuMw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.115.0':
|
||||
resolution: {integrity: sha512-9iVX789DoC3SaOOG+X6NcF/tVChgLp2vcHffzOC2/Z1JTPlz6bMG2ogvcW6/9s0BG2qvhNQImd+gbWYeQbOwVw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.115.0':
|
||||
resolution: {integrity: sha512-RmQmk+mjCB0nMNfEYhaCxwofLo1Z95ebHw1AGvRiWGCd4zhCNOyskgCbMogIcQzSB3SuEKWgkssyaiQYVAA4hQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/binding-openharmony-arm64@0.115.0':
|
||||
resolution: {integrity: sha512-viigraWWQhhDvX5aGq+wrQq58k00Xq3MHz/0R4AFMxGlZ8ogNonpEfNc73Q5Ly87Z6sU9BvxEdG0dnYTfVnmew==}
|
||||
@@ -1055,66 +1066,79 @@ packages:
|
||||
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
|
||||
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.59.0':
|
||||
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
|
||||
@@ -1180,41 +1204,49 @@ packages:
|
||||
resolution: {integrity: sha512-tzLHo5upqlDWK3wSTit0m0iZ8N6pm6S42R/sfeOcPwERcTjhTrbQ6GOEbmwsay845EgzJbGWwaOzVeGLT55YCw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rspack/binding-linux-arm64-gnu@1.7.7':
|
||||
resolution: {integrity: sha512-uSq4qkvmAzSDUTKE2v4yUgHIBdTily1k3BcK5wBCGFm9OPODj5lQZpAdOHHIwu+Jxyjoa7Mb64tghhj9hZcXcA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rspack/binding-linux-arm64-musl@1.4.0-beta.0':
|
||||
resolution: {integrity: sha512-g3YZCNB+oR8+CG83iOoACZrxiM9sKlB33QmJB2PFk5TTryhkNGEq3vwiyqe7AVPWYfuCCqoRdzPNzWBIN80cEg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rspack/binding-linux-arm64-musl@1.7.7':
|
||||
resolution: {integrity: sha512-NhWCBfiu6plpmLRP6c6D5lBUaVrBr1nvjSEc7VyQF8TGh8URo2btH0wngEiX0nWvidsSlERt1l6Y5QPGuiCl1g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rspack/binding-linux-x64-gnu@1.4.0-beta.0':
|
||||
resolution: {integrity: sha512-TNIm9APDmcbbrwWgSxaIbq73r0cKrzyS0Ei7rB0TyX9EmFYSfsCdmdJMwG2yKP3p+egRIDMWU9AIrxL4HIMrBg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rspack/binding-linux-x64-gnu@1.7.7':
|
||||
resolution: {integrity: sha512-aRvf8gCI7jDeEN9i4u9fY5coa3ZAyHzGVA4ZhTJCgZ5wWA5A9SQewMSq7khS1WAAFE1USlk1tUuPujnrGoYrGg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rspack/binding-linux-x64-musl@1.4.0-beta.0':
|
||||
resolution: {integrity: sha512-PCfGShh6y0CqUX8XxuxkEKOBLELuxDZ/sacM047CBIet3CgvqmT0Ff2DKXFIu8Q+NWrKnzvopO7hPv4Zelku6A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rspack/binding-linux-x64-musl@1.7.7':
|
||||
resolution: {integrity: sha512-ALPto4OT7snzXbYDyqkLfh1BvwDTTH1hPYXGUXBzQ0wEV7sXeyvxCC4yjH6B5MhR7W3tFuF4IfDy5Z4BxmOoGQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rspack/binding-wasm32-wasi@1.7.7':
|
||||
resolution: {integrity: sha512-7DZvUp0v75n451qfZw1ppbPakL6NAc2gjb5e9AJcOb7KUMBHNyOxqpPo/jRYKxH7isPpLfpoId79WQGGNTTMAw==}
|
||||
|
||||
@@ -9,12 +9,24 @@ import java.util.List;
|
||||
public class RssFeedSyncEvent extends ApplicationEvent {
|
||||
private final Link link;
|
||||
private final int sum;
|
||||
private final int parallelCount;
|
||||
private final boolean enableWeiboImageRecordAndDownload;
|
||||
private final List<String> disableSyncList;
|
||||
private final String imageSavePath;
|
||||
private final String imageBaseUrl;
|
||||
private final String imageReferer;
|
||||
|
||||
public RssFeedSyncEvent(Object source, Link link, int sum, List<String> disableSyncList) {
|
||||
public RssFeedSyncEvent(Object source, Link link, int sum, int parallelCount,
|
||||
boolean enableWeiboImageRecordAndDownload, List<String> disableSyncList,
|
||||
String imageSavePath, String imageBaseUrl, String imageReferer) {
|
||||
super(source);
|
||||
this.link = link;
|
||||
this.sum = sum;
|
||||
this.parallelCount = parallelCount;
|
||||
this.enableWeiboImageRecordAndDownload = enableWeiboImageRecordAndDownload;
|
||||
this.disableSyncList = disableSyncList;
|
||||
this.imageSavePath = imageSavePath;
|
||||
this.imageBaseUrl = imageBaseUrl;
|
||||
this.imageReferer = imageReferer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ public class CronFriendPost extends AbstractExtension {
|
||||
private String cron;
|
||||
private String timezone;
|
||||
private boolean suspend;
|
||||
private boolean enableWeiboImageRecordAndDownload;
|
||||
private int parallelCount;
|
||||
private String imageSavePath;
|
||||
private String imageBaseUrl;
|
||||
private String imageReferer;
|
||||
|
||||
@Schema(
|
||||
minimum = "0"
|
||||
|
||||
@@ -38,6 +38,9 @@ public class FriendPost extends AbstractExtension {
|
||||
private Instant pubDate;
|
||||
|
||||
private String linkName;
|
||||
|
||||
// Comma-separated image file names extracted from content.
|
||||
private String imageFiles;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.rometools.rome.feed.synd.SyndFeed;
|
||||
import com.rometools.rome.io.SyndFeedInput;
|
||||
import com.rometools.rome.io.XmlReader;
|
||||
import la.moony.friends.RssFeedSyncEvent;
|
||||
import la.moony.friends.extension.CronFriendPost;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import la.moony.friends.extension.Link;
|
||||
import la.moony.friends.extension.RssFeedSyncLog;
|
||||
@@ -27,16 +26,32 @@ import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.extension.controller.*;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.DefaultController;
|
||||
import run.halo.app.extension.controller.DefaultQueue;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.controller.RequestQueue;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.springframework.data.domain.Sort.Order.asc;
|
||||
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
||||
@@ -45,17 +60,38 @@ import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
@Component
|
||||
public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,SmartLifecycle {
|
||||
public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>, SmartLifecycle {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RssSyncReconciler.class);
|
||||
|
||||
// 新增:正则匹配所有img标签的src属性
|
||||
private static final Pattern IMG_SRC_PATTERN = Pattern.compile(
|
||||
"<img[^>]*?src\\s*=\\s*(['\"])(.*?)\\1[^>]*?>",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final Pattern VIDEO_POSTER_PATTERN = Pattern.compile(
|
||||
"<video[^>]*?poster\\s*=\\s*(['\"])(.*?)\\1[^>]*?>",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final Pattern MW2000_IMAGE_FILE_PATTERN = Pattern.compile(
|
||||
".*/mw2000/([^/?#]+)(?:[?#].*)?$",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private static final String DEFAULT_IMAGE_BASE_URL = "https://wx3.sinaimg.cn/middle/";
|
||||
private static final String DEFAULT_IMAGE_REFERER = "https://weibo.com/";
|
||||
private static final String DEFAULT_IMAGE_HOST_PATH = "/root/.halo2/attachments/upload/image-host/";
|
||||
|
||||
private volatile boolean running = false;
|
||||
|
||||
private final ExtensionClient client;
|
||||
private final RequestQueue<Request> queue;
|
||||
private final Controller controller;
|
||||
private final ObjectMapper objectMapper = Unstructured.OBJECT_MAPPER;
|
||||
|
||||
private volatile int configuredParallelCount = 1;
|
||||
private volatile Semaphore syncSemaphore = new Semaphore(1, true);
|
||||
|
||||
public RssSyncReconciler(ExtensionClient client) {
|
||||
this.client = client;
|
||||
@@ -65,9 +101,22 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
int parallelCount = normalizeParallelCount(request.parallelCount());
|
||||
refreshSyncSemaphore(parallelCount);
|
||||
Semaphore semaphore = this.syncSemaphore;
|
||||
boolean acquired = false;
|
||||
try {
|
||||
semaphore.acquire();
|
||||
acquired = true;
|
||||
|
||||
Link link = request.link();
|
||||
int sum = request.sum();
|
||||
List<String> disableSyncList = request.disableSyncList();
|
||||
boolean enableWeiboImageRecordAndDownload =
|
||||
request.enableWeiboImageRecordAndDownload();
|
||||
String imageSavePath = request.imageSavePath();
|
||||
String imageBaseUrl = request.imageBaseUrl();
|
||||
String imageReferer = request.imageReferer();
|
||||
var linkName = link.getMetadata().getName();
|
||||
String rssUrl = getRss(link);
|
||||
boolean isContains = false;
|
||||
@@ -81,13 +130,14 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
newRssFeedSyncLog.setMetadata(metadata);
|
||||
newRssFeedSyncLog.setLinkName(linkName);
|
||||
if (StringUtils.isNotEmpty(rssUrl) && !isContains) {
|
||||
tryToSynchronizeFriendPost(newRssFeedSyncLog, link, sum);
|
||||
}else if (StringUtils.isEmpty(rssUrl)) {
|
||||
tryToSynchronizeFriendPost(newRssFeedSyncLog, link, sum, imageSavePath,
|
||||
imageBaseUrl, imageReferer, enableWeiboImageRecordAndDownload);
|
||||
} else if (StringUtils.isEmpty(rssUrl)) {
|
||||
newRssFeedSyncLog.setSyncTime(Instant.now());
|
||||
newRssFeedSyncLog.setState(RssFeedSyncLog.RssFeedSyncLogState.nolink);
|
||||
newRssFeedSyncLog.setFailureReason("No RSS link");
|
||||
newRssFeedSyncLog.setFailureMessage("无 RSS 链接.");
|
||||
}else if (isContains) {
|
||||
} else if (isContains) {
|
||||
newRssFeedSyncLog.setSyncTime(Instant.now());
|
||||
newRssFeedSyncLog.setState(RssFeedSyncLog.RssFeedSyncLogState.failed);
|
||||
newRssFeedSyncLog.setFailureReason("admin disables sync");
|
||||
@@ -101,10 +151,31 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
rssFeedSyncLog.setFailureReason(newRssFeedSyncLog.getFailureReason());
|
||||
rssFeedSyncLog.setFailureMessage(newRssFeedSyncLog.getFailureMessage());
|
||||
client.update(rssFeedSyncLog);
|
||||
}else {
|
||||
} else {
|
||||
client.create(newRssFeedSyncLog);
|
||||
}
|
||||
return Result.doNotRetry();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("Interrupted while waiting for sync permit.", e);
|
||||
return Result.doNotRetry();
|
||||
} finally {
|
||||
if (acquired) {
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void refreshSyncSemaphore(int parallelCount) {
|
||||
if (parallelCount == configuredParallelCount) {
|
||||
return;
|
||||
}
|
||||
configuredParallelCount = parallelCount;
|
||||
syncSemaphore = new Semaphore(parallelCount, true);
|
||||
}
|
||||
|
||||
private int normalizeParallelCount(int parallelCount) {
|
||||
return parallelCount <= 0 ? 1 : parallelCount;
|
||||
}
|
||||
|
||||
public String getRss(Link link) {
|
||||
@@ -121,10 +192,10 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
linkRss = rssUri;
|
||||
annotations.remove("rss_uri");
|
||||
annotations.put("rss_url", rssUri);
|
||||
}else {
|
||||
annotations.put("is_request","true");
|
||||
} else {
|
||||
annotations.put("is_request", "true");
|
||||
if (StringUtils.isNotEmpty(linkRss)) {
|
||||
annotations.put("rss_url",linkRss);
|
||||
annotations.put("rss_url", linkRss);
|
||||
}
|
||||
}
|
||||
updateLink(link);
|
||||
@@ -133,22 +204,18 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
return rssUrl;
|
||||
}
|
||||
|
||||
|
||||
public void updateLink(Link link) {
|
||||
Map extensionMap = objectMapper.convertValue(link, Map.class);
|
||||
var extension = new Unstructured(extensionMap);
|
||||
client.update(extension);
|
||||
}
|
||||
|
||||
private void tryToSynchronizeFriendPost(RssFeedSyncLog syncLog, Link link, int sum) {
|
||||
private void tryToSynchronizeFriendPost(RssFeedSyncLog syncLog, Link link, int sum,
|
||||
String imageSavePath, String imageBaseUrl, String imageReferer,
|
||||
boolean enableWeiboImageRecordAndDownload) {
|
||||
var linkName = link.getMetadata().getName();
|
||||
var annotations = nullSafeAnnotations(link);
|
||||
var rssUrl = annotations.get("rss_url");
|
||||
|
||||
CronFriendPost cron = new CronFriendPost();
|
||||
var spec = new CronFriendPost.CronSpec();
|
||||
spec.setSuccessfulRetainLimit(5);
|
||||
cron.setSpec(spec);
|
||||
syncLog.setSyncTime(Instant.now());
|
||||
List<FriendPost> friendPostList = fetchFriendPost(rssUrl, sum, syncLog);
|
||||
|
||||
@@ -158,12 +225,12 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
var friendPostListOptions = new ListOptions();
|
||||
friendPostListOptions.setFieldSelector(FieldSelector.of(
|
||||
and(isNull("metadata.deletionTimestamp"),
|
||||
equal("spec.postLink",rssPost.getSpec().getPostLink())
|
||||
equal("spec.postLink", rssPost.getSpec().getPostLink())
|
||||
)
|
||||
));
|
||||
long total = client.listBy(FriendPost.class, friendPostListOptions,
|
||||
PageRequestImpl.ofSize(1)).getTotal();
|
||||
if (total==0) {
|
||||
if (total == 0) {
|
||||
FriendPost friendPost = new FriendPost();
|
||||
// 设置元数据才能保存
|
||||
friendPost.setMetadata(new Metadata());
|
||||
@@ -175,23 +242,59 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
friendPostSpec.setAuthorUrl(link.getSpec().getUrl());
|
||||
friendPostSpec.setLinkName(linkName);
|
||||
String description = friendPost.getSpec().getDescription();
|
||||
//解析html内容转换成文本
|
||||
if (StringUtils.isNotEmpty(description)){
|
||||
description = CommonUtils.parseAndTruncateHtml2Text(description, 100000);
|
||||
String regexp = "[ *|\\s*]*";
|
||||
description = description.replaceFirst(regexp, "").trim();
|
||||
String weiboVideoRegex = "\\s[^\\s]*的微博视频 视频无法显示,请前往微博视频观看。";
|
||||
description = description.replaceAll(weiboVideoRegex, "");
|
||||
friendPostSpec.setDescription(description);
|
||||
String pureText = normalizeDescription(description);
|
||||
if (StringUtils.isBlank(pureText)) {
|
||||
continue;
|
||||
}
|
||||
client.create(friendPost);
|
||||
}
|
||||
}
|
||||
delFriendPost(linkName,sum);
|
||||
|
||||
// 提取图片和视频 poster 文件名,并保存过滤后的纯文本描述
|
||||
List<String> imageFileNames = new ArrayList<>();
|
||||
Matcher matcher = IMG_SRC_PATTERN.matcher(description);
|
||||
while (matcher.find()) {
|
||||
String imgSrc = matcher.group(2);
|
||||
Matcher mw2000Matcher = MW2000_IMAGE_FILE_PATTERN.matcher(imgSrc);
|
||||
if (mw2000Matcher.matches()) {
|
||||
imageFileNames.add(mw2000Matcher.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
public List<FriendPost> fetchFriendPost(String rssAddress,int postsLimit, RssFeedSyncLog sync) {
|
||||
Matcher posterMatcher = VIDEO_POSTER_PATTERN.matcher(description);
|
||||
while (posterMatcher.find()) {
|
||||
String posterSrc = posterMatcher.group(2);
|
||||
String posterFileName = extractFileNameFromUrl(posterSrc);
|
||||
if (StringUtils.isNotBlank(posterFileName)) {
|
||||
imageFileNames.add(posterFileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableWeiboImageRecordAndDownload && !imageFileNames.isEmpty()) {
|
||||
String imageFiles = String.join(",", imageFileNames);
|
||||
friendPostSpec.setImageFiles(imageFiles);
|
||||
// 通知图片处理服务
|
||||
notifyImageService(imageFiles, imageSavePath, imageBaseUrl,
|
||||
imageReferer);
|
||||
}
|
||||
|
||||
friendPostSpec.setDescription(pureText);
|
||||
client.create(friendPost);
|
||||
}
|
||||
}
|
||||
delFriendPost(linkName, sum);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDescription(String description) {
|
||||
if (StringUtils.isEmpty(description)) {
|
||||
return "";
|
||||
}
|
||||
String pureText = CommonUtils.parseAndTruncateHtml2Text(description, 100000);
|
||||
String regexp = "[ *|\\s*]*";
|
||||
pureText = pureText.replaceFirst(regexp, "").trim();
|
||||
String weiboVideoRegex = "\\s[^\\s]*的微博视频 视频无法显示,请前往微博视频观看。";
|
||||
return pureText.replaceAll(weiboVideoRegex, "").trim();
|
||||
}
|
||||
|
||||
public List<FriendPost> fetchFriendPost(String rssAddress, int postsLimit, RssFeedSyncLog sync) {
|
||||
List<FriendPost> friendPostList = new ArrayList<>();
|
||||
try {
|
||||
SyndFeed feed = new SyndFeedInput().build(new XmlReader(new URL(rssAddress)));
|
||||
@@ -205,7 +308,7 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
SyndContent content = entry.getDescription();
|
||||
if (content != null) {
|
||||
description = content.getValue();
|
||||
}else {
|
||||
} else {
|
||||
List<SyndContent> contents = entry.getContents();
|
||||
if (!contents.isEmpty()) {
|
||||
description = contents.get(0).getValue();
|
||||
@@ -235,10 +338,10 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
return friendPostList;
|
||||
}
|
||||
|
||||
public void delFriendPost(String linkName,Integer sum) {
|
||||
public void delFriendPost(String linkName, Integer sum) {
|
||||
var friendPostListOptions = new ListOptions();
|
||||
friendPostListOptions.setFieldSelector(FieldSelector.of(
|
||||
equal("spec.linkName",linkName)
|
||||
equal("spec.linkName", linkName)
|
||||
));
|
||||
long total = client.listBy(FriendPost.class, friendPostListOptions, PageRequestImpl.ofSize(1)).getTotal();
|
||||
if (total > sum) {
|
||||
@@ -253,6 +356,104 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知图片处理服务有新的图片需要处理
|
||||
* @param imageFiles 图片文件名,英文逗号分隔,如 "file1,file2,..."
|
||||
*/
|
||||
private void notifyImageService(String imageFiles, String imageSavePath, String imageBaseUrl,
|
||||
String imageReferer) {
|
||||
if (StringUtils.isBlank(imageFiles)) {
|
||||
return;
|
||||
}
|
||||
new Thread(() -> downloadImagesByFiles(imageFiles, imageSavePath, imageBaseUrl,
|
||||
imageReferer)).start();
|
||||
}
|
||||
|
||||
private void downloadImagesByFiles(String imageFiles, String imageSavePath,
|
||||
String imageBaseUrl, String imageReferer) {
|
||||
if (StringUtils.isBlank(imageFiles)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Path imageHostDir = resolveImageHostDir(imageSavePath);
|
||||
String resolvedBaseUrl = resolveImageBaseUrl(imageBaseUrl);
|
||||
String resolvedReferer = resolveImageReferer(imageBaseUrl, imageReferer);
|
||||
Files.createDirectories(imageHostDir);
|
||||
String[] fileNames = imageFiles.split(",");
|
||||
for (String fileName : fileNames) {
|
||||
String trimmedName = fileName.trim();
|
||||
if (StringUtils.isEmpty(trimmedName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String encodedFileName = URLEncoder.encode(trimmedName, StandardCharsets.UTF_8)
|
||||
.replace("+", "%20");
|
||||
String downloadUrl = resolvedBaseUrl + encodedFileName;
|
||||
Path targetPath = imageHostDir.resolve(trimmedName);
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(downloadUrl)
|
||||
.openConnection();
|
||||
if (StringUtils.isNotBlank(resolvedReferer)) {
|
||||
connection.setRequestProperty("Referer", resolvedReferer);
|
||||
}
|
||||
try (InputStream inputStream = connection.getInputStream()) {
|
||||
Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to download images for files: {}", imageFiles, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveImageHostDir(String imageSavePath) {
|
||||
if (StringUtils.isBlank(imageSavePath)) {
|
||||
return Path.of(DEFAULT_IMAGE_HOST_PATH);
|
||||
}
|
||||
return Path.of(imageSavePath.trim());
|
||||
}
|
||||
|
||||
private String resolveImageBaseUrl(String imageBaseUrl) {
|
||||
String resolvedBaseUrl = StringUtils.isBlank(imageBaseUrl)
|
||||
? DEFAULT_IMAGE_BASE_URL
|
||||
: imageBaseUrl.trim();
|
||||
if (!resolvedBaseUrl.endsWith("/")) {
|
||||
return resolvedBaseUrl + "/";
|
||||
}
|
||||
return resolvedBaseUrl;
|
||||
}
|
||||
|
||||
private String resolveImageReferer(String imageBaseUrl, String imageReferer) {
|
||||
if (StringUtils.isBlank(imageBaseUrl)) {
|
||||
return DEFAULT_IMAGE_REFERER;
|
||||
}
|
||||
if (StringUtils.isBlank(imageReferer)) {
|
||||
return null;
|
||||
}
|
||||
return imageReferer.trim();
|
||||
}
|
||||
|
||||
private String extractFileNameFromUrl(String url) {
|
||||
if (StringUtils.isBlank(url)) {
|
||||
return null;
|
||||
}
|
||||
String sanitizedUrl = url.trim();
|
||||
int queryIndex = sanitizedUrl.indexOf('?');
|
||||
if (queryIndex >= 0) {
|
||||
sanitizedUrl = sanitizedUrl.substring(0, queryIndex);
|
||||
}
|
||||
int fragmentIndex = sanitizedUrl.indexOf('#');
|
||||
if (fragmentIndex >= 0) {
|
||||
sanitizedUrl = sanitizedUrl.substring(0, fragmentIndex);
|
||||
}
|
||||
int lastSlashIndex = sanitizedUrl.lastIndexOf('/');
|
||||
if (lastSlashIndex < 0 || lastSlashIndex == sanitizedUrl.length() - 1) {
|
||||
return null;
|
||||
}
|
||||
return sanitizedUrl.substring(lastSlashIndex + 1);
|
||||
}
|
||||
|
||||
static int pageNullSafe(Integer page) {
|
||||
return ObjectUtils.defaultIfNull(page, 1);
|
||||
}
|
||||
@@ -261,7 +462,6 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
return ObjectUtils.defaultIfNull(size, 10);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return new DefaultController<>(
|
||||
@@ -293,10 +493,16 @@ public class RssSyncReconciler implements Reconciler<RssSyncReconciler.Request>,
|
||||
|
||||
@EventListener(RssFeedSyncEvent.class)
|
||||
public void onReplyEvent(RssFeedSyncEvent event) {
|
||||
var request = new Request(event.getLink(),event.getSum(),event.getDisableSyncList());
|
||||
var request = new Request(event.getLink(), event.getSum(), event.getDisableSyncList(),
|
||||
event.isEnableWeiboImageRecordAndDownload(),
|
||||
event.getImageSavePath(), event.getImageBaseUrl(), event.getImageReferer(),
|
||||
event.getParallelCount());
|
||||
queue.addImmediately(request);
|
||||
}
|
||||
|
||||
public record Request(Link link,int sum,List<String> disableSyncList) {
|
||||
public record Request(Link link, int sum, List<String> disableSyncList,
|
||||
boolean enableWeiboImageRecordAndDownload,
|
||||
String imageSavePath, String imageBaseUrl,
|
||||
String imageReferer, int parallelCount) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public class FriendRouter {
|
||||
RouterFunction<ServerResponse> friendTemplateRoute() {
|
||||
return RouterFunctions.route().GET("/friends",this::handlerFunction)
|
||||
.GET("/friends/page/{page:\\d+}",this::handlerFunction)
|
||||
.GET("/friends/{friendPostName:\\S+}", this::friendPostDetail)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -54,6 +55,16 @@ public class FriendRouter {
|
||||
));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> friendPostDetail(ServerRequest request) {
|
||||
String friendPostName = request.pathVariable("friendPostName");
|
||||
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "friend-post")
|
||||
.flatMap(templateName -> ServerResponse.ok().render(templateName,
|
||||
Map.of(ModelConst.TEMPLATE_ID, templateName,
|
||||
"friend", friendFinder.getByName(friendPostName),
|
||||
"title", getFriendTitle())
|
||||
));
|
||||
}
|
||||
|
||||
Mono<String> getFriendTitle() {
|
||||
return this.settingFetcher.get("base").map(
|
||||
setting -> setting.get("title").asText("友链朋友圈")).defaultIfEmpty(
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package la.moony.friends.search;
|
||||
|
||||
import static la.moony.friends.search.FriendPostHaloDocumentsProvider.FRIEND_POST_DOCUMENT_TYPE;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import la.moony.friends.util.CommonUtils;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.search.HaloDocument;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FriendPostDocumentConverter implements Converter<FriendPost, Mono<HaloDocument>> {
|
||||
|
||||
private static final String EMPTY_CONTENT = "无内容";
|
||||
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HaloDocument> convert(FriendPost friendPost) {
|
||||
var spec = friendPost.getSpec();
|
||||
var author = authorName(friendPost);
|
||||
var title = defaultIfBlank(spec == null ? null : spec.getTitle(), EMPTY_CONTENT);
|
||||
var description = defaultIfBlank(plainDescription(spec), EMPTY_CONTENT);
|
||||
var haloDoc = new HaloDocument();
|
||||
haloDoc.setMetadataName(friendPost.getMetadata().getName());
|
||||
haloDoc.setType(FRIEND_POST_DOCUMENT_TYPE);
|
||||
haloDoc.setId(haloDocId(friendPost));
|
||||
haloDoc.setTitle(title);
|
||||
haloDoc.setDescription(description);
|
||||
haloDoc.setContent(content(title, author, description));
|
||||
haloDoc.setExposed(true);
|
||||
haloDoc.setTags(tags(author));
|
||||
haloDoc.setOwnerName(author);
|
||||
haloDoc.setUpdateTimestamp(updateTimestamp(friendPost));
|
||||
haloDoc.setCreationTimestamp(creationTimestamp(friendPost));
|
||||
haloDoc.setPermalink(permalink(friendPost));
|
||||
haloDoc.setPublished(true);
|
||||
return Mono.just(haloDoc);
|
||||
}
|
||||
|
||||
String haloDocId(FriendPost friendPost) {
|
||||
return FRIEND_POST_DOCUMENT_TYPE + '-' + friendPost.getMetadata().getName();
|
||||
}
|
||||
|
||||
private String authorName(FriendPost friendPost) {
|
||||
var author = Optional.ofNullable(friendPost.getSpec())
|
||||
.map(FriendPost.FriendPostSpec::getAuthor)
|
||||
.orElse(null);
|
||||
return StringUtils.defaultIfBlank(author, EMPTY_CONTENT);
|
||||
}
|
||||
|
||||
private String plainDescription(FriendPost.FriendPostSpec spec) {
|
||||
if (spec == null) {
|
||||
return null;
|
||||
}
|
||||
return CommonUtils.parseAndTruncateHtml2Text(defaultString(spec.getDescription()), 300);
|
||||
}
|
||||
|
||||
private String content(String title, String author, String description) {
|
||||
return String.join(" ", title, author, description).trim();
|
||||
}
|
||||
|
||||
private List<String> tags(String author) {
|
||||
return StringUtils.isBlank(author) ? List.of() : List.of(author);
|
||||
}
|
||||
|
||||
private Instant updateTimestamp(FriendPost friendPost) {
|
||||
var spec = friendPost.getSpec();
|
||||
return Optional.ofNullable(spec)
|
||||
.map(FriendPost.FriendPostSpec::getPubDate)
|
||||
.orElse(creationTimestamp(friendPost));
|
||||
}
|
||||
|
||||
private Instant creationTimestamp(FriendPost friendPost) {
|
||||
return Optional.ofNullable(friendPost.getMetadata().getCreationTimestamp())
|
||||
.orElse(Instant.EPOCH);
|
||||
}
|
||||
|
||||
private String permalink(FriendPost friendPost) {
|
||||
var externalUrl = externalUrlSupplier.get();
|
||||
return externalUrl.resolve("friends/" + friendPost.getMetadata().getName()).toString();
|
||||
}
|
||||
|
||||
private static String defaultString(String value) {
|
||||
return StringUtils.defaultString(value);
|
||||
}
|
||||
|
||||
private static String defaultIfBlank(String value, String defaultValue) {
|
||||
return StringUtils.defaultIfBlank(value, defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package la.moony.friends.search;
|
||||
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.search.HaloDocument;
|
||||
import run.halo.app.search.HaloDocumentsProvider;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FriendPostHaloDocumentsProvider implements HaloDocumentsProvider {
|
||||
|
||||
public static final String FRIEND_POST_DOCUMENT_TYPE = "friendpost.friend.moony.la";
|
||||
|
||||
private static final int PAGE_SIZE = 200;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
|
||||
private final FriendPostDocumentConverter converter;
|
||||
|
||||
@Override
|
||||
public Flux<HaloDocument> fetchAll() {
|
||||
var options = new ListOptions();
|
||||
var notDeleted = isNull("metadata.deletionTimestamp");
|
||||
options.setFieldSelector(FieldSelector.of(notDeleted));
|
||||
var pageRequest = createPageRequest();
|
||||
return client.listBy(FriendPost.class, options, pageRequest)
|
||||
.expand(result -> result.hasNext()
|
||||
? client.listBy(FriendPost.class, options, nextPage(result, pageRequest.getSort()))
|
||||
: Mono.empty())
|
||||
.flatMap(result -> Flux.fromStream(result.get()))
|
||||
.flatMap(converter::convert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return FRIEND_POST_DOCUMENT_TYPE;
|
||||
}
|
||||
|
||||
private PageRequest createPageRequest() {
|
||||
return PageRequestImpl.of(1, PAGE_SIZE,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
}
|
||||
|
||||
private static PageRequest nextPage(ListResult<FriendPost> result, Sort sort) {
|
||||
return PageRequestImpl.of(result.getPage() + 1, result.getSize(), sort);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package la.moony.friends.search;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import la.moony.friends.extension.FriendPost;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.search.event.HaloDocumentAddRequestEvent;
|
||||
import run.halo.app.search.event.HaloDocumentDeleteRequestEvent;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FriendPostSearchReconciler implements Reconciler<Reconciler.Request> {
|
||||
|
||||
private static final String FINALIZER = "friend-post-search-protection";
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private final ExtensionClient client;
|
||||
|
||||
private final FriendPostDocumentConverter converter;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(FriendPost.class, request.name()).ifPresent(friendPost -> {
|
||||
if (ExtensionUtil.isDeleted(friendPost)) {
|
||||
if (ExtensionUtil.removeFinalizers(friendPost.getMetadata(), Set.of(FINALIZER))) {
|
||||
eventPublisher.publishEvent(
|
||||
new HaloDocumentDeleteRequestEvent(this,
|
||||
List.of(converter.haloDocId(friendPost)))
|
||||
);
|
||||
client.update(friendPost);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ExtensionUtil.addFinalizers(friendPost.getMetadata(), Set.of(FINALIZER));
|
||||
|
||||
var haloDoc = converter.convert(friendPost)
|
||||
.blockOptional().orElseThrow();
|
||||
eventPublisher.publishEvent(
|
||||
new HaloDocumentAddRequestEvent(this, List.of(haloDoc)));
|
||||
|
||||
client.update(friendPost);
|
||||
});
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder
|
||||
.extension(new FriendPost())
|
||||
.workerCount(1)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -74,14 +74,25 @@ public class RssFeedSyncLogServiceImpl implements RssFeedSyncLogService {
|
||||
CronFriendPost cron = new CronFriendPost();
|
||||
CronFriendPost.CronSpec cronSpec = new CronFriendPost.CronSpec();
|
||||
cronSpec.setSuccessfulRetainLimit(5);
|
||||
cronSpec.setParallelCount(1);
|
||||
cronSpec.setEnableWeiboImageRecordAndDownload(false);
|
||||
cronSpec.setDisableSyncList(new ArrayList<>());
|
||||
cronSpec.setImageSavePath("");
|
||||
cronSpec.setImageBaseUrl("");
|
||||
cronSpec.setImageReferer("");
|
||||
cron.setSpec(cronSpec);
|
||||
return client.fetch(CronFriendPost.class, "cron-friends-default")
|
||||
.defaultIfEmpty(cron)
|
||||
.flatMap(cronFriendPost -> {
|
||||
var spec = cronFriendPost.getSpec();
|
||||
int successfulRetainLimit = spec.getSuccessfulRetainLimit();
|
||||
int parallelCount = spec.getParallelCount();
|
||||
boolean enableWeiboImageRecordAndDownload =
|
||||
spec.isEnableWeiboImageRecordAndDownload();
|
||||
List<String> disableSyncList = spec.getDisableSyncList();
|
||||
String imageSavePath = spec.getImageSavePath();
|
||||
String imageBaseUrl = spec.getImageBaseUrl();
|
||||
String imageReferer = spec.getImageReferer();
|
||||
int sum = successfulRetainLimit == 0 ? 5 : successfulRetainLimit;
|
||||
var listOptions = new ListOptions();
|
||||
FieldSelector fieldSelector = FieldSelector.of(isNull("metadata.deletionTimestamp"));
|
||||
@@ -90,7 +101,11 @@ public class RssFeedSyncLogServiceImpl implements RssFeedSyncLogService {
|
||||
}
|
||||
listOptions.setFieldSelector(fieldSelector);
|
||||
return client.listAll(Link.class, listOptions, Sort.by("metadata.creationTimestamp"))
|
||||
.doOnNext(link -> eventPublisher.publishEvent(new RssFeedSyncEvent(this, link,sum,disableSyncList)))
|
||||
.doOnNext(link -> eventPublisher.publishEvent(
|
||||
new RssFeedSyncEvent(this, link, sum, parallelCount,
|
||||
enableWeiboImageRecordAndDownload, disableSyncList, imageSavePath,
|
||||
imageBaseUrl, imageReferer)
|
||||
))
|
||||
.then();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" th:content="${friend.spec.description ?: '无内容'}">
|
||||
<title th:text="${site.title + '-' + (friend.spec.title ?: friend.spec.author)}"></title>
|
||||
<link data-n-head="true" rel="icon" type="image/x-icon" th:href="${site.favicon}">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
color: #242424;
|
||||
background: #f7f7f7;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
||||
Arial, "Noto Sans", sans-serif;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 820px;
|
||||
margin: 60px auto 32px;
|
||||
padding: 56px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 20px 0 hsla(0, 0%, 93%, .86);
|
||||
}
|
||||
|
||||
.back {
|
||||
display: inline-block;
|
||||
margin-bottom: 28px;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 18px;
|
||||
color: #1f1f1f;
|
||||
font-size: 28px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 18px;
|
||||
margin-bottom: 36px;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.author img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.content {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin: 0 auto 40px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.wrapper {
|
||||
margin: 0;
|
||||
padding: 32px 22px;
|
||||
min-height: 100vh;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="wrapper">
|
||||
<a class="back" href="/friends">返回朋友圈</a>
|
||||
<h1 th:text="${friend.spec.title ?: '无内容'}"></h1>
|
||||
<div class="meta">
|
||||
<span class="author">
|
||||
<img th:if="${friend.spec.logo != null and not #strings.isEmpty(friend.spec.logo)}"
|
||||
th:src="${friend.spec.logo}" alt="">
|
||||
<a th:if="${friend.spec.authorUrl != null and not #strings.isEmpty(friend.spec.authorUrl)}"
|
||||
th:href="${friend.spec.authorUrl}" target="_blank" rel="noopener noreferrer"
|
||||
th:text="${friend.spec.author}"></a>
|
||||
<span th:unless="${friend.spec.authorUrl != null and not #strings.isEmpty(friend.spec.authorUrl)}"
|
||||
th:text="${friend.spec.author}"></span>
|
||||
</span>
|
||||
<time th:if="${friend.spec.pubDate != null}"
|
||||
th:datetime="${#dates.format(friend.spec.pubDate,'yyyy-MM-dd')}"
|
||||
th:text="${#dates.format(friend.spec.pubDate,'yyyy.MM.dd')}"></time>
|
||||
</div>
|
||||
<div class="content" th:text="${friend.spec.description ?: '无内容'}"></div>
|
||||
<div class="actions" th:if="${friend.spec.postLink != null and not #strings.isEmpty(friend.spec.postLink)}">
|
||||
<a th:href="${friend.spec.postLink}" target="_blank" rel="noopener noreferrer">查看原文</a>
|
||||
</div>
|
||||
</article>
|
||||
<footer>
|
||||
<span>©2024 [[${site.title}]]</span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1511,9 +1511,10 @@
|
||||
<main>
|
||||
<section class="article-list">
|
||||
<article th:each="friend : ${friends.items}">
|
||||
<h2><a th:href="${friend.spec.postLink}" target="_blank" th:text="${friend.spec.title}"></a></h2>
|
||||
<h2><a th:href="@{/friends/{name}(name=${friend.metadata.name})}"
|
||||
th:text="${friend.spec.title ?: '无内容'}"></a></h2>
|
||||
<div class="excerpt">
|
||||
<p th:text="${friend.spec.description}"></p>
|
||||
<p th:text="${friend.spec.description ?: '无内容'}"></p>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="item">
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@kunkunyu/fridends-rss": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "^1.4.3",
|
||||
"@halo-dev/ui-plugin-bundler-kit": "^2.21.2",
|
||||
"@iconify/json": "^2.2.224",
|
||||
"@rspack/cli": "1.4.0-beta.0",
|
||||
|
||||
@@ -32,6 +32,36 @@ export interface CronSpec {
|
||||
* @memberof CronSpec
|
||||
*/
|
||||
'disableSyncList'?: Array<string>;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof CronSpec
|
||||
*/
|
||||
'parallelCount'?: number;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof CronSpec
|
||||
*/
|
||||
'enableWeiboImageRecordAndDownload'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CronSpec
|
||||
*/
|
||||
'imageSavePath'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CronSpec
|
||||
*/
|
||||
'imageBaseUrl'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CronSpec
|
||||
*/
|
||||
'imageReferer'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
|
||||
@@ -38,6 +38,12 @@ export interface FriendPostSpec {
|
||||
* @memberof FriendPostSpec
|
||||
*/
|
||||
'description'?: string;
|
||||
/**
|
||||
* Comma-separated image file names.
|
||||
* @type {string}
|
||||
* @memberof FriendPostSpec
|
||||
*/
|
||||
'imageFiles'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
||||
@@ -18,7 +18,12 @@ const initialFormState: CronFriendPost = {
|
||||
cron: "@daily",
|
||||
timezone:"Asia/Shanghai",
|
||||
suspend: false,
|
||||
enableWeiboImageRecordAndDownload: false,
|
||||
parallelCount: 1,
|
||||
successfulRetainLimit: 0,
|
||||
imageSavePath: "",
|
||||
imageBaseUrl: "",
|
||||
imageReferer: "",
|
||||
disableSyncList: undefined,
|
||||
},
|
||||
kind: "CronFriendPost",
|
||||
@@ -130,6 +135,20 @@ const { mutate:save, isLoading:saveIsLoading } = useMutation({
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<FormKit
|
||||
type="number"
|
||||
name="parallelCount"
|
||||
label="并行请求数量"
|
||||
number="integer"
|
||||
validation="required|number|min:1"
|
||||
help="默认 1(串行),大于 1 为并行"
|
||||
/>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="enableWeiboImageRecordAndDownload"
|
||||
label="记录并下载微博图片"
|
||||
help="默认关闭,开启后才会记录 imageFiles 字段并下载微博图片"
|
||||
/>
|
||||
<FormKit
|
||||
type="number"
|
||||
name="successfulRetainLimit"
|
||||
@@ -138,6 +157,24 @@ const { mutate:save, isLoading:saveIsLoading } = useMutation({
|
||||
validation="required|number|min:0"
|
||||
help="设置之后会保留的数据条数,设置为 0 即为5条"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="imageSavePath"
|
||||
label="图片保存路径"
|
||||
help="为空时使用默认路径 /root/.halo2/attachments/upload/image-host/"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="imageBaseUrl"
|
||||
label="图片下载 Base URL"
|
||||
help="为空时默认 https://wx3.sinaimg.cn/middle/"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="imageReferer"
|
||||
label="图片下载 Referer"
|
||||
help="当 Base URL 为空时默认 https://weibo.com/;当 Base URL 不为空时可留空"
|
||||
/>
|
||||
<LinkFormKit
|
||||
name="disableSyncList"
|
||||
label="禁止同步名单"
|
||||
|
||||
Reference in New Issue
Block a user