nodebb-plugin-discord-onekite 1.0.12 → 1.0.14

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,5 +1,10 @@
1
- # nodebb-plugin-discord-onekite v1.1.1
1
+ # nodebb-plugin-discord-onekite v1.1.3
2
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.
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)
6
+
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
@@ -40,7 +40,6 @@ controllers.renderAdminPage = async function (req, res) {
40
40
  res.render('admin/plugins/discord-onekite', {
41
41
  settings: settings || {},
42
42
  categories: categoriesForTpl,
43
- saved: req.query && (req.query.saved === '1' || req.query.saved === 'true'),
44
43
  });
45
44
  };
46
45
 
package/library.js CHANGED
@@ -7,6 +7,7 @@ const meta = require.main.require('./src/meta');
7
7
  const middleware = require.main.require('./src/middleware');
8
8
 
9
9
  const topics = require.main.require('./src/topics');
10
+ const posts = require.main.require('./src/posts');
10
11
  const user = require.main.require('./src/user');
11
12
 
12
13
  const controllers = require('./lib/controllers');
@@ -46,6 +47,27 @@ function isValidHttpUrl(s) {
46
47
  }
47
48
  }
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
+
49
71
  async function getSettings() {
50
72
  const s = await meta.settings.get(SETTINGS_KEY);
51
73
  return {
@@ -83,22 +105,29 @@ async function sendDiscord(webhookUrl, payload, fallbackContent) {
83
105
  try {
84
106
  await postToDiscord(webhookUrl, payload);
85
107
  } catch (e) {
86
- // Discord embed validation error is commonly 400 with {"embeds":["0"]}
87
- // Retry with plain content so notifications still arrive.
88
108
  if (e && e.statusCode === 400 && fallbackContent) {
89
109
  try {
90
110
  await postToDiscord(webhookUrl, { content: fallbackContent });
91
111
  return;
92
112
  } catch (e2) {
93
- // eslint-disable-next-line no-console
94
113
  console.error(e2);
95
114
  }
96
115
  }
97
- // eslint-disable-next-line no-console
98
116
  console.error(e);
99
117
  }
100
118
  }
101
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
+
102
131
  async function buildPayload({ tid, pid, isReply }) {
103
132
  const baseUrl = normalizeBaseUrl(meta.config.url);
104
133
 
@@ -106,26 +135,32 @@ async function buildPayload({ tid, pid, isReply }) {
106
135
  if (!topicData) return null;
107
136
 
108
137
  const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
109
- const url = isReply && pid ? `${topicUrl}/${pid}` : topicUrl;
138
+ const postUrl = isReply && pid ? `${topicUrl}/${pid}` : `${topicUrl}/${topicData.mainPid || ''}`.replace(/\/$/, '');
110
139
 
111
140
  const u = await user.getUserFields(topicData.uid, ['username']);
112
-
113
141
  const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
114
142
  const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
115
143
 
116
- // Super-minimal embed to avoid Discord rejecting unknown/invalid fields.
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
+
117
147
  const embed = {
118
148
  title,
119
- description: isReply ? `Réponse de **${authorName}**` : `Sujet créé par **${authorName}**`,
149
+ description,
120
150
  };
121
151
 
122
- if (isValidHttpUrl(url)) {
123
- embed.url = url;
152
+ if (isValidHttpUrl(postUrl)) {
153
+ embed.url = postUrl;
124
154
  }
125
155
 
126
- const content = isReply
127
- ? `🗨️ Nouvelle réponse : ${title}\n${url}`
128
- : `🆕 Nouveau sujet : ${title}\n${url}`;
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');
129
164
 
130
165
  return { topicData, embed, content };
131
166
  }
@@ -146,7 +181,8 @@ Plugin.init = async ({ router }) => {
146
181
  controllers.renderAdminPage
147
182
  );
148
183
 
149
- router.post('/admin/plugins/discord-onekite/save',
184
+ // AJAX save endpoint for ACP (used by admin.js)
185
+ router.post('/api/admin/plugins/discord-onekite/save',
150
186
  middleware.admin.checkPrivileges,
151
187
  async (req, res) => {
152
188
  try {
@@ -156,11 +192,11 @@ Plugin.init = async ({ router }) => {
156
192
  cids: req.body.cids || '',
157
193
  };
158
194
  await meta.settings.set(SETTINGS_KEY, payload);
195
+ res.json({ ok: true });
159
196
  } catch (e) {
160
- // eslint-disable-next-line no-console
161
197
  console.error('[discord-onekite] save failed', e);
198
+ res.status(500).json({ ok: false });
162
199
  }
163
- res.redirect('/admin/plugins/discord-onekite?saved=1');
164
200
  }
165
201
  );
166
202
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-discord-onekite",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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
  }
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+ /* global $, app */
3
+
4
+ define('admin/plugins/discord-onekite', ['api'], function (api) {
5
+ const ACP = {};
6
+
7
+ function getMultiSelectValues(selector) {
8
+ const el = document.querySelector(selector);
9
+ if (!el) return [];
10
+ return Array.from(el.selectedOptions || []).map(o => o.value);
11
+ }
12
+
13
+ ACP.init = function () {
14
+ const $form = $('.discord-onekite-settings');
15
+ if (!$form.length) return;
16
+
17
+ $('#save').off('click.discordOnekite').on('click.discordOnekite', function () {
18
+ const payload = {
19
+ webhookUrl: String($('#webhookUrl').val() || '').trim(),
20
+ notifyReplies: $('#notifyReplies').is(':checked'),
21
+ cids: getMultiSelectValues('#cids'),
22
+ };
23
+
24
+ api.post('/admin/plugins/discord-onekite/save', payload).then(function () {
25
+ if (window.app && typeof app.alertSuccess === 'function') {
26
+ app.alertSuccess('Paramètres enregistrés !');
27
+ }
28
+ }).catch(function (err) {
29
+ // eslint-disable-next-line no-console
30
+ console.error(err);
31
+ if (window.app && typeof app.alertError === 'function') {
32
+ app.alertError('Erreur lors de l’enregistrement');
33
+ }
34
+ });
35
+ });
36
+ };
37
+
38
+ return ACP;
39
+ });
@@ -1,18 +1,10 @@
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
- <form role="form" method="post" action="/admin/plugins/discord-onekite/save">
14
- <input type="hidden" name="_csrf" value="{config.csrf_token}" />
15
-
7
+ <form role="form" class="discord-onekite-settings">
16
8
  <div class="mb-3">
17
9
  <label class="form-label" for="webhookUrl">Discord Webhook URL</label>
18
10
  <input type="text" class="form-control" id="webhookUrl" name="webhookUrl" value="{settings.webhookUrl}" placeholder="https://discord.com/api/webhooks/..." />
@@ -37,6 +29,6 @@
37
29
  </p>
38
30
  </div>
39
31
 
40
- <button type="submit" class="btn btn-primary">Enregistrer</button>
32
+ <!-- IMPORT admin/partials/save_button.tpl -->
41
33
  </form>
42
34
  </div>
@@ -1,23 +0,0 @@
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
- })();