Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comments count endpoint #198

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ SUPABASE_INSTALLATION_ACCESS_TOKENS_TABLE=installation_access_tokens
# The usage is different from the values in giscus.json.
ORIGINS=["https://giscus.app", "https://giscus.vercel.app"]
ORIGINS_REGEX=["http://localhost:[0-9]+"]

ENABLE_ADDITIONAL_PAGES=true
6 changes: 6 additions & 0 deletions lib/types/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ export interface GRepositoryDiscussion {
};
}

export interface GRepositoryDiscussionCount {
comments: {
totalCount: number;
}
}

export interface GDiscussionCategory {
id: string;
name: string;
Expand Down
21 changes: 13 additions & 8 deletions pages/api/discussions/categories.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { addCorsHeaders } from '../../../lib/cors';
import { ICategories, IError } from '../../../lib/types/adapter';
import { handleGithubDiscussionResponse } from '../../../services/github/errors';
import { getAppAccessToken } from '../../../services/github/getAppAccessToken';
import { getDiscussionCategories } from '../../../services/github/getDiscussionCategories';
import {
getDiscussionCategories,
GetDiscussionCategoriesResponse,
} from '../../../services/github/getDiscussionCategories';

export default async function DiscussionCategoriesApi(
req: NextApiRequest,
Expand All @@ -24,17 +28,18 @@ export default async function DiscussionCategoriesApi(
}

const response = await getDiscussionCategories(params, token);
const handledResponse = handleGithubDiscussionResponse<GetDiscussionCategoriesResponse['data']>(
response,
params.repo,
);

if ('message' in response) {
res.status(500).json({ error: response.message });
return;
if ('error' in handledResponse) {
return res.status(handledResponse.status).json({ error: handledResponse.error });
}

const {
data: {
search: { nodes: repositories },
},
} = response;
search: { nodes: repositories },
} = handledResponse;

const repository = repositories[0];
if (!repository) {
Expand Down
62 changes: 62 additions & 0 deletions pages/api/discussions/comments-count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { IError } from '../../../lib/types/adapter';
import { DiscussionQuery } from '../../../lib/types/common';
import { handleGithubDiscussionResponse } from '../../../services/github/errors';
import { getAppAccessToken } from '../../../services/github/getAppAccessToken';
import {
getDiscussionCommentsCount,
GetDiscussionCommentsCountResponse,
} from '../../../services/github/getDiscussionCommentsCount';

export default async function getCommentsCount(
req: NextApiRequest,
res: NextApiResponse<number | IError>,
) {
if (!process.env.ENABLE_ADDITIONAL_PAGES) {
return res.status(404).end();
}

const params: DiscussionQuery = {
repo: req.query.repo as string,
term: req.query.term as string,
number: +req.query.number,
category: req.query.category as string,
};

const userToken = req.headers.authorization?.split('Bearer ')[1];
let token = userToken;
if (!token) {
try {
token = await getAppAccessToken(params.repo);
} catch (error) {
res.status(403).json({ error: error.message });
return;
}
}

const response = await getDiscussionCommentsCount(params, token);

const handledResponse = handleGithubDiscussionResponse<
GetDiscussionCommentsCountResponse['data']
>(response, params.repo, userToken);

if ('error' in handledResponse) {
return res.status(handledResponse.status).json({ error: handledResponse.error });
}

const discussion =
'search' in handledResponse
? handledResponse.search.nodes[0] ?? null
: handledResponse.repository.discussion;

if (!discussion) {
res.status(404).json({ error: 'Discussion not found' });
return;
}

res.setHeader(
'Cache-Control',
'public, s-maxage=3600, stale-while-revalidate=3600, stale-if-error=86400',
);
res.status(200).json(discussion.comments.totalCount);
}
51 changes: 14 additions & 37 deletions pages/api/discussions/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { getDiscussion } from '../../../services/github/getDiscussion';
import { getDiscussion, GetDiscussionResponse } from '../../../services/github/getDiscussion';
import { adaptDiscussion } from '../../../lib/adapter';
import { IError, IGiscussion } from '../../../lib/types/adapter';
import { createDiscussion } from '../../../services/github/createDiscussion';
import { GRepositoryDiscussion } from '../../../lib/types/github';
import { getAppAccessToken } from '../../../services/github/getAppAccessToken';
import { addCorsHeaders } from '../../../lib/cors';
import { handleGithubDiscussionResponse } from '../../../services/github/errors';

async function get(req: NextApiRequest, res: NextApiResponse<IGiscussion | IError>) {
const params = {
Expand Down Expand Up @@ -34,49 +35,25 @@ async function get(req: NextApiRequest, res: NextApiResponse<IGiscussion | IErro
}

const response = await getDiscussion(params, token);

if ('message' in response) {
if (response.message.includes('Bad credentials')) {
res.status(403).json({ error: response.message });
return;
}
res.status(500).json({ error: response.message });
return;
}

if ('errors' in response) {
const error = response.errors[0];
if (error?.message?.includes('API rate limit exceeded')) {
let message = `API rate limit exceeded for ${params.repo}`;
if (!userToken) {
message += '. Sign in to increase the rate limit';
}
res.status(429).json({ error: message });
return;
}

console.error(response);
const message = response.errors.map?.(({ message }) => message).join('. ') || 'Unknown error';
res.status(500).json({ error: message });
return;
}

const { data } = response;
if (!data) {
console.error(response);
res.status(500).json({ error: 'Unable to fetch discussion' });
return;
const handledResponse = handleGithubDiscussionResponse<GetDiscussionResponse['data']>(
response,
params.repo,
userToken,
);

if ('error' in handledResponse) {
return res.status(handledResponse.status).json({ error: handledResponse.error });
}

const { viewer } = data;
const { viewer } = handledResponse;

let discussion: GRepositoryDiscussion;
if ('search' in data) {
const { search } = data;
if ('search' in handledResponse) {
const { search } = handledResponse;
const { discussionCount, nodes } = search;
discussion = discussionCount > 0 ? nodes[0] : null;
} else {
discussion = data.repository.discussion;
discussion = handledResponse.repository.discussion;
}

if (!discussion) {
Expand Down
36 changes: 36 additions & 0 deletions services/github/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { GError, GMultipleErrors } from '../../lib/types/github';

export function handleGithubDiscussionResponse<Data>(
response: { data: Data } | GError | GMultipleErrors,
repo: string,
userToken?: string,
) {
if ('message' in response) {
if (response.message.includes('Bad credentials')) {
return { status: 403, error: response.message };
}
return { status: 500, error: response.message };
}

if ('errors' in response) {
const error = response.errors[0];
if (error?.message?.includes('API rate limit exceeded')) {
let message = `API rate limit exceeded for ${repo}`;
if (!userToken) {
message += '. Sign in to increase the rate limit';
}
return { status: 429, error: message };
}

console.error(response);
const message = response.errors.map?.(({ message }) => message).join('. ') || 'Unknown error';
return { status: 500, error: message };
}

const { data } = response;
if (!data) {
console.error(response);
return { status: 500, error: 'Unable to fetch discussion' };
}
return data;
}
39 changes: 12 additions & 27 deletions services/github/getDiscussion.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { DiscussionQuery, PaginationParams } from '../../lib/types/common';
import { GUser, GRepositoryDiscussion, GError, GMultipleErrors } from '../../lib/types/github';
import { parseRepoWithOwner } from '../../lib/utils';
import { GITHUB_GRAPHQL_API_URL } from '../config';
import { GUser, GRepositoryDiscussion } from '../../lib/types/github';
import { githubDiscussionGraphqlRequest } from './graphql';

const DISCUSSION_QUERY = `
id
Expand Down Expand Up @@ -138,32 +137,18 @@ interface SpecificResponse {

export type GetDiscussionResponse = SearchResponse | SpecificResponse;

export async function getDiscussion(
params: GetDiscussionParams,
token: string,
): Promise<GetDiscussionResponse | GError | GMultipleErrors> {
export async function getDiscussion(params: GetDiscussionParams, token: string) {
const { repo: repoWithOwner, term, number, category, ...pagination } = params;

// Force repo to lowercase to prevent GitHub's bug when using category in query.
// https://github.com/giscus/giscus/issues/118
const repo = repoWithOwner.toLowerCase();
const categoryQuery = category ? `category:${JSON.stringify(category)}` : '';
const query = `repo:${repo} ${categoryQuery} in:title ${term}`;
const gql = GET_DISCUSSION_QUERY(number ? 'number' : 'term');

return fetch(GITHUB_GRAPHQL_API_URL, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },

body: JSON.stringify({
query: gql,
variables: {
repo,
query,
number,
...parseRepoWithOwner(repo),
...pagination,
},
}),
}).then((r) => r.json());
return githubDiscussionGraphqlRequest<GetDiscussionResponse, PaginationParams>({
gql,
repoWithOwner,
category,
term,
token,
number,
variables: pagination,
});
}
64 changes: 64 additions & 0 deletions services/github/getDiscussionCommentsCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { DiscussionQuery } from '../../lib/types/common';
import { GRepositoryDiscussionCount } from '../../lib/types/github';
import { githubDiscussionGraphqlRequest } from './graphql';

const DISCUSSION_QUERY = `
comments {
totalCount
}`;

const SEARCH_QUERY = `
search(type: DISCUSSION last: 1 query: $query) {
nodes {
... on Discussion {
${DISCUSSION_QUERY}
}
}
}`;

const SPECIFIC_QUERY = `
repository(owner: $owner, name: $name) {
discussion(number: $number) {
${DISCUSSION_QUERY}
}
}
`;

const GET_DISCUSSION_COMMENTS_COUNT_QUERY = (type: 'term' | 'number') => `
query(${type === 'term' ? '$query: String!' : '$owner: String! $name: String! $number: Int!'}) {
${type === 'term' ? SEARCH_QUERY : SPECIFIC_QUERY}
}`;

interface SearchResponse {
data: {
search: {
nodes: Array<GRepositoryDiscussionCount>;
};
};
}

interface SpecificResponse {
data: {
repository: {
discussion: GRepositoryDiscussionCount;
};
};
}

export type GetDiscussionCommentsCountResponse = SearchResponse | SpecificResponse;

export async function getDiscussionCommentsCount(params: DiscussionQuery, token: string) {
const { repo: repoWithOwner, term, number, category } = params;

const gql = GET_DISCUSSION_COMMENTS_COUNT_QUERY(number ? 'number' : 'term');

return githubDiscussionGraphqlRequest<GetDiscussionCommentsCountResponse>({
gql,
repoWithOwner,
category,
term,
token,
number,
variables: {},
});
}
46 changes: 46 additions & 0 deletions services/github/graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { GError, GMultipleErrors } from '../../lib/types/github';
import { parseRepoWithOwner } from '../../lib/utils';
import { GITHUB_GRAPHQL_API_URL } from '../config';

export async function githubDiscussionGraphqlRequest<
Response,
AdditionalVariables = Record<string, never>,
>({
gql,
repoWithOwner,
category,
term,
token,
number,
variables,
}: {
gql: string;
repoWithOwner: string;
category: string;
term: string;
token: string;
number: number;
variables: AdditionalVariables;
}): Promise<Response | GError | GMultipleErrors> {
// Force repo to lowercase to prevent GitHub's bug when using category in query.
// https://github.com/giscus/giscus/issues/118
const repo = repoWithOwner.toLowerCase();
const categoryQuery = category ? `category:${JSON.stringify(category)}` : '';
const query = `repo:${repo} ${categoryQuery} in:title ${term}`;

return fetch(GITHUB_GRAPHQL_API_URL, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },

body: JSON.stringify({
query: gql,
variables: {
repo,
query,
number,
...parseRepoWithOwner(repo),
...variables,
},
}),
}).then((r) => r.json());
}