197 lines
5.8 KiB
TypeScript
197 lines
5.8 KiB
TypeScript
import MarkdownIt from 'markdown-it';
|
|
|
|
import { config } from '@/config';
|
|
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
|
import type { Route } from '@/types';
|
|
import got from '@/utils/got';
|
|
import { parseDate } from '@/utils/parse-date';
|
|
|
|
const md = MarkdownIt({
|
|
html: true,
|
|
linkify: true,
|
|
});
|
|
|
|
export const route: Route = {
|
|
path: '/discussion/:user/:repo/:state?/:category?',
|
|
categories: ['programming'],
|
|
example: '/github/discussion/DIYgod/RSSHub',
|
|
parameters: {
|
|
user: 'User name',
|
|
repo: 'Repo name',
|
|
state: {
|
|
description: 'The state of discussions',
|
|
default: 'open',
|
|
options: [
|
|
{
|
|
label: 'Open',
|
|
value: 'open',
|
|
},
|
|
{
|
|
label: 'Closed',
|
|
value: 'closed',
|
|
},
|
|
{
|
|
label: 'Answered',
|
|
value: 'answered',
|
|
},
|
|
{
|
|
label: 'Unanswered',
|
|
value: 'unanswered',
|
|
},
|
|
{
|
|
label: 'Locked',
|
|
value: 'locked',
|
|
},
|
|
{
|
|
label: 'Unlocked',
|
|
value: 'unlocked',
|
|
},
|
|
{
|
|
label: 'All',
|
|
value: 'all',
|
|
},
|
|
],
|
|
},
|
|
category: 'Category Name (case-sensitive). Default: `null`.',
|
|
},
|
|
features: {
|
|
requireConfig: [
|
|
{
|
|
name: 'GITHUB_ACCESS_TOKEN',
|
|
description: 'GitHub Access Token',
|
|
},
|
|
],
|
|
},
|
|
radar: [
|
|
{
|
|
source: ['github.com/:user/:repo/discussions', 'github.com/:user/:repo/discussions/:id', 'github.com/:user/:repo'],
|
|
target: '/discussion/:user/:repo',
|
|
},
|
|
],
|
|
name: 'Repo Discussions',
|
|
maintainers: ['waynzh'],
|
|
handler,
|
|
};
|
|
|
|
async function handler(ctx) {
|
|
if (!config.github || !config.github.access_token) {
|
|
throw new ConfigNotFoundError('GitHub Discussions RSS is disabled due to the lack of <a href="https://docs.rsshub.app/deploy/config#route-specific-configurations">relevant config</a>');
|
|
}
|
|
const { user, repo, limit, state = 'open', category = null } = ctx.req.param();
|
|
const { answered, closed, locked } = mapStateToBooleans(state);
|
|
const perPage = Math.min(Number.parseInt(limit) || 100, 100);
|
|
|
|
const host = `https://github.com/${user}/${repo}/discussions`;
|
|
const url = 'https://api.github.com/graphql';
|
|
|
|
let filters = `first: ${perPage}`;
|
|
if (answered !== null) {
|
|
filters += `, answered: ${answered}`;
|
|
}
|
|
if (category !== null) {
|
|
const response = await got({
|
|
method: 'post',
|
|
url,
|
|
headers: {
|
|
Authorization: `bearer ${config.github.access_token}`,
|
|
},
|
|
json: {
|
|
query: `
|
|
{
|
|
repository(owner: "${user}", name: "${repo}") {
|
|
discussionCategories(first: 25) {
|
|
nodes {
|
|
id,
|
|
name,
|
|
}
|
|
},
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
});
|
|
const categoryItem = response.data.data.repository.discussionCategories.nodes.find((item) => item.name === category);
|
|
filters += categoryItem?.id ? `, categoryId: "${categoryItem.id}"` : '';
|
|
}
|
|
|
|
const response = await got({
|
|
method: 'post',
|
|
url,
|
|
headers: {
|
|
Authorization: `bearer ${config.github.access_token}`,
|
|
},
|
|
json: {
|
|
query: `
|
|
{
|
|
repository(owner: "${user}", name: "${repo}") {
|
|
discussions(${filters}) {
|
|
nodes {
|
|
title,
|
|
author {
|
|
login
|
|
},
|
|
createdAt,
|
|
closed,
|
|
isAnswered,
|
|
locked,
|
|
body,
|
|
url
|
|
}
|
|
},
|
|
}
|
|
}
|
|
`,
|
|
},
|
|
});
|
|
|
|
const data = response.data.data.repository.discussions.nodes.filter((item) => (closed === null ? item : item.closed === closed)).filter((item) => (locked === null ? item : item.locked === locked));
|
|
|
|
return {
|
|
allowEmpty: true,
|
|
title: `${user}/${repo} Discussions`,
|
|
link: host,
|
|
item: data.map((item) => ({
|
|
title: item.title,
|
|
author: item.author?.login ?? 'ghost',
|
|
description: item.body ? md.render(item.body) : 'No description',
|
|
pubDate: parseDate(item.createdAt),
|
|
link: item.url,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function mapStateToBooleans(state: string) {
|
|
// 初始化布尔值
|
|
let answered: boolean | null = null;
|
|
let closed: boolean | null = null;
|
|
let locked: boolean | null = null;
|
|
|
|
// 设置布尔值,根据 state 的值
|
|
switch (state) {
|
|
case 'answered':
|
|
answered = true;
|
|
break;
|
|
case 'unanswered':
|
|
answered = false;
|
|
break;
|
|
case 'closed':
|
|
closed = true;
|
|
break;
|
|
case 'open':
|
|
closed = false;
|
|
break;
|
|
case 'locked':
|
|
locked = true;
|
|
break;
|
|
case 'unlocked':
|
|
locked = false;
|
|
break;
|
|
case 'all':
|
|
default:
|
|
// 保持 answered, closed, locked 为 null
|
|
break;
|
|
}
|
|
|
|
return { answered, closed, locked };
|
|
}
|