nodebb-plugin-discord-onekite 1.0.13 → 1.0.15

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,8 @@
1
- # nodebb-plugin-discord-onekite v1.1.2
1
+ # nodebb-plugin-discord-onekite v1.1.4
2
2
 
3
- Fix build error:
4
- - Removed `acpScripts` and instead provides a proper AMD admin module via `modules` mapping.
5
- This avoids RequireJS build failures.
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
6
7
 
7
- - Toast "classique NodeBB" via app.alertSuccess when `?saved=1`.
8
- - Discord notifications: minimal embed + fallback to plain content if Discord rejects embed.
8
+ ACP remains with the "disk" save button and NodeBB toast.
@@ -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 {
@@ -88,40 +110,56 @@ async function sendDiscord(webhookUrl, payload, fallbackContent) {
88
110
  await postToDiscord(webhookUrl, { content: fallbackContent });
89
111
  return;
90
112
  } catch (e2) {
91
- // eslint-disable-next-line no-console
92
113
  console.error(e2);
93
114
  }
94
115
  }
95
- // eslint-disable-next-line no-console
96
116
  console.error(e);
97
117
  }
98
118
  }
99
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, 500);
126
+ } catch {
127
+ return '';
128
+ }
129
+ }
130
+
100
131
  async function buildPayload({ tid, pid, isReply }) {
101
132
  const baseUrl = normalizeBaseUrl(meta.config.url);
102
133
 
103
134
  const topicData = await topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'title', 'slug', 'mainPid']);
104
135
  if (!topicData) return null;
105
136
 
137
+ // Always build the forum link like the example: https://www.onekite.com/topic/<tid or slug>
106
138
  const topicUrl = ensureAbsoluteUrl(baseUrl, `/topic/${topicData.slug || topicData.tid}`);
107
- const url = isReply && pid ? `${topicUrl}/${pid}` : topicUrl;
139
+
140
+ // For replies, link directly to the post if possible
141
+ const postUrl = (isReply && pid) ? `${topicUrl}/${pid}` : topicUrl;
108
142
 
109
143
  const u = await user.getUserFields(topicData.uid, ['username']);
110
144
  const title = (topicData.title || (isReply ? 'Nouvelle réponse' : 'Nouveau sujet')).toString().slice(0, 256);
111
145
  const authorName = (u && u.username ? String(u.username) : 'Utilisateur').slice(0, 256);
112
146
 
147
+ const excerpt = await getPostExcerpt(isReply ? pid : topicData.mainPid);
148
+ const description = truncate(excerpt || (isReply ? `Réponse de ${authorName}` : `Sujet créé par ${authorName}`), 500);
149
+
150
+ // Minimal embed but with URL so the title is clickable
113
151
  const embed = {
114
152
  title,
115
- description: isReply ? `Réponse de **${authorName}**` : `Sujet créé par **${authorName}**`,
153
+ description,
116
154
  };
155
+ if (isValidHttpUrl(postUrl)) embed.url = postUrl;
117
156
 
118
- if (isValidHttpUrl(url)) {
119
- embed.url = url;
120
- }
121
-
122
- const content = isReply
123
- ? `🗨️ Nouvelle réponse : ${title}\n${url}`
124
- : `🆕 Nouveau sujet : ${title}\n${url}`;
157
+ // Ensure the message is clickable: markdown link + also raw URL in first line
158
+ const content = [
159
+ postUrl, // raw URL first line
160
+ isReply ? `🗨️ Nouvelle réponse : [${title}](${postUrl})` : `🆕 Nouveau sujet : [${title}](${topicUrl})`,
161
+ description ? description : '',
162
+ ].filter(Boolean).join('\n');
125
163
 
126
164
  return { topicData, embed, content };
127
165
  }
@@ -142,7 +180,8 @@ Plugin.init = async ({ router }) => {
142
180
  controllers.renderAdminPage
143
181
  );
144
182
 
145
- router.post('/admin/plugins/discord-onekite/save',
183
+ // AJAX save endpoint for ACP (used by admin.js)
184
+ router.post('/api/admin/plugins/discord-onekite/save',
146
185
  middleware.admin.checkPrivileges,
147
186
  async (req, res) => {
148
187
  try {
@@ -152,11 +191,11 @@ Plugin.init = async ({ router }) => {
152
191
  cids: req.body.cids || '',
153
192
  };
154
193
  await meta.settings.set(SETTINGS_KEY, payload);
194
+ res.json({ ok: true });
155
195
  } catch (e) {
156
- // eslint-disable-next-line no-console
157
196
  console.error('[discord-onekite] save failed', e);
197
+ res.status(500).json({ ok: false });
158
198
  }
159
- res.redirect('/admin/plugins/discord-onekite?saved=1');
160
199
  }
161
200
  );
162
201
  };
@@ -182,7 +221,8 @@ Plugin.onTopicPost = async (data) => {
182
221
 
183
222
  if (!cidAllowed(built.topicData.cid, settings.cids)) return;
184
223
 
185
- await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
224
+ // Send content + embed (content ensures clickable link always)
225
+ await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] }, built.content);
186
226
  };
187
227
 
188
228
  Plugin.onTopicReply = async (data) => {
@@ -200,7 +240,7 @@ Plugin.onTopicReply = async (data) => {
200
240
 
201
241
  if (!cidAllowed(built.topicData.cid, settings.cids)) return;
202
242
 
203
- await sendDiscord(settings.webhookUrl, { embeds: [built.embed] }, built.content);
243
+ await sendDiscord(settings.webhookUrl, { content: built.content, embeds: [built.embed] }, built.content);
204
244
  };
205
245
 
206
246
  module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-discord-onekite",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Discord webhook notifier for Onekite (NodeBB v4.x only)",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
@@ -1,27 +1,38 @@
1
1
  'use strict';
2
- /* global app */
2
+ /* global $, app */
3
3
 
4
- define('admin/plugins/discord-onekite', [], function () {
4
+ define('admin/plugins/discord-onekite', ['api'], function (api) {
5
5
  const ACP = {};
6
6
 
7
- function toastIfSaved() {
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
- // ignore
20
- }
7
+ function getMultiSelectValues(selector) {
8
+ const el = document.querySelector(selector);
9
+ if (!el) return [];
10
+ return Array.from(el.selectedOptions || []).map(o => o.value);
21
11
  }
22
12
 
23
13
  ACP.init = function () {
24
- toastIfSaved();
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
+ });
25
36
  };
26
37
 
27
38
  return ACP;
@@ -4,9 +4,7 @@
4
4
  Notifications Discord via webhook.
5
5
  </p>
6
6
 
7
- <form role="form" method="post" action="/admin/plugins/discord-onekite/save">
8
- <input type="hidden" name="_csrf" value="{config.csrf_token}" />
9
-
7
+ <form role="form" class="discord-onekite-settings">
10
8
  <div class="mb-3">
11
9
  <label class="form-label" for="webhookUrl">Discord Webhook URL</label>
12
10
  <input type="text" class="form-control" id="webhookUrl" name="webhookUrl" value="{settings.webhookUrl}" placeholder="https://discord.com/api/webhooks/..." />
@@ -31,12 +29,6 @@
31
29
  </p>
32
30
  </div>
33
31
 
34
- <button type="submit" class="btn btn-primary">Enregistrer</button>
32
+ <!-- IMPORT admin/partials/save_button.tpl -->
35
33
  </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
34
  </div>