nodebb-plugin-discord-onekite 1.0.16 → 1.1.7

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/README.md CHANGED
@@ -1,8 +1,10 @@
1
- # nodebb-plugin-discord-onekite v1.1.4
1
+ # nodebb-plugin-discord-onekite v1.1.6
2
2
 
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
3
+ This fixes the broken v1.1.5 zip (missing files).
4
+
5
+ ACP:
6
+ - Standard NodeBB save disk button
7
+ - AJAX save endpoint + NodeBB toast
7
8
 
8
- ACP remains with the "disk" save button and NodeBB toast.
9
+ Discord:
10
+ - Single clickable line "Nouveau sujet : [Titre](URL)" then excerpt below
package/library.js CHANGED
@@ -1 +1,219 @@
1
- // see chat explanation; trimmed for brevity
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
+
12
+ const controllers = require('./lib/controllers');
13
+
14
+ const SETTINGS_KEY = 'discord-onekite';
15
+
16
+ function normalizeCids(value) {
17
+ if (!value) return [];
18
+ if (Array.isArray(value)) return value.map(v => String(v)).filter(Boolean);
19
+ if (typeof value === 'string') return value.split(',').map(s => s.trim()).filter(Boolean);
20
+ return [];
21
+ }
22
+
23
+ function normalizeBaseUrl(url) {
24
+ if (!url) return '';
25
+ let s = String(url).trim();
26
+ if (!/^https?:\/\//i.test(s)) s = `https://${s}`;
27
+ s = s.replace(/\/+$/, '');
28
+ return s;
29
+ }
30
+
31
+ function ensureAbsoluteUrl(baseUrl, maybeUrl) {
32
+ if (!maybeUrl) return '';
33
+ const s = String(maybeUrl);
34
+ if (/^https?:\/\//i.test(s)) return s;
35
+ if (s.startsWith('//')) return `https:${s}`;
36
+ if (s.startsWith('/')) return `${baseUrl}${s}`;
37
+ return s;
38
+ }
39
+
40
+ function stripHtml(html) {
41
+ if (!html) return '';
42
+ return String(html)
43
+ .replace(/<br\s*\/?>/gi, '\n')
44
+ .replace(/<\/p>/gi, '\n')
45
+ .replace(/<[^>]*>/g, '')
46
+ .replace(/&nbsp;/g, ' ')
47
+ .replace(/&amp;/g, '&')
48
+ .replace(/&lt;/g, '<')
49
+ .replace(/&gt;/g, '>')
50
+ .replace(/&#39;/g, "'")
51
+ .replace(/&quot;/g, '"')
52
+ .trim();
53
+ }
54
+
55
+ function truncate(s, n) {
56
+ const str = String(s || '');
57
+ if (str.length <= n) return str;
58
+ return str.slice(0, n - 1) + '…';
59
+ }
60
+
61
+ async function getSettings() {
62
+ const s = await meta.settings.get(SETTINGS_KEY);
63
+ return {
64
+ webhookUrl: (s && s.webhookUrl) ? String(s.webhookUrl).trim() : '',
65
+ notifyReplies: !!(s && (s.notifyReplies === true || s.notifyReplies === 'on' || s.notifyReplies === 'true')),
66
+ cids: normalizeCids(s && s.cids),
67
+ };
68
+ }
69
+
70
+ function cidAllowed(topicCid, allowedCids) {
71
+ if (!allowedCids || allowedCids.length === 0) return true;
72
+ return allowedCids.includes(String(topicCid));
73
+ }
74
+
75
+ async function postToDiscord(webhookUrl, payload) {
76
+ const res = await request(webhookUrl, {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify(payload),
80
+ });
81
+
82
+ const bodyText = await res.body.text().catch(() => '');
83
+
84
+ if (res.statusCode < 200 || res.statusCode >= 300) {
85
+ const err = new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${bodyText}`);
86
+ err.statusCode = res.statusCode;
87
+ err.responseBody = bodyText;
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ async function sendDiscord(webhookUrl, payload) {
93
+ if (!webhookUrl) return;
94
+ try {
95
+ await postToDiscord(webhookUrl, payload);
96
+ } catch (e) {
97
+ // If Discord rejects embeds, retry with plain content only.
98
+ if (e && e.statusCode === 400 && payload && payload.content) {
99
+ try {
100
+ await postToDiscord(webhookUrl, { content: payload.content });
101
+ return;
102
+ } catch (e2) {
103
+ console.error(e2);
104
+ }
105
+ }
106
+ console.error(e);
107
+ }
108
+ }
109
+
110
+ async function getPostExcerpt(pid) {
111
+ if (!pid) return '';
112
+ try {
113
+ const p = await posts.getPostFields(pid, ['content']);
114
+ const text = stripHtml(p && p.content);
115
+ return truncate(text, 500);
116
+ } catch {
117
+ return '';
118
+ }
119
+ }
120
+
121
+ async function buildPayload({ tid, pid, isReply }) {
122
+ const baseUrl = normalizeBaseUrl(meta.config.url);
123
+
124
+ const topicData = await topics.getTopicFields(tid, ['tid', 'cid', 'title', 'slug', 'mainPid']);
125
+ if (!topicData) return null;
126
+
127
+ const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
128
+ const targetUrl = (isReply && pid) ? `${topicUrl}/${pid}` : topicUrl;
129
+
130
+ const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
131
+ const excerpt = await getPostExcerpt(isReply ? pid : topicData.mainPid);
132
+
133
+ // Single clickable link line + excerpt below
134
+ const linkLine = isReply
135
+ ? `🗨️ Nouvelle réponse : [${title}](${targetUrl})`
136
+ : `🆕 Nouveau sujet : [${title}](${topicUrl})`;
137
+
138
+ const content = [linkLine, excerpt].filter(Boolean).join('\n');
139
+
140
+ const embed = { title, description: excerpt || '' , url: targetUrl };
141
+
142
+ return { topicData, content, embed };
143
+ }
144
+
145
+ function extractTidPid(data) {
146
+ const tid = data?.tid || data?.topic?.tid || data?.post?.tid;
147
+ const pid = data?.pid || data?.post?.pid;
148
+ return { tid, pid };
149
+ }
150
+
151
+ const Plugin = {};
152
+
153
+ Plugin.init = async ({ router }) => {
154
+ routeHelpers.setupAdminPageRoute(
155
+ router,
156
+ '/admin/plugins/discord-onekite',
157
+ [],
158
+ controllers.renderAdminPage
159
+ );
160
+
161
+ // AJAX save endpoint (used by admin.js via api.post)
162
+ router.post('/admin/plugins/discord-onekite/save',
163
+ middleware.admin.checkPrivileges,
164
+ async (req, res) => {
165
+ try {
166
+ const payload = {
167
+ webhookUrl: req.body.webhookUrl || '',
168
+ notifyReplies: req.body.notifyReplies ? 'on' : '',
169
+ cids: req.body.cids || '',
170
+ };
171
+ await meta.settings.set(SETTINGS_KEY, payload);
172
+ res.json({ ok: true });
173
+ } catch (e) {
174
+ console.error('[discord-onekite] save failed', e);
175
+ res.status(500).json({ ok: false });
176
+ }
177
+ }
178
+ );
179
+ };
180
+
181
+ Plugin.addAdminNavigation = (header) => {
182
+ header.plugins.push({
183
+ route: '/plugins/discord-onekite',
184
+ icon: 'fa-bell',
185
+ name: 'Discord Onekite',
186
+ });
187
+ return header;
188
+ };
189
+
190
+ Plugin.onTopicPost = async (data) => {
191
+ const settings = await getSettings();
192
+ if (!settings.webhookUrl) return;
193
+
194
+ const { tid } = extractTidPid(data);
195
+ if (!tid) return;
196
+
197
+ const built = await buildPayload({ tid, isReply: false });
198
+ if (!built) return;
199
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
200
+
201
+ await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] });
202
+ };
203
+
204
+ Plugin.onTopicReply = async (data) => {
205
+ const settings = await getSettings();
206
+ if (!settings.notifyReplies) return;
207
+ if (!settings.webhookUrl) return;
208
+
209
+ const { tid, pid } = extractTidPid(data);
210
+ if (!tid || !pid) return;
211
+
212
+ const built = await buildPayload({ tid, pid, isReply: true });
213
+ if (!built) return;
214
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
215
+
216
+ await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] });
217
+ };
218
+
219
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-discord-onekite",
3
- "version": "1.0.16",
3
+ "version": "1.1.7",
4
4
  "description": "Discord webhook notifier for Onekite (NodeBB v4.x only)",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@ define('admin/plugins/discord-onekite', ['api'], function (api) {
21
21
  cids: getMultiSelectValues('#cids'),
22
22
  };
23
23
 
24
+ // api.post prefixes /api automatically, so server route must be /admin/...
24
25
  api.post('/admin/plugins/discord-onekite/save', payload).then(function () {
25
26
  if (window.app && typeof app.alertSuccess === 'function') {
26
27
  app.alertSuccess('Paramètres enregistrés !');