nodebb-plugin-discord-onekite 1.0.11 → 1.0.12

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,5 @@
1
- # nodebb-plugin-discord-onekite v1.0.9
1
+ # nodebb-plugin-discord-onekite v1.1.1
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:
4
+ - Makes embeds ultra-minimal (title/description/url only).
5
+ - On Discord 400 embed validation error, retries with plain `content` so a notification is always delivered.
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,73 @@ 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;
77
+ }
78
+ }
79
+
80
+ async function sendDiscord(webhookUrl, payload, fallbackContent) {
81
+ if (!webhookUrl) return;
82
+
83
+ try {
84
+ await postToDiscord(webhookUrl, payload);
85
+ } catch (e) {
86
+ // Discord embed validation error is commonly 400 with {"embeds":["0"]}
87
+ // Retry with plain content so notifications still arrive.
88
+ if (e && e.statusCode === 400 && fallbackContent) {
89
+ try {
90
+ await postToDiscord(webhookUrl, { content: fallbackContent });
91
+ return;
92
+ } catch (e2) {
93
+ // eslint-disable-next-line no-console
94
+ console.error(e2);
95
+ }
96
+ }
97
+ // eslint-disable-next-line no-console
98
+ console.error(e);
49
99
  }
50
100
  }
51
101
 
52
- async function buildTopicEmbed({ tid, pid, isReply }) {
53
- const baseUrl = meta.config.url;
102
+ async function buildPayload({ tid, pid, isReply }) {
103
+ const baseUrl = normalizeBaseUrl(meta.config.url);
54
104
 
55
- const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'timestamp', 'mainPid']);
105
+ const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'mainPid']);
56
106
  if (!topicData) return null;
57
107
 
58
- const topicUrl = `${baseUrl}/topic/${topicData.slug || topicData.tid}`;
59
- const u = await user.getUserFields(topicData.uid, ['username', 'picture']);
108
+ const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
109
+ const url = isReply && pid ? `${topicUrl}/${pid}` : topicUrl;
110
+
111
+ const u = await user.getUserFields(topicData.uid, ['username']);
112
+
113
+ const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
114
+ const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
60
115
 
116
+ // Super-minimal embed to avoid Discord rejecting unknown/invalid fields.
61
117
  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)' },
118
+ title,
119
+ description: isReply ? `Réponse de **${authorName}**` : `Sujet créé par **${authorName}**`,
67
120
  };
68
121
 
69
- if (isReply) embed.description = 'Nouvelle réponse dans un sujet.';
122
+ if (isValidHttpUrl(url)) {
123
+ embed.url = url;
124
+ }
125
+
126
+ const content = isReply
127
+ ? `🗨️ Nouvelle réponse : ${title}\n${url}`
128
+ : `🆕 Nouveau sujet : ${title}\n${url}`;
70
129
 
71
- return { topicData, embed };
130
+ return { topicData, embed, content };
72
131
  }
73
132
 
74
133
  function extractTidPid(data) {
@@ -115,50 +174,37 @@ Plugin.addAdminNavigation = (header) => {
115
174
  return header;
116
175
  };
117
176
 
118
- // Fired when a new topic is posted (creation)
119
177
  Plugin.onTopicPost = async (data) => {
120
- try {
121
- const settings = await getSettings();
122
- if (!settings.webhookUrl) return;
178
+ const settings = await getSettings();
179
+ if (!settings.webhookUrl) return;
123
180
 
124
- const { tid } = extractTidPid(data);
125
- if (!tid) return;
181
+ const { tid } = extractTidPid(data);
182
+ if (!tid) return;
126
183
 
127
- const built = await buildTopicEmbed({ tid, isReply: false });
128
- if (!built) return;
184
+ const built = await buildPayload({ tid, isReply: false });
185
+ if (!built) return;
129
186
 
130
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
187
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
131
188
 
132
- await postToDiscord(settings.webhookUrl, { embeds: [built.embed] });
133
- } catch (err) {
134
- // eslint-disable-next-line no-console
135
- console.error(err);
136
- }
189
+ await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
137
190
  };
138
191
 
139
- // Fired when a reply is made in a topic
140
192
  Plugin.onTopicReply = async (data) => {
141
- try {
142
- const settings = await getSettings();
143
- if (!settings.notifyReplies) return;
144
- if (!settings.webhookUrl) return;
193
+ const settings = await getSettings();
194
+ if (!settings.notifyReplies) return;
195
+ if (!settings.webhookUrl) return;
145
196
 
146
- const { tid, pid } = extractTidPid(data);
147
- if (!tid || !pid) return;
197
+ const { tid, pid } = extractTidPid(data);
198
+ if (!tid || !pid) return;
148
199
 
149
- const built = await buildTopicEmbed({ tid, pid, isReply: true });
150
- if (!built) return;
200
+ const built = await buildPayload({ tid, pid, isReply: true });
201
+ if (!built) return;
151
202
 
152
- // Safety: don't notify if somehow this is the main post
153
- if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
203
+ if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
154
204
 
155
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
205
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
156
206
 
157
- await postToDiscord(settings.webhookUrl, { embeds: [built.embed] });
158
- } catch (err) {
159
- // eslint-disable-next-line no-console
160
- console.error(err);
161
- }
207
+ await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
162
208
  };
163
209
 
164
210
  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.12",
4
4
  "description": "Discord webhook notifier for Onekite (NodeBB v4.x only)",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -12,13 +12,10 @@
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
  }
19
- } catch (e) {
20
- // ignore
21
- }
18
+ } catch (e) {}
22
19
  }
23
20
 
24
21
  $(window).on('action:ajaxify.end', showToastIfSaved);