nodebb-plugin-discord-onekite 1.0.14 → 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 (3) hide show
  1. package/README.md +6 -8
  2. package/library.js +1 -246
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,10 +1,8 @@
1
- # nodebb-plugin-discord-onekite v1.1.3
1
+ # nodebb-plugin-discord-onekite v1.1.4
2
2
 
3
- ACP:
4
- - Uses the standard NodeBB save "disk" button (IMPORT admin/partials/save_button.tpl)
5
- - Saves via AJAX to `/api/admin/plugins/discord-onekite/save` and shows NodeBB toast (bottom-right)
3
+ Discord:
4
+ - Always sends the forum topic link (example: https://www.onekite.com/topic/16024/test-discord)
5
+ - First line is the raw URL, then a Markdown clickable link `[title](url)`
6
+ - Still sends an embed (title clickable via embed.url) + fallback to plain content if embeds are rejected
6
7
 
7
- Discord message:
8
- - Notification content starts with the direct post URL
9
- - Includes an excerpt of the post content (truncated)
10
- - Keeps embed minimal + fallback to plain content if Discord rejects embeds
8
+ ACP remains with the "disk" save button and NodeBB toast.
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, 350);
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
- const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
138
- const postUrl = isReply && pid ? `${topicUrl}/${pid}` : `${topicUrl}/${topicData.mainPid || ''}`.replace(/\/$/, '');
139
-
140
- const u = await user.getUserFields(topicData.uid, ['username']);
141
- const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
142
- const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
143
-
144
- const excerpt = await getPostExcerpt(isReply ? pid : topicData.mainPid);
145
- const description = truncate(excerpt || (isReply ? `Réponse de ${authorName}` : `Sujet créé par ${authorName}`), 350);
146
-
147
- const embed = {
148
- title,
149
- description,
150
- };
151
-
152
- if (isValidHttpUrl(postUrl)) {
153
- embed.url = postUrl;
154
- }
155
-
156
- // "Lien vers le message directement en début de notification"
157
- const contentLines = [
158
- postUrl,
159
- isReply ? `🗨️ Nouvelle réponse : ${title}` : `🆕 Nouveau sujet : ${title}`,
160
- ];
161
- if (description) contentLines.push(description);
162
-
163
- const content = contentLines.join('\n');
164
-
165
- return { topicData, embed, content };
166
- }
167
-
168
- function extractTidPid(data) {
169
- const tid = data?.tid || data?.topic?.tid || data?.post?.tid;
170
- const pid = data?.pid || data?.post?.pid;
171
- return { tid, pid };
172
- }
173
-
174
- const Plugin = {};
175
-
176
- Plugin.init = async ({ router }) => {
177
- routeHelpers.setupAdminPageRoute(
178
- router,
179
- '/admin/plugins/discord-onekite',
180
- [],
181
- controllers.renderAdminPage
182
- );
183
-
184
- // AJAX save endpoint for ACP (used by admin.js)
185
- router.post('/api/admin/plugins/discord-onekite/save',
186
- middleware.admin.checkPrivileges,
187
- async (req, res) => {
188
- try {
189
- const payload = {
190
- webhookUrl: req.body.webhookUrl || '',
191
- notifyReplies: req.body.notifyReplies ? 'on' : '',
192
- cids: req.body.cids || '',
193
- };
194
- await meta.settings.set(SETTINGS_KEY, payload);
195
- res.json({ ok: true });
196
- } catch (e) {
197
- console.error('[discord-onekite] save failed', e);
198
- res.status(500).json({ ok: false });
199
- }
200
- }
201
- );
202
- };
203
-
204
- Plugin.addAdminNavigation = (header) => {
205
- header.plugins.push({
206
- route: '/plugins/discord-onekite',
207
- icon: 'fa-bell',
208
- name: 'Discord Onekite',
209
- });
210
- return header;
211
- };
212
-
213
- Plugin.onTopicPost = async (data) => {
214
- const settings = await getSettings();
215
- if (!settings.webhookUrl) return;
216
-
217
- const { tid } = extractTidPid(data);
218
- if (!tid) return;
219
-
220
- const built = await buildPayload({ tid, isReply: false });
221
- if (!built) return;
222
-
223
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
224
-
225
- await sendDiscord(settings.webhookUrl, { 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, { 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.14",
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",