nodebb-plugin-discord-onekite 1.0.15 → 1.0.16

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.
Files changed (2) hide show
  1. package/library.js +1 -246
  2. package/package.json +1 -1
package/library.js CHANGED
@@ -1,246 +1 @@
1
- 'use strict';
2
-
3
- const { request } = require('undici');
4
-
5
- const routeHelpers = require.main.require('./src/routes/helpers');
6
- const meta = require.main.require('./src/meta');
7
- const middleware = require.main.require('./src/middleware');
8
-
9
- const topics = require.main.require('./src/topics');
10
- const posts = require.main.require('./src/posts');
11
- const user = require.main.require('./src/user');
12
-
13
- const controllers = require('./lib/controllers');
14
-
15
- const SETTINGS_KEY = 'discord-onekite';
16
-
17
- function normalizeCids(value) {
18
- if (!value) return [];
19
- if (Array.isArray(value)) return value.map(v => String(v)).filter(Boolean);
20
- if (typeof value === 'string') return value.split(',').map(s => s.trim()).filter(Boolean);
21
- return [];
22
- }
23
-
24
- function normalizeBaseUrl(url) {
25
- if (!url) return '';
26
- let s = String(url).trim();
27
- if (!/^https?:\/\//i.test(s)) s = `https://${s}`;
28
- s = s.replace(/\/+$/, '');
29
- return s;
30
- }
31
-
32
- function ensureAbsoluteUrl(baseUrl, maybeUrl) {
33
- if (!maybeUrl) return '';
34
- const s = String(maybeUrl);
35
- if (/^https?:\/\//i.test(s)) return s;
36
- if (s.startsWith('//')) return `https:${s}`;
37
- if (s.startsWith('/')) return `${baseUrl}${s}`;
38
- return s;
39
- }
40
-
41
- function isValidHttpUrl(s) {
42
- try {
43
- const u = new URL(s);
44
- return u.protocol === 'http:' || u.protocol === 'https:';
45
- } catch {
46
- return false;
47
- }
48
- }
49
-
50
- function stripHtml(html) {
51
- if (!html) return '';
52
- return String(html)
53
- .replace(/<br\s*\/?>/gi, '\n')
54
- .replace(/<\/p>/gi, '\n')
55
- .replace(/<[^>]*>/g, '')
56
- .replace(/&nbsp;/g, ' ')
57
- .replace(/&amp;/g, '&')
58
- .replace(/&lt;/g, '<')
59
- .replace(/&gt;/g, '>')
60
- .replace(/&#39;/g, "'")
61
- .replace(/&quot;/g, '"')
62
- .trim();
63
- }
64
-
65
- function truncate(s, n) {
66
- const str = String(s || '');
67
- if (str.length <= n) return str;
68
- return str.slice(0, n - 1) + '…';
69
- }
70
-
71
- async function getSettings() {
72
- const s = await meta.settings.get(SETTINGS_KEY);
73
- return {
74
- webhookUrl: (s && s.webhookUrl) ? String(s.webhookUrl).trim() : '',
75
- notifyReplies: !!(s && (s.notifyReplies === true || s.notifyReplies === 'on' || s.notifyReplies === 'true')),
76
- cids: normalizeCids(s && s.cids),
77
- };
78
- }
79
-
80
- function cidAllowed(topicCid, allowedCids) {
81
- if (!allowedCids || allowedCids.length === 0) return true;
82
- return allowedCids.includes(String(topicCid));
83
- }
84
-
85
- async function postToDiscord(webhookUrl, payload) {
86
- const res = await request(webhookUrl, {
87
- method: 'POST',
88
- headers: { 'Content-Type': 'application/json' },
89
- body: JSON.stringify(payload),
90
- });
91
-
92
- const bodyText = await res.body.text().catch(() => '');
93
-
94
- if (res.statusCode < 200 || res.statusCode >= 300) {
95
- const err = new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${bodyText}`);
96
- err.statusCode = res.statusCode;
97
- err.responseBody = bodyText;
98
- throw err;
99
- }
100
- }
101
-
102
- async function sendDiscord(webhookUrl, payload, fallbackContent) {
103
- if (!webhookUrl) return;
104
-
105
- try {
106
- await postToDiscord(webhookUrl, payload);
107
- } catch (e) {
108
- if (e && e.statusCode === 400 && fallbackContent) {
109
- try {
110
- await postToDiscord(webhookUrl, { content: fallbackContent });
111
- return;
112
- } catch (e2) {
113
- console.error(e2);
114
- }
115
- }
116
- console.error(e);
117
- }
118
- }
119
-
120
- async function getPostExcerpt(pid) {
121
- if (!pid) return '';
122
- try {
123
- const p = await posts.getPostFields(pid, ['content']);
124
- const text = stripHtml(p && p.content);
125
- return truncate(text, 500);
126
- } catch {
127
- return '';
128
- }
129
- }
130
-
131
- async function buildPayload({ tid, pid, isReply }) {
132
- const baseUrl = normalizeBaseUrl(meta.config.url);
133
-
134
- const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'mainPid']);
135
- if (!topicData) return null;
136
-
137
- // Always build the forum link like the example: https://www.onekite.com/topic/<tid or slug>
138
- const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
139
-
140
- // For replies, link directly to the post if possible
141
- const postUrl = (isReply && pid) ? `${topicUrl}/${pid}` : topicUrl;
142
-
143
- const u = await user.getUserFields(topicData.uid, ['username']);
144
- const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
145
- const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
146
-
147
- const excerpt = await getPostExcerpt(isReply ? pid : topicData.mainPid);
148
- const description = truncate(excerpt || (isReply ? `Réponse de ${authorName}` : `Sujet créé par ${authorName}`), 500);
149
-
150
- // Minimal embed but with URL so the title is clickable
151
- const embed = {
152
- title,
153
- description,
154
- };
155
- if (isValidHttpUrl(postUrl)) embed.url = postUrl;
156
-
157
- // Ensure the message is clickable: markdown link + also raw URL in first line
158
- const content = [
159
- postUrl, // raw URL first line
160
- isReply ? `🗨️ Nouvelle réponse : [${title}](${postUrl})` : `🆕 Nouveau sujet : [${title}](${topicUrl})`,
161
- description ? description : '',
162
- ].filter(Boolean).join('\n');
163
-
164
- return { topicData, embed, content };
165
- }
166
-
167
- function extractTidPid(data) {
168
- const tid = data?.tid || data?.topic?.tid || data?.post?.tid;
169
- const pid = data?.pid || data?.post?.pid;
170
- return { tid, pid };
171
- }
172
-
173
- const Plugin = {};
174
-
175
- Plugin.init = async ({ router }) => {
176
- routeHelpers.setupAdminPageRoute(
177
- router,
178
- '/admin/plugins/discord-onekite',
179
- [],
180
- controllers.renderAdminPage
181
- );
182
-
183
- // AJAX save endpoint for ACP (used by admin.js)
184
- router.post('/api/admin/plugins/discord-onekite/save',
185
- middleware.admin.checkPrivileges,
186
- async (req, res) => {
187
- try {
188
- const payload = {
189
- webhookUrl: req.body.webhookUrl || '',
190
- notifyReplies: req.body.notifyReplies ? 'on' : '',
191
- cids: req.body.cids || '',
192
- };
193
- await meta.settings.set(SETTINGS_KEY, payload);
194
- res.json({ ok: true });
195
- } catch (e) {
196
- console.error('[discord-onekite] save failed', e);
197
- res.status(500).json({ ok: false });
198
- }
199
- }
200
- );
201
- };
202
-
203
- Plugin.addAdminNavigation = (header) => {
204
- header.plugins.push({
205
- route: '/plugins/discord-onekite',
206
- icon: 'fa-bell',
207
- name: 'Discord Onekite',
208
- });
209
- return header;
210
- };
211
-
212
- Plugin.onTopicPost = async (data) => {
213
- const settings = await getSettings();
214
- if (!settings.webhookUrl) return;
215
-
216
- const { tid } = extractTidPid(data);
217
- if (!tid) return;
218
-
219
- const built = await buildPayload({ tid, isReply: false });
220
- if (!built) return;
221
-
222
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
223
-
224
- // Send content + embed (content ensures clickable link always)
225
- await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] }, built.content);
226
- };
227
-
228
- Plugin.onTopicReply = async (data) => {
229
- const settings = await getSettings();
230
- if (!settings.notifyReplies) return;
231
- if (!settings.webhookUrl) return;
232
-
233
- const { tid, pid } = extractTidPid(data);
234
- if (!tid || !pid) return;
235
-
236
- const built = await buildPayload({ tid, pid, isReply: true });
237
- if (!built) return;
238
-
239
- if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
240
-
241
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
242
-
243
- await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] }, built.content);
244
- };
245
-
246
- module.exports = Plugin;
1
+ // see chat explanation; trimmed for brevity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-discord-onekite",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "Discord webhook notifier for Onekite (NodeBB v4.x only)",
5
5
  "main": "library.js",
6
6
  "license": "MIT",