UNPKG

rsshub

Version:
316 lines (292 loc) • 13.4 kB
import { Route, Data } from '@/types'; import got from '@/utils/got'; import { config } from '@/config'; import ConfigNotFoundError from '@/errors/types/config-not-found'; export const route: Route = { path: '/entry/:feeds/:parameters?', description: ` 1. Support to get all content: You can obtain the content of all subscription sources by using keywords such as \`/miniflux/all\` or \`/miniflux/default\`. 2. Support to get the subscription content of a specific subscription source by its ID. Please obtain the subscription source ID on the page where it is located under \`Sources\` (shortcut keys \`g\` \`f\`). The URL for each category (or subscription source) displays its ID information. There are several format options available: 1. Support \`/miniflux/feed=[feed_id]\`, please replace \`[feed_id]\` with the actual ID of the subscribed feed (note that it should be just a number without brackets). 2. Support subscribing to multiple feeds using \`/miniflux/feed=[feed1_id]&feed=[feed2_id]\` or \`/miniflux/feeds=[feed1_id]&[feed2_id]\`. 3. Additionally, you can use shorthand notation by directly using feed IDs: \`/miniflux/[feed1_id]&[feed2_id]\`. 3. Further customization options are available based on your needs: 1. All parameters/options provided by MiniFlux are supported ([link](https://miniflux.app/docs/api.html#endpoint-get-feed-entries)). As noted in their documentation, multiple filtering options should be connected with \`&\`. Except for \`status\`, only the first occurrence of duplicate filter options will be considered. 2. Specifically, this route defaults to sorting entries from new to old (\`direction=desc\`). 3. Moreover, this route supports additional options including: - Using the \`feed_name\` parameter to control title formatting; setting \`feed_name=1\` will display each title as "Article Title | Feed Name," while default is set at \`0\`, showing only article titles. - Utilizing the \`mark\` parameter to specify actions after fetching subscriptions in RSSHub, such as maintaining unchanged state (\`unchanged\`, default), marking as read (\`read\`), removing (\`removed\`) or marking as unread (\`unread\`). Note that marking as read should not simply be understood as a means for implementing synchronization services; rather, it functions more like an aid for MiniFlux's automatic cleaning feature. - Future support may include utilizing the \`link\` parameter to control output URLs (this functionality requires corresponding interfaces from MiniFlux). It could involve generating URLs through MiniFlux entity sharing features or original content links. - The output content quantity can be controlled via the 'limit' parameter; although all matching contents are typically outputted by default, **it is recommended that users set this parameter**. `, categories: ['other'], example: '/miniflux/feeds=1&2&3/mark=read&limit=7&status=unread', parameters: { feeds: 'Subscribe source ID or get all.', parameters: 'Filter and set parameters, use `&` to connect multiple.', }, features: { requireConfig: [ { name: 'MINIFLUX_INSTANCE', description: 'The instance used by the user, by default, is the official MiniFlux [paid service address](https://reader.miniflux.app)', }, { name: 'MINIFLUX_TOKEN', description: "User's API key, please log in to the instance used and go to `Settings` -> `API Key` -> `Create a new API key` to obtain.", }, ], requirePuppeteer: false, antiCrawler: false, supportBT: false, supportPodcast: false, supportScihub: false, }, name: 'Feed entry', maintainers: ['emdoe', 'DIYgod'], handler, }; async function handler(ctx) { // Unchanged entries status after fetching. // mark = unchanged | read | removed | unread let mark = 'unchanged'; // Return shared link as default behavior // link = shared | original // let link = 'shared'; // Add feed's name to each article, default is off. let addFeedName = 0; // Here we use `limit` to temporarily store the limit number. let limit = 0; const instance = config.miniflux.instance; const token = config.miniflux.token; if (!token) { throw new ConfigNotFoundError('This RSS feed is disabled due to its incorrect configuration: the token is missing.'); } // In this function, var`mark`, `link`, and `limit`, `addFeedName` // could be changed. function filterHandler(item) { if (item.search('=') === -1) { return ''; } const filter = item.slice(0, item.indexOf('=')); const option = item.slice(item.lastIndexOf('=') + 1); switch (filter) { case 'mark': if ((option === 'read' || option === 'removed' || option === 'unread') && !setMark.length) { mark = option; setMark.push(1); } item = ''; break; // case 'link': // (option == 'original') ? // link = option : // link = 'shared'; // item = ''; // break; case 'feed_name': if (Number.parseInt(option) === 1 && !setFeedName.length) { addFeedName = 1; setFeedName.push(1); } item = ''; break; case 'direction': if (option !== 'asc') { item = 'direction=desc'; } break; // If user mistakenly set `category=Int` case 'category': Number.isNaN(Number.parseInt(option)) ? (item = '') : (item = `category_id=${option}`); break; case 'order': if (option !== 'id' && option !== 'category_title' && option !== 'published_at' && option !== 'status' && option !== 'category_id') { item = ''; } break; // Program should behave differently for `limit` parameter in // each mode. So currently we only save the (last & existed) // parameter, since user may mistakenly input this parameter // several times. case 'limit': if (!Number.isNaN(option) && !setLimit.length) { limit = option; setLimit.push(1); } item = ''; break; default: break; } return item; } const entriesID = []; const feedsName = []; const articles = []; // MiniFlux will only preserve the *first* valid filter option // for each parameter, in order to matching the default behavior // here we use arrays to track the setting for `limit`, // `mark` and `feed_name`. const setLimit = []; const setMark = []; const setFeedName = []; const feeds = ctx.req.param('feeds'); let parameters = ctx.req.param('parameters'); // Set default direction if (parameters.search('direction=') === -1) { parameters += '&direction=desc'; } parameters = parameters .split('&') .map((item) => filterHandler(item)) .filter(Boolean) .join('&'); let queryLimit = ctx.req.query('limit'); let result: Data; if (feeds.search(/feeds?=/g) !== -1 || !Number.isNaN(Number.parseInt(feeds.split('&').join('')))) { const feedsID = feeds.replaceAll(/feeds?=/g, ''); const feedsList = [feedsID.split('&')].flat(); if (limit && queryLimit) { if (limit < queryLimit) { queryLimit = limit * feedsList.length; } else { const eachLimit = Number.parseInt(queryLimit / feedsList.length); if (eachLimit) { limit = eachLimit; } else { limit = 1; queryLimit = feedsList.length; } } parameters += `&limit=${limit}`; } else if (limit) { parameters += `&limit=${limit}`; } else if (queryLimit) { const eachLimit = Number.parseInt(queryLimit / feedsList.length); if (eachLimit) { limit = eachLimit; } else { limit = 1; queryLimit = feedsList.length; } parameters += `&limit=${limit}`; } await Promise.all( feedsList.map(async (feed) => { const url = `${instance}/v1/feeds/${feed}/entries?${parameters}`; const response = await got({ method: 'get', url, headers: { 'X-Auth-Token': token, }, }); const entries = response.data.entries; // Whether or not get the title of this feed let getFeedTitle = 0; // entries.map(item => { for (const entry of entries) { entriesID.push(entry.id); if (!getFeedTitle) { feedsName.push(entry.feed.title); getFeedTitle = 1; } // Whether or not user would like to add the feed's name // to the title of article. let entryTitle = entry.title; if (addFeedName) { entryTitle += ` | ${entry.feed.title}`; } // let entryURL = `${instance}/share/${entry.share_code}`; // if (link == 'original') { // entryURL = entry.url; // } articles.push({ title: entryTitle, author: entry.author, pubDate: entry.published_at, description: entry.content, link: entry.url, }); } }) ); const feedsNumber = feedsName.length; let agTitle, agInfo; if (feedsNumber > 2) { agTitle = `MiniFlux | Aggregator For ${feedsNumber} Feeds`; agInfo = 'An aggregator powered by MiniFlux and RSSHub. ' + 'This aggregator truthfully preserves the contents in ' + `${feedsNumber} feeds, including: ` + `<li>${feedsName.join('<li></li>')}</li>`; } else if (feedsNumber) { agTitle = `MiniFlux | ${feedsName.join(', ')}`; agInfo = 'A RSS feed powered by MiniFlux and RSSHub ' + 'effortlessly republishes the contents in ' + `"${feedsName.join('" & "')}".`; } else { agTitle = `MiniFlux | Feeds Aggregator`; agInfo = 'An aggregator powered by MiniFlux and RSSHub ' + 'with empty content. If this is not your intention, ' + `please double-check your setting for parameters.`; } result = { title: agTitle, link: instance, description: agInfo, item: articles, allowEmpty: true, }; } else { if (limit && queryLimit) { if (limit < queryLimit) { queryLimit = limit; } // Here we could add a '&' since parameter(s) list must not empty. parameters += `&limit=${queryLimit}`; } else if (queryLimit) { parameters += `&limit=${queryLimit}`; } else if (limit) { parameters += `&limit=${limit}`; } const response = await got.get(`${instance}/v1/entries?${parameters}`, { headers: { 'X-Auth-Token': token }, }); const entries = response.data.entries; const articles = []; for (const entry of entries) { entriesID.push(entry.id); let entryTitle = entry.title; if (addFeedName) { entryTitle += ` | ${entry.feed.title}`; } // let entryURL = `${instance}/share/${entry.share_code}`; // if (link == 'original') { // entryURL = entry.url; // } articles.push({ title: entryTitle, author: entry.author, pubDate: entry.published_at, description: entry.content, link: entry.url, }); } result = { title: `MiniFlux | All`, link: instance, description: `All feeds on ${instance} powered by MiniFlux`, item: articles, allowEmpty: true, }; } if (mark !== 'unchanged') { got({ method: 'put', url: `${instance}/v1/entries`, headers: { 'Content-Type': 'application/json', 'X-Auth-Token': token, }, json: { entry_ids: entriesID, status: mark, }, }); } return result; }