master
 1import type { CommentData, CommentProvider } from './types';
 2import { cleanCommentHtml } from './utils';
 3import { asideConfig, commentConfig } from '@/config';
 4import { t } from '@utils/i18n';
 5
 6export const ArtalkProvider: CommentProvider = {
 7  async setup() {
 8    const artalkConfig = commentConfig.artalk!;
 9    const desiredCount = asideConfig.recentComment.count;
10
11    const apiBase = artalkConfig.serverURL;
12    const apiConfigUrl = new URL(`api/v2/conf`, apiBase).toString();
13
14    // fetch comment config first
15    const responseConfig = await fetch(apiConfigUrl, { method: 'GET' });
16    if (!responseConfig.ok) {
17      throw new Error('Failed to fetch comment config');
18    }
19    const configData: {
20      gravatar: {
21        mirror: string;
22        params: string;
23      };
24    } = (await responseConfig.json()).frontend_conf;
25
26    const getAvatarUrl = (email: string) => {
27      return `${configData.gravatar.mirror}${email}?${configData.gravatar.params}`;
28    };
29
30    // type for items returned by Artalk
31    type FetchItem = {
32      id: number;
33      nick: string;
34      content_marked: string;
35      date: string;
36      email_encrypted: string;
37      page_url: string;
38      is_collapsed: boolean;
39      visible: boolean;
40    }[];
41
42    const collected: FetchItem = [];
43    const seenIds = new Set<number>();
44    let attempt = 0;
45    let limit = desiredCount;
46    let prevCollectedCount = 0;
47
48    while (collected.length < desiredCount && attempt < 3) {
49      const apiCommentUrl = new URL(
50        `api/v2/stats/latest_comments?limit=${limit}`,
51        apiBase
52      ).toString();
53      const resp = await fetch(apiCommentUrl, { method: 'GET' });
54      if (!resp.ok) {
55        throw new Error('Failed to fetch recent comments');
56      }
57      const data: FetchItem = (await resp.json()).data || [];
58
59      let addedThisRound = 0;
60      for (const item of data) {
61        if (!item.visible) continue; // skip invisible comments
62        if (seenIds.has(item.id)) continue; // dedupe
63        collected.push(item);
64        seenIds.add(item.id);
65        addedThisRound++;
66        if (collected.length >= desiredCount) break;
67      }
68
69      // if increasing limit didn't yield any new visible comments, stop
70      if (addedThisRound === 0 && prevCollectedCount === collected.length) {
71        break;
72      }
73
74      prevCollectedCount = collected.length;
75      if (collected.length >= desiredCount) break;
76
77      // increase limit to try to fetch more next round
78      limit = limit + (desiredCount - collected.length);
79      attempt++;
80    }
81
82    // map to CommentData and return up to desiredCount
83    return collected.slice(0, desiredCount).map(
84      (item): CommentData => ({
85        avatarUrl: getAvatarUrl(item.email_encrypted),
86        commentContent: item.is_collapsed
87          ? t.info.commentAbbrs.collapsed()
88          : cleanCommentHtml(item.content_marked),
89        commentUrl: `${item.page_url}#atk-comment-${item.id}`,
90        author: item.nick,
91        time: new Date(item.date),
92      })
93    );
94  },
95};