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.
- package/library.js +21 -146
- package/package.json +1 -1
- package/plugin.json +1 -1
package/library.js
CHANGED
|
@@ -3,133 +3,45 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
30
|
+
// Markdown masked links -> URL
|
|
122
31
|
str = str.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, _text, url) => {
|
|
123
32
|
return normalizeUrl(url);
|
|
124
33
|
});
|
|
125
34
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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:
|
|
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
package/plugin.json
CHANGED