nodebb-plugin-discord-onekite 1.0.11 → 1.0.13

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,4 +1,8 @@
1
- # nodebb-plugin-discord-onekite v1.0.9
1
+ # nodebb-plugin-discord-onekite v1.1.2
2
2
 
3
- - Uses action:topic.post for new topics and action:topic.reply for replies (more reliable than action:*save for notifications).
4
- - Server-side ACP settings + NodeBB-style toast (bottom-right) after save.
3
+ Fix build error:
4
+ - Removed `acpScripts` and instead provides a proper AMD admin module via `modules` mapping.
5
+ This avoids RequireJS build failures.
6
+
7
+ - Toast "classique NodeBB" via app.alertSuccess when `?saved=1`.
8
+ - Discord notifications: minimal embed + fallback to plain content if Discord rejects embed.
package/library.js CHANGED
@@ -20,6 +20,32 @@ function normalizeCids(value) {
20
20
  return [];
21
21
  }
22
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 isValidHttpUrl(s) {
41
+ try {
42
+ const u = new URL(s);
43
+ return u.protocol === 'http:' || u.protocol === 'https:';
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
23
49
  async function getSettings() {
24
50
  const s = await meta.settings.get(SETTINGS_KEY);
25
51
  return {
@@ -35,40 +61,69 @@ function cidAllowed(topicCid, allowedCids) {
35
61
  }
36
62
 
37
63
  async function postToDiscord(webhookUrl, payload) {
38
- if (!webhookUrl) return;
39
-
40
64
  const res = await request(webhookUrl, {
41
65
  method: 'POST',
42
66
  headers: { 'Content-Type': 'application/json' },
43
67
  body: JSON.stringify(payload),
44
68
  });
45
69
 
70
+ const bodyText = await res.body.text().catch(() => '');
71
+
46
72
  if (res.statusCode < 200 || res.statusCode >= 300) {
47
- const text = await res.body.text().catch(() => '');
48
- throw new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${text}`);
73
+ const err = new Error(`[discord-onekite] Discord webhook HTTP ${res.statusCode}: ${bodyText}`);
74
+ err.statusCode = res.statusCode;
75
+ err.responseBody = bodyText;
76
+ throw err;
49
77
  }
50
78
  }
51
79
 
52
- async function buildTopicEmbed({ tid, pid, isReply }) {
53
- const baseUrl = meta.config.url;
80
+ async function sendDiscord(webhookUrl, payload, fallbackContent) {
81
+ if (!webhookUrl) return;
54
82
 
55
- const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'timestamp', 'mainPid']);
83
+ try {
84
+ await postToDiscord(webhookUrl, payload);
85
+ } catch (e) {
86
+ if (e && e.statusCode === 400 && fallbackContent) {
87
+ try {
88
+ await postToDiscord(webhookUrl, { content: fallbackContent });
89
+ return;
90
+ } catch (e2) {
91
+ // eslint-disable-next-line no-console
92
+ console.error(e2);
93
+ }
94
+ }
95
+ // eslint-disable-next-line no-console
96
+ console.error(e);
97
+ }
98
+ }
99
+
100
+ async function buildPayload({ tid, pid, isReply }) {
101
+ const baseUrl = normalizeBaseUrl(meta.config.url);
102
+
103
+ const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'mainPid']);
56
104
  if (!topicData) return null;
57
105
 
58
- const topicUrl = `${baseUrl}/topic/${topicData.slug || topicData.tid}`;
59
- const u = await user.getUserFields(topicData.uid, ['username', 'picture']);
106
+ const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
107
+ const url = isReply && pid ? `${topicUrl}/${pid}` : topicUrl;
108
+
109
+ const u = await user.getUserFields(topicData.uid, ['username']);
110
+ const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
111
+ const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
60
112
 
61
113
  const embed = {
62
- title: topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet'),
63
- url: isReply && pid ? `${topicUrl}/${pid}` : topicUrl,
64
- timestamp: new Date(Date.now()).toISOString(),
65
- author: u?.username ? { name: u.username, icon_url: u.picture || undefined } : undefined,
66
- footer: { text: 'NodeBB → Discord (Onekite)' },
114
+ title,
115
+ description: isReply ? `Réponse de **${authorName}**` : `Sujet créé par **${authorName}**`,
67
116
  };
68
117
 
69
- if (isReply) embed.description = 'Nouvelle réponse dans un sujet.';
118
+ if (isValidHttpUrl(url)) {
119
+ embed.url = url;
120
+ }
70
121
 
71
- return { topicData, embed };
122
+ const content = isReply
123
+ ? `🗨️ Nouvelle réponse : ${title}\n${url}`
124
+ : `🆕 Nouveau sujet : ${title}\n${url}`;
125
+
126
+ return { topicData, embed, content };
72
127
  }
73
128
 
74
129
  function extractTidPid(data) {
@@ -115,50 +170,37 @@ Plugin.addAdminNavigation = (header) => {
115
170
  return header;
116
171
  };
117
172
 
118
- // Fired when a new topic is posted (creation)
119
173
  Plugin.onTopicPost = async (data) => {
120
- try {
121
- const settings = await getSettings();
122
- if (!settings.webhookUrl) return;
174
+ const settings = await getSettings();
175
+ if (!settings.webhookUrl) return;
123
176
 
124
- const { tid } = extractTidPid(data);
125
- if (!tid) return;
177
+ const { tid } = extractTidPid(data);
178
+ if (!tid) return;
126
179
 
127
- const built = await buildTopicEmbed({ tid, isReply: false });
128
- if (!built) return;
180
+ const built = await buildPayload({ tid, isReply: false });
181
+ if (!built) return;
129
182
 
130
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
183
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
131
184
 
132
- await postToDiscord(settings.webhookUrl, { embeds: [built.embed] });
133
- } catch (err) {
134
- // eslint-disable-next-line no-console
135
- console.error(err);
136
- }
185
+ await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
137
186
  };
138
187
 
139
- // Fired when a reply is made in a topic
140
188
  Plugin.onTopicReply = async (data) => {
141
- try {
142
- const settings = await getSettings();
143
- if (!settings.notifyReplies) return;
144
- if (!settings.webhookUrl) return;
189
+ const settings = await getSettings();
190
+ if (!settings.notifyReplies) return;
191
+ if (!settings.webhookUrl) return;
145
192
 
146
- const { tid, pid } = extractTidPid(data);
147
- if (!tid || !pid) return;
193
+ const { tid, pid } = extractTidPid(data);
194
+ if (!tid || !pid) return;
148
195
 
149
- const built = await buildTopicEmbed({ tid, pid, isReply: true });
150
- if (!built) return;
196
+ const built = await buildPayload({ tid, pid, isReply: true });
197
+ if (!built) return;
151
198
 
152
- // Safety: don't notify if somehow this is the main post
153
- if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
199
+ if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
154
200
 
155
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
201
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
156
202
 
157
- await postToDiscord(settings.webhookUrl, { embeds: [built.embed] });
158
- } catch (err) {
159
- // eslint-disable-next-line no-console
160
- console.error(err);
161
- }
203
+ await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
162
204
  };
163
205
 
164
206
  module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-discord-onekite",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Discord webhook notifier for Onekite (NodeBB v4.x only)",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -22,7 +22,7 @@
22
22
  }
23
23
  ],
24
24
  "templates": "templates",
25
- "acpScripts": [
26
- "static/lib/acp-toast.js"
27
- ]
25
+ "modules": {
26
+ "../admin/plugins/discord-onekite.js": "static/lib/admin.js"
27
+ }
28
28
  }
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
- /* global $, app, ajaxify */
2
+ /* global app */
3
3
 
4
- (function () {
5
- function showToastIfSaved() {
6
- if (!ajaxify || !ajaxify.data || ajaxify.data.template !== 'admin/plugins/discord-onekite') return;
4
+ define('admin/plugins/discord-onekite', [], function () {
5
+ const ACP = {};
7
6
 
7
+ function toastIfSaved() {
8
8
  try {
9
9
  const url = new URL(window.location.href);
10
10
  const saved = url.searchParams.get('saved');
@@ -12,7 +12,6 @@
12
12
  if (window.app && typeof app.alertSuccess === 'function') {
13
13
  app.alertSuccess('Paramètres enregistrés !');
14
14
  }
15
- // remove the query param to avoid re-toasting on navigation
16
15
  url.searchParams.delete('saved');
17
16
  window.history.replaceState({}, document.title, url.toString());
18
17
  }
@@ -21,6 +20,9 @@
21
20
  }
22
21
  }
23
22
 
24
- $(window).on('action:ajaxify.end', showToastIfSaved);
25
- $(showToastIfSaved);
26
- })();
23
+ ACP.init = function () {
24
+ toastIfSaved();
25
+ };
26
+
27
+ return ACP;
28
+ });
@@ -1,15 +1,9 @@
1
1
  <div class="acp-page-container">
2
2
  <h4>Discord Onekite</h4>
3
3
  <p class="text-muted">
4
- Notifications Discord via webhook (rendu serveur).
4
+ Notifications Discord via webhook.
5
5
  </p>
6
6
 
7
- <!-- IF saved -->
8
- <div class="alert alert-success" role="alert">
9
- Paramètres enregistrés !
10
- </div>
11
- <!-- ENDIF saved -->
12
-
13
7
  <form role="form" method="post" action="/admin/plugins/discord-onekite/save">
14
8
  <input type="hidden" name="_csrf" value="{config.csrf_token}" />
15
9
 
@@ -39,4 +33,10 @@
39
33
 
40
34
  <button type="submit" class="btn btn-primary">Enregistrer</button>
41
35
  </form>
36
+
37
+ <noscript>
38
+ <!-- IF saved -->
39
+ <div class="alert alert-success mt-3" role="alert">Paramètres enregistrés !</div>
40
+ <!-- ENDIF saved -->
41
+ </noscript>
42
42
  </div>