nodebb-plugin-discord-onekite 1.0.10 → 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,2 +1,5 @@
1
- # nodebb-plugin-discord-onekite v1.0.8
2
- Clean build (no debug). Server-side ACP + success alert.
1
+ # nodebb-plugin-discord-onekite v1.1.1
2
+
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,41 +61,79 @@ 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, type }) {
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']);
60
112
 
61
- const isReply = type === 'reply';
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);
115
+
116
+ // Super-minimal embed to avoid Discord rejecting unknown/invalid fields.
62
117
  const embed = {
63
- title: topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet'),
64
- url: isReply && pid ? `${topicUrl}/${pid}` : topicUrl,
65
- timestamp: new Date(isReply ? Date.now() : (topicData.timestamp || Date.now())).toISOString(),
66
- author: u?.username ? { name: u.username, icon_url: u.picture || undefined } : undefined,
67
- footer: { text: 'NodeBB → Discord (Onekite)' },
118
+ title,
119
+ description: isReply ? `Réponse de **${authorName}**` : `Sujet créé par **${authorName}**`,
68
120
  };
69
121
 
70
- 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}`;
129
+
130
+ return { topicData, embed, content };
131
+ }
71
132
 
72
- return { topicData, embed };
133
+ function extractTidPid(data) {
134
+ const tid = data?.tid || data?.topic?.tid || data?.post?.tid;
135
+ const pid = data?.pid || data?.post?.pid;
136
+ return { tid, pid };
73
137
  }
74
138
 
75
139
  const Plugin = {};
@@ -110,57 +174,37 @@ Plugin.addAdminNavigation = (header) => {
110
174
  return header;
111
175
  };
112
176
 
113
- Plugin.onTopicSave = async (data) => {
114
- try {
115
- const settings = await getSettings();
116
- if (!settings.webhookUrl) return;
117
-
118
- const tid = data?.tid || data?.topic?.tid;
119
- if (!tid) return;
177
+ Plugin.onTopicPost = async (data) => {
178
+ const settings = await getSettings();
179
+ if (!settings.webhookUrl) return;
120
180
 
121
- const isNew =
122
- data?.isNew === true ||
123
- data?.topic?.isNew === true ||
124
- (typeof data?.topic?.postcount === 'number' && data.topic.postcount === 1);
181
+ const { tid } = extractTidPid(data);
182
+ if (!tid) return;
125
183
 
126
- if (!isNew) return;
184
+ const built = await buildPayload({ tid, isReply: false });
185
+ if (!built) return;
127
186
 
128
- const built = await buildTopicEmbed({ tid, type: 'topic' });
129
- if (!built) return;
187
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
130
188
 
131
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
132
-
133
- await postToDiscord(settings.webhookUrl, { embeds: [built.embed] });
134
- } catch (err) {
135
- // eslint-disable-next-line no-console
136
- console.error(err);
137
- }
189
+ await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
138
190
  };
139
191
 
140
- Plugin.onPostSave = async (data) => {
141
- try {
142
- const settings = await getSettings();
143
- if (!settings.notifyReplies) return;
144
- if (!settings.webhookUrl) return;
192
+ Plugin.onTopicReply = async (data) => {
193
+ const settings = await getSettings();
194
+ if (!settings.notifyReplies) return;
195
+ if (!settings.webhookUrl) return;
145
196
 
146
- const pid = data?.pid || data?.post?.pid;
147
- const tid = data?.tid || data?.post?.tid;
148
- if (!pid || !tid) return;
197
+ const { tid, pid } = extractTidPid(data);
198
+ if (!tid || !pid) return;
149
199
 
150
- const isNew = data?.isNew === true || data?.post?.isNew === true;
151
- if (!isNew) return;
200
+ const built = await buildPayload({ tid, pid, isReply: true });
201
+ if (!built) return;
152
202
 
153
- const built = await buildTopicEmbed({ tid, pid, type: 'reply' });
154
- if (!built) return;
203
+ if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
155
204
 
156
- if (built.topicData?.mainPid && String(built.topicData.mainPid) === String(pid)) return;
157
- if (!cidAllowed(built.topicData.cid, settings.cids)) return;
205
+ if (!cidAllowed(built.topicData.cid, settings.cids)) return;
158
206
 
159
- await postToDiscord(settings.webhookUrl, { embeds: [built.embed] });
160
- } catch (err) {
161
- // eslint-disable-next-line no-console
162
- console.error(err);
163
- }
207
+ await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
164
208
  };
165
209
 
166
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.10",
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",
package/plugin.json CHANGED
@@ -13,13 +13,16 @@
13
13
  "method": "addAdminNavigation"
14
14
  },
15
15
  {
16
- "hook": "action:topic.save",
17
- "method": "onTopicSave"
16
+ "hook": "action:topic.post",
17
+ "method": "onTopicPost"
18
18
  },
19
19
  {
20
- "hook": "action:post.save",
21
- "method": "onPostSave"
20
+ "hook": "action:topic.reply",
21
+ "method": "onTopicReply"
22
22
  }
23
23
  ],
24
- "templates": "templates"
24
+ "templates": "templates",
25
+ "acpScripts": [
26
+ "static/lib/acp-toast.js"
27
+ ]
25
28
  }
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+ /* global $, app, ajaxify */
3
+
4
+ (function () {
5
+ function showToastIfSaved() {
6
+ if (!ajaxify || !ajaxify.data || ajaxify.data.template !== 'admin/plugins/discord-onekite') return;
7
+
8
+ try {
9
+ const url = new URL(window.location.href);
10
+ const saved = url.searchParams.get('saved');
11
+ if (saved === '1' || saved === 'true') {
12
+ if (window.app && typeof app.alertSuccess === 'function') {
13
+ app.alertSuccess('Paramètres enregistrés !');
14
+ }
15
+ url.searchParams.delete('saved');
16
+ window.history.replaceState({}, document.title, url.toString());
17
+ }
18
+ } catch (e) {}
19
+ }
20
+
21
+ $(window).on('action:ajaxify.end', showToastIfSaved);
22
+ $(showToastIfSaved);
23
+ })();