@waline/vercel 1.38.0 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waline/vercel",
3
- "version": "1.38.0",
3
+ "version": "1.39.0",
4
4
  "description": "vercel server for waline comment system",
5
5
  "keywords": [
6
6
  "blog",
@@ -43,6 +43,7 @@
43
43
  "nunjucks": "^3.2.4",
44
44
  "phpass": "^0.1.1",
45
45
  "prismjs": "^1.30.0",
46
+ "rss": "^1.2.2",
46
47
  "speakeasy": "^2.0.0",
47
48
  "think-helper": "^1.1.4",
48
49
  "think-logger3": "^1.4.0",
@@ -0,0 +1,158 @@
1
+ const RSS = require('rss');
2
+ const BaseRest = require('../rest.js');
3
+ const { getMarkdownParser } = require('../../service/markdown/index.js');
4
+
5
+ const markdownParser = getMarkdownParser();
6
+
7
+ const isHttpUrl = (value) => /^(https?:)?\/\//i.test(value);
8
+
9
+ const buildAbsoluteUrl = (baseUrl, path) => {
10
+ if (!path) return baseUrl || '';
11
+ if (isHttpUrl(path)) return path;
12
+ if (!baseUrl) return path;
13
+ const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
14
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
15
+
16
+ return `${normalizedBase}${normalizedPath}`;
17
+ };
18
+
19
+ const buildRssXml = ({ title, link, description, items }) => {
20
+ const channelLink = link || '';
21
+ const now = new Date().toUTCString();
22
+
23
+ const feed = new RSS({
24
+ title,
25
+ description,
26
+ site_url: channelLink,
27
+ pubDate: now,
28
+ });
29
+
30
+ items.forEach((item) => {
31
+ feed.item({
32
+ title: item.title || 'Comment',
33
+ description: item.description,
34
+ url: item.link || '',
35
+ guid: item.guid || item.link || '',
36
+ date: item.pubDate || now,
37
+ });
38
+ });
39
+
40
+ return feed.xml({ indent: true });
41
+ };
42
+
43
+ const setRssResponse = (ctx, xml) => {
44
+ ctx.set('Content-Type', 'application/rss+xml; charset=utf-8');
45
+ ctx.body = xml;
46
+ };
47
+
48
+ module.exports = class extends BaseRest {
49
+ constructor(ctx) {
50
+ super(ctx);
51
+ this.modelInstance = this.getModel('Comment');
52
+ }
53
+
54
+ async getAction() {
55
+ const { path, email, user_id: userId, count } = this.get();
56
+ const limit = Number.isFinite(Number(count)) ? Number(count) : 20;
57
+ const safeLimit = Math.min(Math.max(limit, 1), 50);
58
+
59
+ const siteUrl = process.env.SITE_URL || this.ctx.serverURL;
60
+ const siteName = process.env.SITE_NAME || 'Waline';
61
+
62
+ const where = {
63
+ status: ['NOT IN', ['waiting', 'spam']],
64
+ };
65
+
66
+ if (path) {
67
+ where.url = path;
68
+ } else if (email || userId) {
69
+ const parentWhere = {
70
+ status: ['NOT IN', ['waiting', 'spam']],
71
+ };
72
+
73
+ if (email && userId) {
74
+ parentWhere._complex = {
75
+ _logic: 'or',
76
+ mail: email,
77
+ user_id: userId,
78
+ };
79
+ } else if (email) {
80
+ parentWhere.mail = email;
81
+ } else if (userId) {
82
+ parentWhere.user_id = userId;
83
+ }
84
+
85
+ const parents = await this.modelInstance.select(parentWhere, {});
86
+ const parentIds = parents.map(({ objectId }) => objectId).filter(Boolean);
87
+
88
+ if (parentIds.length === 0) {
89
+ setRssResponse(
90
+ this.ctx,
91
+ buildRssXml({
92
+ title: `${siteName} Reply Comments`,
93
+ link: siteUrl,
94
+ description: 'Recent reply comments.',
95
+ items: [],
96
+ }),
97
+ );
98
+ return;
99
+ }
100
+
101
+ where.pid = ['IN', parentIds];
102
+ }
103
+
104
+ const comments = await this.modelInstance.select(where, {
105
+ desc: 'insertedAt',
106
+ limit: safeLimit,
107
+ field: ['comment', 'insertedAt', 'link', 'nick', 'url', 'user_id'],
108
+ });
109
+
110
+ const userModel = this.getModel('Users');
111
+ const userIds = [...new Set(comments.map(({ user_id }) => user_id).filter(Boolean))];
112
+ let users = [];
113
+
114
+ if (userIds.length > 0) {
115
+ users = await userModel.select(
116
+ { objectId: ['IN', userIds] },
117
+ { field: ['display_name', 'url'] },
118
+ );
119
+ }
120
+
121
+ const items = comments.map((comment) => {
122
+ const user = users.find(({ objectId }) => objectId === comment.user_id);
123
+ const nick = user?.display_name || comment.nick || 'Anonymous';
124
+ const commentUrl = buildAbsoluteUrl(siteUrl, comment.url);
125
+ const itemLink = commentUrl ? `${commentUrl}#${comment.objectId}` : '';
126
+ const description = markdownParser(comment.comment || '');
127
+
128
+ return {
129
+ title: `${nick} commented${comment.url ? ` on ${comment.url}` : ''}`,
130
+ link: itemLink || commentUrl,
131
+ guid: comment.objectId,
132
+ pubDate: new Date(comment.insertedAt).toUTCString(),
133
+ description,
134
+ };
135
+ });
136
+
137
+ const title = path
138
+ ? `${siteName} Comments for ${path}`
139
+ : email || userId
140
+ ? `${siteName} Reply Comments`
141
+ : `${siteName} Recent Comments`;
142
+ const description = path
143
+ ? `Recent comments for ${path}.`
144
+ : email || userId
145
+ ? 'Recent reply comments.'
146
+ : 'Recent comments.';
147
+
148
+ setRssResponse(
149
+ this.ctx,
150
+ buildRssXml({
151
+ title,
152
+ link: siteUrl,
153
+ description,
154
+ items,
155
+ }),
156
+ );
157
+ }
158
+ };
@@ -3,6 +3,7 @@ const it = require('./it.json');
3
3
  const zhCN = require('./zh-CN.json');
4
4
  const zhTW = require('./zh-TW.json');
5
5
  const jp = require('./jp.json');
6
+ const koKR = require('./ko-KR.json');
6
7
  const de = require('./de.json');
7
8
  const esMX = require('./es.json');
8
9
  const fr = require('./fr.json');
@@ -19,6 +20,8 @@ module.exports = {
19
20
  'it-it': it,
20
21
  jp,
21
22
  'jp-jp': jp,
23
+ ko: koKR,
24
+ 'ko-kr': koKR,
22
25
  de,
23
26
  esMX,
24
27
  fr,
@@ -0,0 +1,19 @@
1
+ {
2
+ "import data format not support!": "가져오기 데이터 형식이 지원되지 않습니다!",
3
+ "USER_EXIST": "이미 존재하는 사용자입니다",
4
+ "USER_NOT_EXIST": "사용자가 존재하지 않습니다",
5
+ "USER_REGISTERED": "사용자가 등록되었습니다",
6
+ "TOKEN_EXPIRED": "토큰이 만료되었습니다",
7
+ "TWO_FACTOR_AUTH_ERROR_DETAIL": "2단계 인증 오류 상세",
8
+ "[{{name | safe}}] Registration Confirm Mail": "[{{name | safe}}] 가입 확인 메일",
9
+ "Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "<a href=\"{{url}}\">{{url}}</a>을 클릭하여 가입을 확인해 주세요. 링크는 1시간 동안 유효합니다. 가입하지 않으셨다면 이 이메일을 무시해 주세요.",
10
+ "[{{name | safe}}] Reset Password": "[{{name | safe}}] 비밀번호 재설정",
11
+ "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "<a href=\"{{url}}\">{{url}}</a>을 클릭하여 로그인하고 가능한 빨리 비밀번호를 변경해 주세요!",
12
+ "Duplicate Content": "중복된 내용",
13
+ "Comment too fast": "댓글 작성이 너무 빠릅니다",
14
+ "Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "가입 확인 메일 발송에 실패했습니다. {%- if isAdmin -%}메일 설정을 확인해 주세요{%- else -%}이메일 주소를 확인하고 관리자에게 문의해 주세요{%- endif -%}.",
15
+ "MAIL_SUBJECT": "{{site.name | safe}}에서 댓글에 답글이 달렸습니다",
16
+ "MAIL_TEMPLATE": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a>에서 댓글에 답글이 달렸습니다 </h2>{{parent.nick}}님이 작성한 댓글: <div style='padding:0 12px 0 12px;margin-top:18px'> <div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{parent.comment | safe}}</div><p><strong>{{self.nick}}</strong>님이 답글을 남겼습니다:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p><a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>전체 답글 보기</a> 또는 <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a> 방문.</p><br/> </div></div>",
17
+ "MAIL_SUBJECT_ADMIN": "{{site.name | safe}}에 새 댓글이 달렸습니다",
18
+ "MAIL_TEMPLATE_ADMIN": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a>에 새 댓글이 달렸습니다 </h2> <p><strong>{{self.nick}}</strong>님이 작성했습니다:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p><a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>페이지 보기</a></p><br/></div>"
19
+ }
@@ -0,0 +1,54 @@
1
+ const Base = require('../base.js');
2
+
3
+ module.exports = class extends Base {
4
+ /**
5
+ * @api {GET} /api/comment/rss Get site recent comments RSS
6
+ * @apiGroup Comment
7
+ * @apiVersion 0.0.1
8
+ *
9
+ * @apiParam {String} [count] return comments number, default value is 20
10
+ *
11
+ * @apiHeader {String} Content-Type application/rss+xml
12
+ * @apiSuccess (200) {String} body RSS 2.0 xml content
13
+ */
14
+ /**
15
+ * @api {GET} /api/comment/rss?path= Get comments RSS for a path
16
+ * @apiGroup Comment
17
+ * @apiVersion 0.0.1
18
+ *
19
+ * @apiParam {String} path comment url path
20
+ * @apiParam {String} [count] return comments number, default value is 20
21
+ *
22
+ * @apiHeader {String} Content-Type application/rss+xml
23
+ * @apiSuccess (200) {String} body RSS 2.0 xml content
24
+ */
25
+ /**
26
+ * @api {GET} /api/comment/rss?email=&user_id= Get reply comments RSS for user
27
+ * @apiGroup Comment
28
+ * @apiVersion 0.0.1
29
+ *
30
+ * @apiParam {String} [email] comment user email
31
+ * @apiParam {String} [user_id] comment user id
32
+ * @apiParam {String} [count] return comments number, default value is 20
33
+ *
34
+ * @apiHeader {String} Content-Type application/rss+xml
35
+ * @apiSuccess (200) {String} body RSS 2.0 xml content
36
+ */
37
+ getAction() {
38
+ this.rules = {
39
+ path: {
40
+ string: true,
41
+ },
42
+ email: {
43
+ email: true,
44
+ },
45
+ user_id: {
46
+ string: true,
47
+ },
48
+ count: {
49
+ int: { max: 50 },
50
+ default: 20,
51
+ },
52
+ };
53
+ }
54
+ };