nodebb-plugin-onekite-discord 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.
Files changed (3) hide show
  1. package/library.js +21 -146
  2. package/package.json +1 -1
  3. package/plugin.json +1 -1
package/library.js CHANGED
@@ -3,133 +3,45 @@
3
3
 
4
4
 
5
5
  /**
6
- * Extract links from text in several formats NodeBB may produce.
7
- * Returns array of { text, url }.
8
- *
9
- * Supported inputs:
10
- * - Markdown: [text](url)
11
- * - HTML: <a href="url">text</a>
12
- * - NodeBB rendered: text(url) or text (url)
13
- * - Bare URLs: https://... or http://... or www.example.com
14
- *
15
- * Note: We'll output masked links in embed fields, where Discord supports them.
6
+ * Discord embed descriptions do not support masked links like [text](url).
7
+ * To keep links clickable, flatten any link-like markup into plain URLs,
8
+ * which Discord auto-linkifies.
16
9
  */
17
- function extractDiscordLinks(str) {
18
- if (!str || typeof str !== 'string') return [];
19
-
20
- const links = [];
21
- const seen = new Set();
22
-
23
- const normalizeUrl = (u) => {
24
- if (!u) return null;
25
- let url = String(u).trim();
26
- url = url.replace(/^<(.+)>$/, '$1');
27
- url = url.replace(/^https:\//i, 'https://');
28
- url = url.replace(/^http:\//i, 'http://');
29
- if (/^www\./i.test(url)) url = 'https://' + url;
30
- // Very small sanity check
31
- if (!/^https?:\/\//i.test(url)) return null;
32
- return url;
33
- };
34
-
35
- const push = (text, url) => {
36
- const u = normalizeUrl(url);
37
- if (!u) return;
38
- const key = `${text || ''}||${u}`;
39
- if (seen.has(key)) return;
40
- seen.add(key);
41
- links.push({ text: (text || '').trim(), url: u });
42
- };
43
-
44
- // HTML links
45
- str.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi, (_m, href, text) => {
46
- push(text, href);
47
- return _m;
48
- });
49
-
50
- // Markdown masked links
51
- str.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, url) => {
52
- push(text, url);
53
- return _m;
54
- });
55
-
56
- // NodeBB rendered: "texte du lien(url)" or "texte du lien (url)"
57
- str.replace(/([^\n\r()]{1,200}?)\s*\((https?:\/\/[^)\s]+|www\.[^)\s]+)\)/g, (_m, text, url) => {
58
- push(text, url);
59
- return _m;
60
- });
61
-
62
- // Bare URLs (avoid trailing punctuation)
63
- str.replace(/\b(https?:\/\/[^\s<>()]+)\b/g, (_m, url) => {
64
- push('', url.replace(/[),.;!?]+$/g, ''));
65
- return _m;
66
- });
67
- str.replace(/\b(www\.[^\s<>()]+)\b/g, (_m, url) => {
68
- push('', url.replace(/[),.;!?]+$/g, ''));
69
- return _m;
70
- });
71
-
72
- return links;
73
- }
74
-
75
- /**
76
- * Remove/flatten link markup from text for embed.description readability.
77
- * - [text](url) -> text
78
- * - <a href="url">text</a> -> text
79
- * - text(url) -> text
80
- * - leaves bare URLs as-is (optional), but we can also remove them if desired.
81
- */
82
- function stripLinkMarkup(str) {
83
- if (!str || typeof str !== 'string') return str;
84
-
85
- // HTML links -> text
86
- str = str.replace(/<a\s+[^>]*href=["'][^"']+["'][^>]*>(.*?)<\/a>/gi, (_m, text) => text);
87
-
88
- // Markdown masked -> text
89
- str = str.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text) => text);
90
-
91
- // NodeBB rendered -> text
92
- str = str.replace(/([^\n\r()]{1,200}?)\s*\((https?:\/\/[^)\s]+|www\.[^)\s]+)\)/g, (_m, text) => String(text).trim());
93
-
94
- return str;
95
- }
96
-
97
- /**
98
- * Normalize links for Discord embeds.
99
- * Discord does NOT support masked links in embed descriptions.
100
- * NodeBB may already render links as: texte(url)
101
- * We always output a plain URL so Discord auto-linkifies it.
102
- */
103
- function formatDiscordLinks(str) {
10
+ function flattenLinksToUrls(str) {
104
11
  if (!str || typeof str !== 'string') return str;
105
12
 
106
13
  const normalizeUrl = (u) => {
107
- if (!u) return u;
14
+ if (!u) return '';
108
15
  let url = String(u).trim();
109
16
  url = url.replace(/^<(.+)>$/, '$1');
17
+ // fix common typos
110
18
  url = url.replace(/^https:\//i, 'https://');
111
19
  url = url.replace(/^http:\//i, 'http://');
20
+ // add scheme for bare www.
112
21
  if (/^www\./i.test(url)) url = 'https://' + url;
113
22
  return url;
114
23
  };
115
24
 
116
- // HTML <a href="...">...</a> -> URL
25
+ // HTML links -> URL
117
26
  str = str.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>.*?<\/a>/gi, (_m, href) => {
118
27
  return normalizeUrl(href);
119
28
  });
120
29
 
121
- // Markdown [text](url) -> URL
30
+ // Markdown masked links -> URL
122
31
  str = str.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, _text, url) => {
123
32
  return normalizeUrl(url);
124
33
  });
125
34
 
126
- // Already-rendered form from NodeBB: texte du lien(url) or texte du lien (url)
127
- // Convert to: "texte du lien https://url" so Discord auto-linkifies.
128
- str = str.replace(/([^\n\r()]{1,200}?)\s*\((https?:\/\/[^)\s]+|www\.[^)\s]+)\)/g, (_m, text, url) => {
129
- const cleanText = String(text).trim();
130
- const cleanUrl = normalizeUrl(url);
131
- if (!cleanUrl) return _m;
132
- return cleanText ? `${cleanText} ${cleanUrl}` : cleanUrl;
35
+ // NodeBB rendered: "texte du lien(url)" or "texte du lien (url)" -> URL
36
+ str = str.replace(/([^\n\r()]{1,200}?)\s*\((https?:\/\/[^)\s]+|www\.[^)\s]+)\)/g, (_m, _text, url) => {
37
+ return normalizeUrl(url);
38
+ });
39
+
40
+ // Ensure bare "www.xxx" tokens become "https://www.xxx"
41
+ str = str.replace(/\b(www\.[^\s<>()]+)\b/g, (_m, url) => {
42
+ // strip trailing punctuation
43
+ const clean = url.replace(/[),.;!?]+$/g, '');
44
+ return normalizeUrl(clean);
133
45
  });
