nodebb-plugin-link-preview 1.3.2 → 2.0.0
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 +95 -71
- package/package.json +4 -3
- package/plugin.json +3 -1
package/library.js
CHANGED
|
@@ -9,8 +9,8 @@ const dns = require('dns');
|
|
|
9
9
|
|
|
10
10
|
const { getLinkPreview } = require('link-preview-js');
|
|
11
11
|
const { load } = require('cheerio');
|
|
12
|
+
const { isURL } = require('validator');
|
|
12
13
|
|
|
13
|
-
const db = require.main.require('./src/database');
|
|
14
14
|
const meta = require.main.require('./src/meta');
|
|
15
15
|
const cache = require.main.require('./src/cache');
|
|
16
16
|
const posts = require.main.require('./src/posts');
|
|
@@ -82,64 +82,24 @@ async function preview(url) {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!hashes.length) {
|
|
85
|
+
async function process(content, opts) {
|
|
86
|
+
const { embedHtml, embedImage, embedAudio, embedVideo } = await meta.settings.get('link-preview');
|
|
87
|
+
if (![embedHtml, embedImage, embedAudio, embedVideo].some(prop => prop === 'on')) {
|
|
89
88
|
return content;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
const
|
|
93
|
-
const attachments = (await db.getObjects(keys)).filter(Boolean);
|
|
94
|
-
const urls = attachments
|
|
95
|
-
.filter(attachment => cache.has(`link-preview:${attachment.url}`))
|
|
96
|
-
.map(attachment => attachment.url);
|
|
97
|
-
|
|
98
|
-
const previews = urls.map(url => cache.get(`link-preview:${url}`));
|
|
99
|
-
const html = await Promise.all(previews.map(async preview => await render(preview)));
|
|
100
|
-
|
|
101
|
-
// Append all readily-available previews to content
|
|
102
|
-
content = `${content}\n\n<div class="row">${html.map(html => `<div class="col-6">${html}</div>`).join('\n')}</div>`;
|
|
91
|
+
const requests = new Map();
|
|
103
92
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
postsCache.del(String(pid));
|
|
111
|
-
|
|
112
|
-
// fire post edit event with mocked data
|
|
113
|
-
if (await topics.exists(tid)) {
|
|
114
|
-
const urls = attachments.map(attachment => attachment.url);
|
|
115
|
-
|
|
116
|
-
const previews = urls.map(url => cache.get(`link-preview:${url}`));
|
|
117
|
-
let html = await Promise.all(previews.map(async preview => await render(preview)));
|
|
118
|
-
html = `${content}\n\n<div class="row">${html.map(html => `<div class="col-6">${html}</div>`).join('\n')}</div>`;
|
|
119
|
-
|
|
120
|
-
websockets.in(`topic_${tid}`).emit('event:post_edited', {
|
|
121
|
-
post: {
|
|
122
|
-
tid,
|
|
123
|
-
pid,
|
|
124
|
-
changed: true,
|
|
125
|
-
content: html,
|
|
126
|
-
},
|
|
127
|
-
topic: {},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
93
|
+
// Retrieve attachments
|
|
94
|
+
if (opts.hasOwnProperty('pid') && await posts.exists(opts.pid)) {
|
|
95
|
+
const attachments = await posts.attachments.get(opts.pid);
|
|
96
|
+
attachments.forEach(({ url, _type }) => {
|
|
97
|
+
const type = _type || 'attachment';
|
|
98
|
+
requests.set(url, { type });
|
|
131
99
|
});
|
|
132
100
|
}
|
|
133
101
|
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function process(content, opts) {
|
|
138
|
-
const { embedHtml, embedImage, embedAudio, embedVideo } = await meta.settings.get('link-preview');
|
|
139
|
-
if (![embedHtml, embedImage, embedAudio, embedVideo].some(prop => prop === 'on')) {
|
|
140
|
-
return content;
|
|
141
|
-
}
|
|
142
|
-
|
|
102
|
+
// Parse inline urls
|
|
143
103
|
const $ = load(content, null, false);
|
|
144
104
|
for (const anchor of $('a')) {
|
|
145
105
|
const $anchor = $(anchor);
|
|
@@ -162,28 +122,73 @@ async function process(content, opts) {
|
|
|
162
122
|
continue;
|
|
163
123
|
}
|
|
164
124
|
|
|
125
|
+
// Inline url takes precedence over attachment
|
|
126
|
+
requests.set(url, {
|
|
127
|
+
type: 'inline',
|
|
128
|
+
target: $anchor,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Render cache hits immediately
|
|
133
|
+
let attachmentHtml = '';
|
|
134
|
+
const cold = new Set();
|
|
135
|
+
await Promise.all(Array.from(requests.keys()).map(async (url) => {
|
|
136
|
+
const options = requests.get(url);
|
|
165
137
|
const cached = cache.get(`link-preview:${url}`);
|
|
166
138
|
if (cached) {
|
|
167
139
|
const html = await render(cached);
|
|
168
140
|
if (html) {
|
|
169
|
-
|
|
141
|
+
switch (options.type) {
|
|
142
|
+
case 'inline': {
|
|
143
|
+
const $anchor = options.target;
|
|
144
|
+
$anchor.replaceWith($(html));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
case 'attachment': {
|
|
149
|
+
attachmentHtml += `<div class="col-6">${html}</div>`;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
170
153
|
}
|
|
171
|
-
|
|
154
|
+
} else {
|
|
155
|
+
cold.add(url);
|
|
172
156
|
}
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// Start preview for cache misses, but continue for now so as to not block response
|
|
160
|
+
if (cold.size) {
|
|
161
|
+
const coldArr = Array.from(cold);
|
|
162
|
+
Promise.all(coldArr.map(preview)).then(async (previews) => {
|
|
163
|
+
await Promise.all(previews.map(async (preview, idx) => {
|
|
164
|
+
if (!preview) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
173
167
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
168
|
+
const url = coldArr[idx];
|
|
169
|
+
const options = requests.get(url);
|
|
170
|
+
const parsedUrl = new URL(url);
|
|
171
|
+
preview.hostname = parsedUrl.hostname;
|
|
172
|
+
|
|
173
|
+
const html = await render(preview);
|
|
174
|
+
if (html) {
|
|
175
|
+
switch (options.type) {
|
|
176
|
+
case 'inline': {
|
|
177
|
+
const $anchor = options.target;
|
|
178
|
+
$anchor.replaceWith($(html));
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'attachment': {
|
|
183
|
+
attachmentHtml += `<div class="col-6">${html}</div>`;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}));
|
|
182
189
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
190
|
+
let content = $.html();
|
|
191
|
+
content += attachmentHtml ? `\n\n<div class="row">${attachmentHtml}</div>` : '';
|
|
187
192
|
|
|
188
193
|
// bust posts cache item
|
|
189
194
|
if (opts.hasOwnProperty('pid') && await posts.exists(opts.pid)) {
|
|
@@ -191,13 +196,12 @@ async function process(content, opts) {
|
|
|
191
196
|
|
|
192
197
|
// fire post edit event with mocked data
|
|
193
198
|
if (opts.hasOwnProperty('tid') && await topics.exists(opts.tid)) {
|
|
194
|
-
$anchor.replaceWith($(html));
|
|
195
199
|
websockets.in(`topic_${opts.tid}`).emit('event:post_edited', {
|
|
196
200
|
post: {
|
|
197
201
|
tid: opts.tid,
|
|
198
202
|
pid: opts.pid,
|
|
199
203
|
changed: true,
|
|
200
|
-
content
|
|
204
|
+
content,
|
|
201
205
|
},
|
|
202
206
|
topic: {},
|
|
203
207
|
});
|
|
@@ -206,7 +210,9 @@ async function process(content, opts) {
|
|
|
206
210
|
});
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
|
|
213
|
+
content = $.html();
|
|
214
|
+
content += attachmentHtml ? `\n\n<div class="row">${attachmentHtml}</div>` : '';
|
|
215
|
+
return content;
|
|
210
216
|
}
|
|
211
217
|
|
|
212
218
|
async function render(preview) {
|
|
@@ -285,14 +291,32 @@ plugin.onParse = async (payload) => {
|
|
|
285
291
|
if (typeof payload === 'string') { // raw
|
|
286
292
|
payload = await process(payload, {});
|
|
287
293
|
} else if (payload && payload.postData && payload.postData.content) { // post
|
|
288
|
-
|
|
289
|
-
content = await processAttachments({ content, pid, tid });
|
|
294
|
+
const { content, pid, tid } = payload.postData;
|
|
290
295
|
payload.postData.content = await process(content, { pid, tid });
|
|
291
296
|
}
|
|
292
297
|
|
|
293
298
|
return payload;
|
|
294
299
|
};
|
|
295
300
|
|
|
301
|
+
plugin.onPost = async ({ post }) => {
|
|
302
|
+
if (post._activitypub) {
|
|
303
|
+
return; // no attachment parsing for content from activitypub; attachments saved via notes.assert
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Only match standalone URLs on their own line
|
|
307
|
+
const lines = post.content.split('\n');
|
|
308
|
+
const urls = lines.filter(line => isURL(line));
|
|
309
|
+
|
|
310
|
+
let previews = await Promise.all(urls.map(async url => await preview(url)));
|
|
311
|
+
previews = previews.map(({ url, contentType: mediaType }) => ({
|
|
312
|
+
type: 'inline',
|
|
313
|
+
url,
|
|
314
|
+
mediaType,
|
|
315
|
+
})).filter(Boolean);
|
|
316
|
+
|
|
317
|
+
posts.attachments.update(post.pid, previews);
|
|
318
|
+
};
|
|
319
|
+
|
|
296
320
|
plugin.addAdminNavigation = (header) => {
|
|
297
321
|
header.plugins.push({
|
|
298
322
|
route: '/plugins/link-preview',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodebb-plugin-link-preview",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A starter kit for quickly creating NodeBB plugins",
|
|
5
5
|
"main": "library.js",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"readmeFilename": "README.md",
|
|
33
33
|
"nbbpm": {
|
|
34
|
-
"compatibility": "^
|
|
34
|
+
"compatibility": "^4.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@commitlint/cli": "17.6.5",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"cheerio": "^1.0.0-rc.12",
|
|
47
|
-
"link-preview-js": "^3.0.4"
|
|
47
|
+
"link-preview-js": "^3.0.4",
|
|
48
|
+
"validator": "^13.11.0"
|
|
48
49
|
}
|
|
49
50
|
}
|
package/plugin.json
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
|
8
8
|
{ "hook": "filter:settings.get", "method": "applyDefaults" },
|
|
9
9
|
{ "hook": "filter:parse.post", "method": "onParse" },
|
|
10
|
-
{ "hook": "filter:parse.raw", "method": "onParse" }
|
|
10
|
+
{ "hook": "filter:parse.raw", "method": "onParse" },
|
|
11
|
+
{ "hook": "action:post.save", "method": "onPost" },
|
|
12
|
+
{ "hook": "action:post.edit", "method": "onPost" }
|
|
11
13
|
],
|
|
12
14
|
"scss": [
|
|
13
15
|
"static/scss/link-preview.scss"
|