liangyue commited on
Commit
bab5ad5
1 Parent(s): 2e74677
README.md CHANGED
@@ -1,12 +1,141 @@
1
- ---
2
- title: Wx Rss
3
- emoji: 🔥
4
- colorFrom: red
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- app_port: 4000
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/cooderl/wewe-rss/main/assets/logo.png" width="80" alt="预览"/>
3
+
4
+ <h1 align="center"><a href="https://github.com/cooderl/wewe-rss">WeWe RSS</a></h1>
5
+
6
+ 更优雅的微信公众号订阅方式。
7
+
8
+ ![主界面](https://raw.githubusercontent.com/cooderl/wewe-rss/main/assets/preview1.png)
9
+
10
+ </div>
11
+
12
+ ## 功能
13
+
14
+ - [x] 支持微信公众号订阅(基于微信读书)
15
+ - [x] 后台自动定时更新内容
16
+ - [x] 微信公众号RSS生成(支持`.atom`\.`rss`\.`json`格式)
17
+ - [x] 支持全文内容输出,让阅读无障碍
18
+ - [x] 所有订阅源导出OPML
19
+
20
+ ## 部署
21
+
22
+ ### 一键部署(待完善添加模板)
23
+
24
+ 你可以通过以下平台一键部署,只需填写本项目的URL即可。
25
+
26
+ [Zeabur](https://zeabur.com/)
27
+
28
+ [Railway](https://railway.app/)
29
+
30
+ [Hugging Face部署参考](https://github.com/cooderl/wewe-rss/issues/32)
31
+
32
+ ### Docker Compose 部署
33
+
34
+ 可参考 [docker-compose.yml](https://github.com/cooderl/wewe-rss/blob/main/docker-compose.yml) 和 [docker-compose.sqlite.yml](https://github.com/cooderl/wewe-rss/blob/main/docker-compose.sqlite.yml)
35
+
36
+ ### Docker 命令启动
37
+
38
+ #### Sqlite
39
+
40
+ ```sh
41
+ docker run -d \
42
+ --name wewe-rss \
43
+ -p 4000:4000 \
44
+ -e DATABASE_TYPE=sqlite \
45
+ -e AUTH_CODE=123567 \
46
+ -v $(pwd)/data:/app/data \
47
+ cooderl/wewe-rss-sqlite:latest
48
+ ```
49
+
50
+ #### Mysql
51
+
52
+ 1. 创建docker网络
53
+
54
+ ```sh
55
+ docker network create wewe-rss
56
+ ```
57
+
58
+ 2. 启动 MySQL 数据库
59
+
60
+ ```sh
61
+ docker run -d \
62
+ --name db \
63
+ -e MYSQL_ROOT_PASSWORD=123456 \
64
+ -e TZ='Asia/Shanghai' \
65
+ -e MYSQL_DATABASE='wewe-rss' \
66
+ -v db_data:/var/lib/mysql \
67
+ --network wewe-rss \
68
+ mysql:latest --default-authentication-plugin=mysql_native_password
69
+ ```
70
+
71
+ 3. 启动 Server
72
+
73
+ ```sh
74
+ docker run -d \
75
+ --name wewe-rss \
76
+ -p 4000:4000 \
77
+ -e DATABASE_URL='mysql://root:123456@db:3306/wewe-rss?schema=public&connect_timeout=30&pool_timeout=30&socket_timeout=30' \
78
+ -e AUTH_CODE=123567 \
79
+ --network wewe-rss \
80
+ cooderl/wewe-rss:latest
81
+
82
+ ```
83
+
84
+ [Nginx配置参考](https://raw.githubusercontent.com/cooderl/wewe-rss/main/assets/nginx.example.conf)
85
+
86
+ ### 本地部署
87
+
88
+ 如果你想本地部署,请使用 `pnpm install && pnpm run -r build && pnpm run start:server` 命令(可以配合 pm2 来守护进程,防止被杀死)。
89
+
90
+ ## 环境变量
91
+
92
+ - `DATABASE_URL` (**必填项**)数据库地址,例如 `mysql://root:123456@127.0.0.1:3306/wewe-rss`。
93
+
94
+ - `DATABASE_TYPE` 数据库类型,使用 `sqlite` 时需要填写 `sqlite`。
95
+
96
+ - `AUTH_CODE` 服务端接口请求授权码,(`/feeds`路径不需要)。
97
+
98
+ - `SERVER_ORIGIN_URL` 服务端访问地址,用于生成RSS的完整路径(外网访问时,设置为服务器的公网 IP 或者域名地址)。
99
+
100
+ - `MAX_REQUEST_PER_MINUTE` 每分钟最大请求次数,默认 60。
101
+
102
+ - `FEED_MODE` 输出模式,可选值 `fulltext`(RSS全文模式会使接口响应会变慢,占用更多内存)。
103
+
104
+ - `CRON_EXPRESSION` 定时更新订阅源Cron表达式,默认为 `35 5,17 * * *`。
105
+
106
+
107
+ ## 使用方式
108
+
109
+ 1. 进入账号管理,点击添加账号,微信扫码登录微信读书账号。
110
+ <img width="400" src="./assets/preview2.png"/>
111
+
112
+ 1. 进入公众号源,点击添加,通过提交微信公众号分享链接,订阅微信公众号。
113
+ **(添加频率过高容易被封控,等24小时解封)**
114
+ <img width="400" src="./assets/preview3.png"/>
115
+
116
+
117
+ ## 本地开发
118
+
119
+ 1. 安装 nodejs 18 和 pnpm;
120
+ 2. 修改环境变量`cp ./apps/web/.env.local.example ./apps/web/.env`和`cp ./apps/server/.env.local.example ./apps/server/.env`
121
+ 3. 执行 `pnpm install && pnpm dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
122
+ 4. 前端访问 `http://localhost:5173` ,后端访问 `http://localhost:4000`
123
+
124
+ ## 风险声明
125
+
126
+ 为了确保本项目的持久运行,某些接口请求将通过`weread.111965.xyz`进行转发。请放心,该转发服务不会保存任何数据。
127
+
128
+ ## 打赏
129
+
130
+ 如果您觉得我们的项目有价值,并希望帮助我们继续发展,可以用以下几种加密货币打赏:
131
+
132
+ BTC(Bitcoin): `1DGU9zRC8cvexq3W92Kzxqg5sNnbWPz9fE`
133
+
134
+ ETH(Ethereum, ERC20): `0x6bb8cef666c346ac3926fd32edd27d8246dcece0`
135
+
136
+ USDT(Tron, TRC20): `TLsukYHcXN34RXABZwppRE5AuPp8AWY7Wv`
137
+
138
+
139
+ ## License
140
+
141
+ [MIT](https://raw.githubusercontent.com/cooderl/wewe-rss/main/LICENSE) @cooderl
apps/server/package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "server",
3
- "version": "1.7.1",
4
  "description": "",
5
  "author": "",
6
  "private": true,
 
1
  {
2
  "name": "server",
3
+ "version": "1.8.0",
4
  "description": "",
5
  "author": "",
6
  "private": true,
apps/server/src/app.controller.ts CHANGED
@@ -1,9 +1,14 @@
1
  import { Controller, Get, Redirect, Render } from '@nestjs/common';
2
  import { AppService } from './app.service';
 
 
3
 
4
  @Controller()
5
  export class AppController {
6
- constructor(private readonly appService: AppService) {}
 
 
 
7
 
8
  @Get()
9
  getHello(): string {
@@ -23,9 +28,12 @@ export class AppController {
23
  @Render('index.hbs')
24
  dashRender() {
25
  const { originUrl: weweRssServerOriginUrl } =
26
- this.appService.getFeedConfig();
 
 
27
  return {
28
  weweRssServerOriginUrl,
 
29
  };
30
  }
31
  }
 
1
  import { Controller, Get, Redirect, Render } from '@nestjs/common';
2
  import { AppService } from './app.service';
3
+ import { ConfigService } from '@nestjs/config';
4
+ import { ConfigurationType } from './configuration';
5
 
6
  @Controller()
7
  export class AppController {
8
+ constructor(
9
+ private readonly appService: AppService,
10
+ private readonly configService: ConfigService,
11
+ ) {}
12
 
13
  @Get()
14
  getHello(): string {
 
28
  @Render('index.hbs')
29
  dashRender() {
30
  const { originUrl: weweRssServerOriginUrl } =
31
+ this.configService.get<ConfigurationType['feed']>('feed')!;
32
+ const { code } = this.configService.get<ConfigurationType['auth']>('auth')!;
33
+
34
  return {
35
  weweRssServerOriginUrl,
36
+ enabledAuthCode: !!code,
37
  };
38
  }
39
  }
apps/server/src/app.service.ts CHANGED
@@ -1,6 +1,5 @@
1
  import { Injectable } from '@nestjs/common';
2
  import { ConfigService } from '@nestjs/config';
3
- import { ConfigurationType } from './configuration';
4
 
5
  @Injectable()
6
  export class AppService {
@@ -12,8 +11,4 @@ export class AppService {
12
  </div>
13
  `;
14
  }
15
-
16
- getFeedConfig() {
17
- return this.configService.get<ConfigurationType['feed']>('feed')!;
18
- }
19
  }
 
1
  import { Injectable } from '@nestjs/common';
2
  import { ConfigService } from '@nestjs/config';
 
3
 
4
  @Injectable()
5
  export class AppService {
 
11
  </div>
12
  `;
13
  }
 
 
 
 
14
  }
apps/server/src/trpc/trpc.router.ts CHANGED
@@ -22,12 +22,12 @@ export class TrpcRouter {
22
  list: this.trpcService.protectedProcedure
23
  .input(
24
  z.object({
25
- limit: z.number().min(1).max(100).nullish(),
26
  cursor: z.string().nullish(),
27
  }),
28
  )
29
  .query(async ({ input }) => {
30
- const limit = input.limit ?? 50;
31
  const { cursor } = input;
32
 
33
  const items = await this.prismaService.account.findMany({
@@ -132,12 +132,12 @@ export class TrpcRouter {
132
  list: this.trpcService.protectedProcedure
133
  .input(
134
  z.object({
135
- limit: z.number().min(1).max(100).nullish(),
136
  cursor: z.string().nullish(),
137
  }),
138
  )
139
  .query(async ({ input }) => {
140
- const limit = input.limit ?? 50;
141
  const { cursor } = input;
142
 
143
  const items = await this.prismaService.feed.findMany({
@@ -247,13 +247,13 @@ export class TrpcRouter {
247
  list: this.trpcService.protectedProcedure
248
  .input(
249
  z.object({
250
- limit: z.number().min(1).max(100).nullish(),
251
  cursor: z.string().nullish(),
252
  mpId: z.string().nullish(),
253
  }),
254
  )
255
  .query(async ({ input }) => {
256
- const limit = input.limit ?? 50;
257
  const { cursor, mpId } = input;
258
 
259
  const items = await this.prismaService.article.findMany({
@@ -401,7 +401,7 @@ export class TrpcRouter {
401
  const authCode =
402
  this.configService.get<ConfigurationType['auth']>('auth')!.code;
403
 
404
- if (req.headers.authorization !== authCode) {
405
  return {
406
  errorMsg: 'authCode不正确!',
407
  };
 
22
  list: this.trpcService.protectedProcedure
23
  .input(
24
  z.object({
25
+ limit: z.number().min(1).max(500).nullish(),
26
  cursor: z.string().nullish(),
27
  }),
28
  )
29
  .query(async ({ input }) => {
30
+ const limit = input.limit ?? 500;
31
  const { cursor } = input;
32
 
33
  const items = await this.prismaService.account.findMany({
 
132
  list: this.trpcService.protectedProcedure
133
  .input(
134
  z.object({
135
+ limit: z.number().min(1).max(500).nullish(),
136
  cursor: z.string().nullish(),
137
  }),
138
  )
139
  .query(async ({ input }) => {
140
+ const limit = input.limit ?? 500;
141
  const { cursor } = input;
142
 
143
  const items = await this.prismaService.feed.findMany({
 
247
  list: this.trpcService.protectedProcedure
248
  .input(
249
  z.object({
250
+ limit: z.number().min(1).max(500).nullish(),
251
  cursor: z.string().nullish(),
252
  mpId: z.string().nullish(),
253
  }),
254
  )
255
  .query(async ({ input }) => {
256
+ const limit = input.limit ?? 500;
257
  const { cursor, mpId } = input;
258
 
259
  const items = await this.prismaService.article.findMany({
 
401
  const authCode =
402
  this.configService.get<ConfigurationType['auth']>('auth')!.code;
403
 
404
+ if (authCode && req.headers.authorization !== authCode) {
405
  return {
406
  errorMsg: 'authCode不正确!',
407
  };
apps/web/index.html CHANGED
@@ -11,6 +11,7 @@
11
  <div id="root"></div>
12
  <script>
13
  window.__WEWE_RSS_SERVER_ORIGIN_URL__ = '{{ weweRssServerOriginUrl }}';
 
14
  </script>
15
  <script type="module" src="/src/main.tsx"></script>
16
  </body>
 
11
  <div id="root"></div>
12
  <script>
13
  window.__WEWE_RSS_SERVER_ORIGIN_URL__ = '{{ weweRssServerOriginUrl }}';
14
+ window.__WEWE_RSS_ENABLED_AUTH_CODE__ = {{ enabledAuthCode }};
15
  </script>
16
  <script type="module" src="/src/main.tsx"></script>
17
  </body>
apps/web/package.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "web",
3
  "private": true,
4
- "version": "1.7.1",
5
  "type": "module",
6
  "scripts": {
7
  "dev": "vite",
 
1
  {
2
  "name": "web",
3
  "private": true,
4
+ "version": "1.8.0",
5
  "type": "module",
6
  "scripts": {
7
  "dev": "vite",
apps/web/src/pages/accounts/index.tsx CHANGED
@@ -25,9 +25,7 @@ import { statusMap } from '@web/constants';
25
  const AccountPage = () => {
26
  const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure();
27
 
28
- const { refetch, data, isFetching } = trpc.account.list.useQuery({
29
- limit: 100,
30
- });
31
 
32
  const { mutateAsync: updateAccount } = trpc.account.edit.useMutation({});
33
 
 
25
  const AccountPage = () => {
26
  const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure();
27
 
28
+ const { refetch, data, isFetching } = trpc.account.list.useQuery({});
 
 
29
 
30
  const { mutateAsync: updateAccount } = trpc.account.edit.useMutation({});
31
 
apps/web/src/pages/feeds/index.tsx CHANGED
@@ -30,9 +30,7 @@ const Feeds = () => {
30
 
31
  const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
32
  const { refetch: refetchFeedList, data: feedData } = trpc.feed.list.useQuery(
33
- {
34
- limit: 100,
35
- },
36
  {
37
  refetchOnWindowFocus: true,
38
  },
@@ -93,6 +91,38 @@ const Feeds = () => {
93
  return feedData?.items.find((item) => item.id === currentMpId);
94
  }, [currentMpId, feedData?.items]);
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  return (
97
  <>
98
  <div className="h-full flex justify-between">
@@ -238,15 +268,26 @@ const Feeds = () => {
238
  </Tooltip>
239
  </div>
240
  ) : (
241
- <Link
242
- size="sm"
243
- showAnchorIcon
244
- target="_blank"
245
- href={`${serverOriginUrl}/feeds/all.atom`}
246
- color="foreground"
247
- >
248
- RSS
249
- </Link>
 
 
 
 
 
 
 
 
 
 
 
250
  )}
251
  </div>
252
  <div className="p-2 overflow-y-auto">
 
30
 
31
  const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
32
  const { refetch: refetchFeedList, data: feedData } = trpc.feed.list.useQuery(
33
+ {},
 
 
34
  {
35
  refetchOnWindowFocus: true,
36
  },
 
91
  return feedData?.items.find((item) => item.id === currentMpId);
92
  }, [currentMpId, feedData?.items]);
93
 
94
+ const handleExportOpml = async (ev) => {
95
+ ev.preventDefault();
96
+ ev.stopPropagation();
97
+ if (!feedData?.items?.length) {
98
+ console.warn('没有订阅源');
99
+ return;
100
+ }
101
+
102
+ let opmlContent = `<?xml version="1.0" encoding="UTF-8"?>
103
+ <opml version="2.0">
104
+ <head>
105
+ <title>WeWeRSS 所有订阅源</title>
106
+ </head>
107
+ <body>
108
+ `;
109
+
110
+ feedData?.items.forEach((sub) => {
111
+ opmlContent += ` <outline text="${sub.mpName}" type="rss" xmlUrl="${window.location.origin}/feeds/${sub.id}.atom" htmlUrl="${window.location.origin}/feeds/${sub.id}.atom"/>\n`;
112
+ });
113
+
114
+ opmlContent += ` </body>
115
+ </opml>`;
116
+
117
+ const blob = new Blob([opmlContent], { type: 'text/xml;charset=utf-8;' });
118
+ const link = document.createElement('a');
119
+ link.href = URL.createObjectURL(blob);
120
+ link.download = 'WeWeRSS-All.opml';
121
+ document.body.appendChild(link);
122
+ link.click();
123
+ document.body.removeChild(link);
124
+ };
125
+
126
  return (
127
  <>
128
  <div className="h-full flex justify-between">
 
268
  </Tooltip>
269
  </div>
270
  ) : (
271
+ <div className="flex gap-2">
272
+ <Link
273
+ href="#"
274
+ color="foreground"
275
+ onClick={handleExportOpml}
276
+ size="sm"
277
+ >
278
+ 导出OPML
279
+ </Link>
280
+ <Divider orientation="vertical" />
281
+ <Link
282
+ size="sm"
283
+ showAnchorIcon
284
+ target="_blank"
285
+ href={`${serverOriginUrl}/feeds/all.atom`}
286
+ color="foreground"
287
+ >
288
+ RSS
289
+ </Link>
290
+ </div>
291
  )}
292
  </div>
293
  <div className="p-2 overflow-y-auto">
apps/web/src/provider/trpc.tsx CHANGED
@@ -5,13 +5,19 @@ import { useState } from 'react';
5
  import { toast } from 'sonner';
6
  import { isTRPCClientError, trpc } from '../utils/trpc';
7
  import { getAuthCode, setAuthCode } from '../utils/auth';
8
- import { serverOriginUrl } from '../utils/env';
9
 
10
  export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
11
  children,
12
  }) => {
13
  const navigate = useNavigate();
14
 
 
 
 
 
 
 
15
  const [queryClient] = useState(
16
  () =>
17
  new QueryClient({
@@ -38,8 +44,7 @@ export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
38
  description: error.message,
39
  });
40
 
41
- setAuthCode('');
42
- navigate('/login');
43
  } else {
44
  toast.error('请求失败!', {
45
  description: error.message,
@@ -56,8 +61,7 @@ export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
56
  toast.error('无权限', {
57
  description: error.message,
58
  });
59
- setAuthCode(null);
60
- navigate('/login');
61
  } else {
62
  toast.error('请求失败!', {
63
  description: error.message,
@@ -82,9 +86,10 @@ export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
82
  const token = getAuthCode();
83
 
84
  if (!token) {
85
- navigate('/login');
86
  return {};
87
  }
 
88
  return token
89
  ? {
90
  Authorization: `${token}`,
 
5
  import { toast } from 'sonner';
6
  import { isTRPCClientError, trpc } from '../utils/trpc';
7
  import { getAuthCode, setAuthCode } from '../utils/auth';
8
+ import { enabledAuthCode, serverOriginUrl } from '../utils/env';
9
 
10
  export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
11
  children,
12
  }) => {
13
  const navigate = useNavigate();
14
 
15
+ const handleNoAuth = () => {
16
+ if (enabledAuthCode) {
17
+ setAuthCode('');
18
+ navigate('/login');
19
+ }
20
+ };
21
  const [queryClient] = useState(
22
  () =>
23
  new QueryClient({
 
44
  description: error.message,
45
  });
46
 
47
+ handleNoAuth();
 
48
  } else {
49
  toast.error('请求失败!', {
50
  description: error.message,
 
61
  toast.error('无权限', {
62
  description: error.message,
63
  });
64
+ handleNoAuth();
 
65
  } else {
66
  toast.error('请求失败!', {
67
  description: error.message,
 
86
  const token = getAuthCode();
87
 
88
  if (!token) {
89
+ handleNoAuth();
90
  return {};
91
  }
92
+
93
  return token
94
  ? {
95
  Authorization: `${token}`,
apps/web/src/utils/env.ts CHANGED
@@ -5,3 +5,6 @@ export const serverOriginUrl = isProd
5
  : import.meta.env.VITE_SERVER_ORIGIN_URL;
6
 
7
  export const appVersion = __APP_VERSION__;
 
 
 
 
5
  : import.meta.env.VITE_SERVER_ORIGIN_URL;
6
 
7
  export const appVersion = __APP_VERSION__;
8
+
9
+ export const enabledAuthCode =
10
+ window.__WEWE_RSS_ENABLED_AUTH_CODE__ === false ? false : true;
apps/web/src/vite-env.d.ts CHANGED
@@ -7,6 +7,7 @@ interface ImportMetaEnv {
7
 
8
  interface Window {
9
  __WEWE_RSS_SERVER_ORIGIN_URL__?: string;
 
10
  }
11
 
12
  declare const __APP_VERSION__: string;
 
7
 
8
  interface Window {
9
  __WEWE_RSS_SERVER_ORIGIN_URL__?: string;
10
+ __WEWE_RSS_ENABLED_AUTH_CODE__?: boolean;
11
  }
12
 
13
  declare const __APP_VERSION__: string;
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "wewe-rss",
3
- "version": "1.7.1",
4
  "private": true,
5
  "author": "cooderl <cooder@111965.xyz>",
6
  "description": "",
 
1
  {
2
  "name": "wewe-rss",
3
+ "version": "1.8.0",
4
  "private": true,
5
  "author": "cooderl <cooder@111965.xyz>",
6
  "description": "",