134
46
 
135
47
  return str;
@@ -176,26 +88,6 @@ function ensureAbsoluteUrl(baseUrl, maybeUrl) {
176
88
  return s;
177
89
  }
178
90
 
179
-
180
- function normalizeExcerptLinks(input) {
181
- if (!input) return '';
182
- let s = String(input);
183
-
184
- // Convert HTML anchors to "text (url)" before stripping HTML
185
- s = s.replace(/<a\s+[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (m, href, inner) => {
186
- const text = stripHtml(inner).trim() || href;
187
- return `${text} (${href})`;
188
- });
189
-
190
- // Convert Markdown links [text](url) to "text (url)"
191
- s = s.replace(/\[([^\]]+)\]\(([^\s)]+)\)/g, (m, label, url) => `${label} (${url})`);
192
-
193
- // Fix common malformed scheme "https:/example.com" -> "https://example.com"
194
- s = s.replace(/\b(https?):\/(?!\/)/g, '$1://');
195
-
196
- return s;
197
- }
198
-
199
91
  function stripHtml(html) {
200
92
  if (!html) return '';
201
93
  return String(html)
@@ -297,8 +189,7 @@ async function getPostExcerpt(pid) {
297
189
  if (!pid) return '';
298
190
  try {
299
191
  const p = await posts.getPostFields(pid, ['content']);
300
- const normalized = normalizeExcerptLinks(p && p.content);
301
- const text = stripHtml(normalized);
192
+ const text = stripHtml(p && p.content);
302
193
  return truncate(text, 500);
303
194
  } catch {
304
195
  return '';
@@ -329,22 +220,6 @@ async function buildEmbed({ tid, pid, isReply }) {
329
220
 
330
221
  const excerpt = await getPostExcerpt(isReply ? pid : topicData.mainPid);
331
222
 
332
- // Extract all links and put them in embed fields (Discord supports masked links in fields)
333
- const extractedLinks = extractDiscordLinks(excerpt || '');
334
- const linkFields = [];
335
- const maxFields = 25; // Discord limit
336
- for (let i = 0; i < extractedLinks.length && linkFields.length < maxFields; i += 1) {
337
- const { text, url } = extractedLinks[i];
338
- const label = text && text.length ? text : url;
339
- // Field values have a 1024 char limit; keep it safe
340
- const value = `[${label.substring(0, 200)}](${url})`;
341
- linkFields.push({
342
- name: `Lien ${linkFields.length + 1}`,
343
- value: value.substring(0, 1024),
344
- inline: false,
345
- });
346
- }
347
-
348
223
  const postForUser = await posts.getPostFields(isReply ? pid : topicData.mainPid, ['uid']);
349
224
  const username = await user.getUserField(postForUser.uid, 'username');
350
225
 
@@ -352,7 +227,7 @@ for (let i = 0; i < extractedLinks.length && linkFields.length < maxFields; i +=
352
227
  const embedTitle = `${replyIcon}${safeTitle} – ${username}`.slice(0, 256);
353
228
 
354
229
  // Embed title becomes clickable via embed.url
355
- return { topicData, content: '', embed: { title: embedTitle, url: targetUrl, description: stripLinkMarkup(excerpt || ''), fields: linkFields } };
230
+ return { topicData, content: '', embed: { title: embedTitle, url: targetUrl, description: flattenLinksToUrls(excerpt || '') } };
356
231
  }
357
232
 
358
233
  function extractTidPid(data) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-discord",
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",
package/plugin.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "acpScripts": [
32
32
  "public/admin.js"
33
33
  ],
34
- "version": "1.0.13"
34
+ "version": "1.0.15"
35
35
  }