diff --git a/docs/install/README.md b/docs/install/README.md
index 0700d7252a..85b3b7508b 100644
--- a/docs/install/README.md
+++ b/docs/install/README.md
@@ -425,3 +425,10 @@ RSSHub 支持 `memory` 和 `redis` 两种缓存方式
- `WEIBO_APP_KEY`: 微博 App Key
- `WEIBO_APP_SECRET`: 微博 App Secret
- `WEIBO_REDIRECT_URL`: 微博登录授权回调地址,默认为 `RSSHub地址/weibo/timeline/0`,自定义回调地址请确保最后可以转跳到 `RSSHub地址/weibo/timeline/0?code=xxx`
+
+- 饭否 全部路由: [申请地址](https://github.com/FanfouAPI/FanFouAPIDoc/wiki/Oauth)
+
+ - `FANFOU_CONSUMER_KEY`: 饭否 Consumer Key
+ - `FANFOU_CONSUMER_SECRET`: 饭否 Consumer Secret
+ - `FANFOU_USERNAME`: 饭否登录用户名、邮箱、手机号
+ - `FANFOU_PASSWORD`: 饭否密码
diff --git a/docs/social-media.md b/docs/social-media.md
index 67fba73cab..43ffda8fd4 100644
--- a/docs/social-media.md
+++ b/docs/social-media.md
@@ -510,6 +510,32 @@ pageClass: routes
+## 饭否
+
+::: warning 注意
+部署时需要申请并配置饭否 Consumer Key、Consumer Secret、用户名和密码,具体见部署文档
+:::
+
+### 用户动态
+
+
+
+### 当前登录用户的时间线
+
+
+
+### 用户收藏
+
+
+
+### 热门话题
+
+
+
+### 饭否搜索
+
+
+
## 方格子
### 出版專題
diff --git a/lib/config.js b/lib/config.js
index f5302a6205..a1f39b6423 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -100,6 +100,12 @@ const calculateValue = () => {
app_secret: envs.WEIBO_APP_SECRET,
redirect_url: envs.WEIBO_REDIRECT_URL,
},
+ fanfou: {
+ consumer_key: envs.FANFOU_CONSUMER_KEY,
+ consumer_secret: envs.FANFOU_CONSUMER_SECRET,
+ username: envs.FANFOU_USERNAME,
+ password: envs.FANFOU_PASSWORD,
+ },
};
};
calculateValue();
diff --git a/lib/router.js b/lib/router.js
index 542632b4a9..53443f5997 100644
--- a/lib/router.js
+++ b/lib/router.js
@@ -1953,4 +1953,11 @@ router.get('/alter-cn/news', require('./routes/alter-cn/news'));
// Visual Studio Code Marketplace
router.get('/vscode/marketplace/:type?', require('./routes/vscode/marketplace'));
+// 饭否
+router.get('/fanfou/user_timeline/:uid', require('./routes/fanfou/user_timeline'));
+router.get('/fanfou/home_timeline', require('./routes/fanfou/home_timeline'));
+router.get('/fanfou/favorites/:uid', require('./routes/fanfou/favorites'));
+router.get('/fanfou/trends', require('./routes/fanfou/trends'));
+router.get('/fanfou/public_timeline/:keyword', require('./routes/fanfou/public_timeline'));
+
module.exports = router;
diff --git a/lib/routes/fanfou/favorites.js b/lib/routes/fanfou/favorites.js
new file mode 100644
index 0000000000..49161c63f9
--- /dev/null
+++ b/lib/routes/fanfou/favorites.js
@@ -0,0 +1,39 @@
+const config = require('@/config').value;
+const utils = require('./utils');
+
+module.exports = async (ctx) => {
+ if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
+ throw 'Fanfou RSS is disabled due to the lack of relevant config';
+ }
+
+ const uid = ctx.params.uid;
+ const fanfou = await utils.getFanfou();
+ const timeline = await fanfou.get(`/favorites/${uid}`, { id: uid, mode: 'lite', format: 'html' });
+
+ const result = await Promise.all(
+ timeline.map(async (item) => {
+ let img_html = '';
+ if (item.photo) {
+ img_html = `
`;
+ }
+ const voItem = {
+ title: item.text,
+ author: item.user.name,
+ description: item.text + img_html,
+ pubDate: item.created_at,
+ link: `https://fanfou.com/statuses/${item.id}`,
+ };
+ return Promise.resolve(voItem);
+ })
+ );
+
+ const users = await fanfou.get(`/users/show`, { id: uid });
+ const authorName = users.screen_name;
+
+ ctx.state.data = {
+ title: `${authorName}的饭否收藏`,
+ link: `https://fanfou.com/favorites/${uid}`,
+ description: `${authorName}的饭否收藏`,
+ item: result,
+ };
+};
diff --git a/lib/routes/fanfou/home_timeline.js b/lib/routes/fanfou/home_timeline.js
new file mode 100644
index 0000000000..9ff4b7b8ae
--- /dev/null
+++ b/lib/routes/fanfou/home_timeline.js
@@ -0,0 +1,35 @@
+const config = require('@/config').value;
+const utils = require('./utils');
+
+module.exports = async (ctx) => {
+ if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
+ throw 'Fanfou RSS is disabled due to the lack of relevant config';
+ }
+
+ const fanfou = await utils.getFanfou();
+ const timeline = await fanfou.get('/statuses/home_timeline', { mode: 'lite', format: 'html' });
+
+ const result = await Promise.all(
+ timeline.map(async (item) => {
+ let img_html = '';
+ if (item.photo) {
+ img_html = `
`;
+ }
+ const voItem = {
+ title: item.plain_text,
+ author: item.user.name,
+ description: item.text + img_html,
+ pubDate: item.created_at,
+ link: `http://fanfou.com/statuses/${item.id}`,
+ };
+ return Promise.resolve(voItem);
+ })
+ );
+
+ ctx.state.data = {
+ title: `我的饭否动态`,
+ link: `https://fanfou.com/home`,
+ description: `我的饭否动态`,
+ item: result,
+ };
+};
diff --git a/lib/routes/fanfou/public_timeline.js b/lib/routes/fanfou/public_timeline.js
new file mode 100644
index 0000000000..e04f649c16
--- /dev/null
+++ b/lib/routes/fanfou/public_timeline.js
@@ -0,0 +1,36 @@
+const config = require('@/config').value;
+const utils = require('./utils');
+
+module.exports = async (ctx) => {
+ if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
+ throw 'Fanfou RSS is disabled due to the lack of relevant config';
+ }
+
+ const keyword = ctx.params.keyword;
+ const fanfou = await utils.getFanfou();
+ const timeline = await fanfou.get('/search/public_timeline', { q: keyword, mode: 'lite', format: 'html' });
+
+ const result = await Promise.all(
+ timeline.map(async (item) => {
+ let img_html = '';
+ if (item.photo) {
+ img_html = `
`;
+ }
+ const voItem = {
+ title: item.plain_text,
+ author: item.user.name,
+ description: item.text + img_html,
+ pubDate: item.created_at,
+ link: `https://fanfou.com/statuses/${item.id}`,
+ };
+ return Promise.resolve(voItem);
+ })
+ );
+
+ ctx.state.data = {
+ title: `饭否搜索-${keyword}`,
+ link: `https://fanfou.com/q/${keyword}`,
+ description: `饭否搜索-${keyword}`,
+ item: result,
+ };
+};
diff --git a/lib/routes/fanfou/trends.js b/lib/routes/fanfou/trends.js
new file mode 100644
index 0000000000..68f6342945
--- /dev/null
+++ b/lib/routes/fanfou/trends.js
@@ -0,0 +1,25 @@
+const utils = require('./utils');
+
+module.exports = async (ctx) => {
+ const fanfou = await utils.getFanfou();
+ const response = await fanfou.get(`/trends/list`);
+
+ const result = await Promise.all(
+ response.trends.map(async (item) => {
+ const voItem = {
+ title: item.name,
+ description: item.name,
+ pubDate: new Date(),
+ link: item.url,
+ };
+ return Promise.resolve(voItem);
+ })
+ );
+
+ ctx.state.data = {
+ title: `饭否热门话题`,
+ link: `https://fanfou.com/q/`,
+ description: `饭否热门话题`,
+ item: result,
+ };
+};
diff --git a/lib/routes/fanfou/user_timeline.js b/lib/routes/fanfou/user_timeline.js
new file mode 100644
index 0000000000..f28260959f
--- /dev/null
+++ b/lib/routes/fanfou/user_timeline.js
@@ -0,0 +1,38 @@
+const config = require('@/config').value;
+const utils = require('./utils');
+
+module.exports = async (ctx) => {
+ if (!config.fanfou || !config.fanfou.consumer_key || !config.fanfou.consumer_secret || !config.fanfou.username || !config.fanfou.password) {
+ throw 'Fanfou RSS is disabled due to the lack of relevant config';
+ }
+
+ const uid = ctx.params.uid;
+ const fanfou = await utils.getFanfou();
+ const timeline = await fanfou.get('/statuses/user_timeline', { id: uid, mode: 'lite', format: 'html' });
+
+ const result = await Promise.all(
+ timeline.map(async (item) => {
+ let img_html = '';
+ if (item.photo) {
+ img_html = `
`;
+ }
+ const voItem = {
+ title: item.plain_text,
+ author: item.user.name,
+ description: item.text + img_html,
+ pubDate: item.created_at,
+ link: `https://fanfou.com/statuses/${item.id}`,
+ };
+ return Promise.resolve(voItem);
+ })
+ );
+
+ const authorName = result[0].author;
+
+ ctx.state.data = {
+ title: `${authorName}的饭否`,
+ link: `https://fanfou.com/${uid}`,
+ description: `${authorName}的饭否`,
+ item: result,
+ };
+};
diff --git a/lib/routes/fanfou/utils.js b/lib/routes/fanfou/utils.js
new file mode 100644
index 0000000000..a1353e385a
--- /dev/null
+++ b/lib/routes/fanfou/utils.js
@@ -0,0 +1,34 @@
+const logger = require('@/utils/logger');
+const config = require('@/config').value;
+const Fanfou = require('fanfou-sdk');
+
+const consumerKey = config.fanfou.consumer_key;
+const consumerSecret = config.fanfou.consumer_secret;
+const username = config.fanfou.username;
+const password = config.fanfou.password;
+
+let fanfou_client;
+let authed = false;
+
+const getFanfou = async () => {
+ if (authed === true) {
+ return fanfou_client;
+ } else {
+ fanfou_client = new Fanfou({
+ consumerKey,
+ consumerSecret,
+ username,
+ password,
+ });
+
+ await fanfou_client.xauth();
+ logger.info('Fanfou login success.');
+
+ authed = true;
+ return fanfou_client;
+ }
+};
+
+module.exports = {
+ getFanfou,
+};
diff --git a/package.json b/package.json
index 5b8cef5470..a7d366b810 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"dotenv": "8.2.0",
"emailjs-imap-client": "3.0.7",
"etag": "1.8.1",
+ "fanfou-sdk": "4.0.5",
"form-data": "3.0.0",
"git-rev-sync": "1.12.0",
"googleapis": "45.0.0",
diff --git a/yarn.lock b/yarn.lock
index cafbdb6eda..b57acade20 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4458,6 +4458,18 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+fanfou-sdk@4.0.5:
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/fanfou-sdk/-/fanfou-sdk-4.0.5.tgz#5b3f2ab5ee8a383fc21ab58bc6c1ed97cbce7113"
+ integrity sha512-s3Ec5D3Am5daLQtu9IwkVWPSV40Tg//jPJVAYbLVMITLvCgq58H2ufJ5bgfsQAJrQXVL0ZhkN+mwhgp/4BJzXA==
+ dependencies:
+ form-data "^2.3.3"
+ got "^9.5.1"
+ he "^1.1.1"
+ hmacsha1 "^1.0.0"
+ oauth-1.0a "^2.2.5"
+ query-string "^6.2.0"
+
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
@@ -4682,7 +4694,7 @@ form-data@3.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
-form-data@^2.3.1, form-data@^2.5.0:
+form-data@^2.3.1, form-data@^2.3.3, form-data@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
@@ -5046,7 +5058,7 @@ googleapis@45.0.0:
google-auth-library "^5.2.0"
googleapis-common "^3.1.0"
-got@9.6.0:
+got@9.6.0, got@^9.5.1:
version "9.6.0"
resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
@@ -5225,7 +5237,7 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
-he@1.2.0, he@1.2.x, he@^1.1.0, he@^1.2.0:
+he@1.2.0, he@1.2.x, he@^1.1.0, he@^1.1.1, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -5249,6 +5261,11 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
+hmacsha1@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hmacsha1/-/hmacsha1-1.0.0.tgz#c1b7ae03a4ea116348090af14f8148c12938a917"
+ integrity sha1-wbeuA6TqEWNICQrxT4FIwSk4qRc=
+
hogan.js@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd"
@@ -7871,6 +7888,11 @@ nwsapi@^2.0.7, nwsapi@^2.2.0:
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
+oauth-1.0a@^2.2.5:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz#eadbccdb3bceea412d24586e6f39b2b412f0e491"
+ integrity sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==
+
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
@@ -9039,7 +9061,7 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
-query-string@6.9.0:
+query-string@6.9.0, query-string@^6.2.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.9.0.tgz#1c3b727c370cf00f177c99f328fda2108f8fa3dd"
integrity sha512-KG4bhCFYapExLsUHrFt+kQVEegF2agm4cpF/VNc6pZVthIfCc/GK8t8VyNIE3nyXG9DK3Tf2EGkxjR6/uRdYsA==