@chrisburnell/eleventy-cache-webmentions 0.2.0 → 1.0.0-beta.2
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/eleventy-cache-webmentions.js +144 -124
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ const fetch = require("node-fetch")
|
|
|
2
2
|
const sanitizeHTML = require("sanitize-html")
|
|
3
3
|
const uniqBy = require("lodash/uniqBy")
|
|
4
4
|
const { AssetCache } = require("@11ty/eleventy-fetch")
|
|
5
|
+
const { defaultsDeep } = require("lodash")
|
|
5
6
|
|
|
6
7
|
const absoluteURL = (url, domain) => {
|
|
7
8
|
try {
|
|
@@ -37,161 +38,180 @@ const epoch = (value) => {
|
|
|
37
38
|
return new Date(value).getTime()
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
uniqueKey: "webmentions",
|
|
44
|
-
allowedHTML: {
|
|
45
|
-
allowedTags: ["b", "i", "em", "strong", "a"],
|
|
46
|
-
allowedAttributes: {
|
|
47
|
-
a: ["href"],
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
urlReplacements: {},
|
|
51
|
-
maximumHtmlLength: 2000,
|
|
52
|
-
maximumHtmlText: "mentioned this in",
|
|
53
|
-
},
|
|
54
|
-
options
|
|
55
|
-
)
|
|
41
|
+
const getPublished = (webmention) => {
|
|
42
|
+
return webmention["wm-received"] || webmention?.["data"]["published"] || webmention["verified_date"] || webmention["published"]
|
|
43
|
+
}
|
|
56
44
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
const getContent = (webmention) => {
|
|
46
|
+
return webmention?.["content"]["html"] || webmention["content"] || webmention?.["data"]["content"]
|
|
47
|
+
}
|
|
60
48
|
|
|
61
|
-
|
|
49
|
+
const getSource = (webmention) => {
|
|
50
|
+
return webmention["url"] || webmention?.["data"]["url"] || webmention["wm-source"] || webmention["source"]
|
|
51
|
+
}
|
|
62
52
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
53
|
+
const getTarget = (webmention) => {
|
|
54
|
+
return webmention["wm-target"] || webmention["target"]
|
|
55
|
+
}
|
|
67
56
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.catch((error) => {
|
|
89
|
-
console.log(`[${hostname(options.domain)}] Something went wrong with your request to ${hostname(options.feed)}!`, error)
|
|
90
|
-
})
|
|
91
|
-
}
|
|
57
|
+
const getType = (webmention) => {
|
|
58
|
+
return webmention["wm-property"] || webmention?.["activity"]["type"] || webmention["type"]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const defaults = {
|
|
62
|
+
uniqueKey: "webmentions",
|
|
63
|
+
allowedHTML: {
|
|
64
|
+
allowedTags: ["b", "i", "em", "strong", "a"],
|
|
65
|
+
allowedAttributes: {
|
|
66
|
+
a: ["href"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
urlReplacements: {},
|
|
70
|
+
maximumHtmlLength: 2000,
|
|
71
|
+
maximumHtmlText: "mentioned this in",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const fetchWebmentions = async (options) => {
|
|
75
|
+
let asset = new AssetCache(options.uniqueKey)
|
|
76
|
+
asset.ensureDir()
|
|
92
77
|
|
|
93
|
-
|
|
78
|
+
let webmentions = []
|
|
79
|
+
|
|
80
|
+
// If there is a cached file at all, grab its contents now
|
|
81
|
+
if (asset.isCacheValid("9001y")) {
|
|
82
|
+
webmentions = await asset.getCachedValue()
|
|
94
83
|
}
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
// If there is a cached file but it is outside of expiry, fetch fresh
|
|
86
|
+
// results since the most recent webmention
|
|
87
|
+
if (!asset.isCacheValid(options.duration)) {
|
|
88
|
+
const since = webmentions.length ? getPublished(webmentions[0]) : false
|
|
89
|
+
const url = `${options.feed}${since ? `${options.feed.includes("?") ? "&" : "?"}since=${since}` : ""}`
|
|
90
|
+
await fetch(url)
|
|
91
|
+
.then(async (response) => {
|
|
92
|
+
if (response.ok) {
|
|
93
|
+
const feed = await response.json()
|
|
94
|
+
if (feed[options.key].length) {
|
|
95
|
+
webmentions = uniqBy([...feed[options.key], ...webmentions], (entry) => {
|
|
96
|
+
return getSource(entry)
|
|
97
|
+
})
|
|
98
|
+
console.log(`[${hostname(options.domain)}] ${feed[options.key].length} new Webmentions fetched into cache.`)
|
|
99
|
+
}
|
|
100
|
+
await asset.save(webmentions, "json")
|
|
101
|
+
return webmentions
|
|
102
|
+
}
|
|
103
|
+
return Promise.reject(response)
|
|
104
|
+
})
|
|
105
|
+
.catch((error) => {
|
|
106
|
+
console.log(`[${hostname(options.domain)}] Something went wrong with your request to ${hostname(options.feed)}!`, error)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Get the target of the Webmention and fix it up
|
|
103
|
-
let url = baseUrl(fixUrl((webmention["wm-target"] || webmention["target"]).replace(/\/?$/, "/"), options.urlReplacements))
|
|
110
|
+
return webmentions
|
|
111
|
+
}
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const filteredWebmentions = async (options) => {
|
|
114
|
+
const rawWebmentions = await fetchWebmentions(options)
|
|
115
|
+
let webmentions = {}
|
|
108
116
|
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
// Sort Webmentions into groups by target
|
|
118
|
+
rawWebmentions.forEach((webmention) => {
|
|
119
|
+
// Get the target of the Webmention and fix it up
|
|
120
|
+
let url = baseUrl(fixUrl(getTarget(webmention).replace(/\/?$/, "/"), options.urlReplacements))
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
webmentions[url] = uniqBy(webmentions[url], (item) => {
|
|
115
|
-
return item["url"]
|
|
116
|
-
})
|
|
122
|
+
if (!webmentions[url]) {
|
|
123
|
+
webmentions[url] = []
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
|
|
126
|
+
webmentions[url].push(webmention)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Sort Webmentions in groups by url and remove duplicates by `url`
|
|
130
|
+
for (let url in webmentions) {
|
|
131
|
+
webmentions[url] = uniqBy(webmentions[url], (entry) => {
|
|
132
|
+
return getSource(entry)
|
|
133
|
+
})
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
url = absoluteURL(url, options.domain)
|
|
136
|
+
return webmentions
|
|
137
|
+
}
|
|
125
138
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
const getWebmentions = async (options, url, allowedTypes = {}) => {
|
|
140
|
+
const webmentions = await filteredWebmentions(options)
|
|
141
|
+
url = absoluteURL(url, options.domain)
|
|
129
142
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!("content" in entry)) {
|
|
143
|
-
return entry
|
|
144
|
-
}
|
|
145
|
-
const { html, text } = entry.content
|
|
146
|
-
if (html && html.length > options.maximumHtmlLength) {
|
|
147
|
-
entry.content.value = `${options.maximumHtmlText} <a href="${entry["wm-source"] || entry["source"]}">${entry["wm-source"] || entry["source"]}</a>`
|
|
148
|
-
} else if (Object.keys(options.allowedHTML).length) {
|
|
149
|
-
entry.content.value = sanitizeHTML(html || text, options.allowedHTML)
|
|
150
|
-
} else {
|
|
151
|
-
entry.content.value = html || text
|
|
152
|
-
}
|
|
143
|
+
if (!url || !webmentions || !webmentions[url]) {
|
|
144
|
+
return []
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const results = webmentions[url]
|
|
148
|
+
// filter webmentions by allowed response post types
|
|
149
|
+
.filter((entry) => {
|
|
150
|
+
return typeof allowedTypes === "object" && Object.keys(allowedTypes).length ? allowedTypes.includes(getType(entry)) : true
|
|
151
|
+
})
|
|
152
|
+
// sanitize content of webmentions against HTML limit
|
|
153
|
+
.map((entry) => {
|
|
154
|
+
if (!("content" in entry) || !("data" in entry)) {
|
|
153
155
|
return entry
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
})
|
|
156
|
+
}
|
|
157
|
+
const html = getContent(entry)
|
|
158
|
+
if (html && html.length > options.maximumHtmlLength) {
|
|
159
|
+
entry.content = `${options.maximumHtmlText} <a href="${getSource(entry)}">${getSource(entry)}</a>`
|
|
160
|
+
} else if (Object.keys(options.allowedHTML).length) {
|
|
161
|
+
entry.content = sanitizeHTML(html, options.allowedHTML)
|
|
162
|
+
} else {
|
|
163
|
+
entry.content = html
|
|
164
|
+
}
|
|
165
|
+
return entry
|
|
166
|
+
})
|
|
167
|
+
// sort by published
|
|
168
|
+
.sort((a, b) => {
|
|
169
|
+
return epoch(getPublished(a)) - epoch(getPublished(b))
|
|
170
|
+
})
|
|
159
171
|
|
|
160
|
-
|
|
172
|
+
return results
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const getWebmentionsFilter = async (options, url, allowedTypes, callback) => {
|
|
176
|
+
if (typeof callback !== "function") {
|
|
177
|
+
callback = allowedTypes
|
|
178
|
+
allowedTypes = {}
|
|
161
179
|
}
|
|
180
|
+
const webmentions = await getWebmentions(options, url, allowedTypes)
|
|
181
|
+
callback(null, webmentions)
|
|
182
|
+
}
|
|
162
183
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const webmentions = await getWebmentions(url, allowedTypes)
|
|
169
|
-
callback(null, webmentions)
|
|
184
|
+
module.exports = (eleventyConfig, options = {}) => {
|
|
185
|
+
options = Object.assign(defaults, options)
|
|
186
|
+
|
|
187
|
+
if (!options.domain) {
|
|
188
|
+
throw new Error("domain is a required field when adding the plugin to your eleventyConfig using addPlugin. See https://chrisburnell.com/eleventy-cache-webmentions/#installation for more information.")
|
|
170
189
|
}
|
|
171
190
|
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
191
|
+
if (!options.feed) {
|
|
192
|
+
throw new Error("feed is a required field when adding the plugin to your eleventyConfig using addPlugin. See https://chrisburnell.com/eleventy-cache-webmentions/#installation for more information.")
|
|
193
|
+
}
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
if (!options.key) {
|
|
196
|
+
throw new Error("key is a required field when adding the plugin to your eleventyConfig using addPlugin. See https://chrisburnell.com/eleventy-cache-webmentions/#installation for more information.")
|
|
197
|
+
}
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
199
|
+
if (eleventyConfig) {
|
|
200
|
+
// Global Data
|
|
201
|
+
eleventyConfig.addGlobalData("webmentions", filteredWebmentions(options))
|
|
184
202
|
|
|
185
203
|
// Liquid Filter
|
|
186
204
|
eleventyConfig.addLiquidFilter("getWebmentions", getWebmentionsFilter)
|
|
187
205
|
|
|
188
206
|
// Nunjucks Filter
|
|
189
207
|
eleventyConfig.addNunjucksAsyncFilter("getWebmentions", getWebmentionsFilter)
|
|
208
|
+
}
|
|
190
209
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
return {
|
|
211
|
+
defaults: defaults,
|
|
212
|
+
webmentions: filteredWebmentions(options),
|
|
213
|
+
fetchWebmentions: fetchWebmentions,
|
|
214
|
+
filteredWebmentions: filteredWebmentions,
|
|
215
|
+
getWebmentions: getWebmentions,
|
|
196
216
|
}
|
|
197
217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrisburnell/eleventy-cache-webmentions",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"description": "Fetch and cache webmentions using eleventy-fetch.",
|
|
5
5
|
"main": "eleventy-cache-webmentions.js",
|
|
6
6
|
"author": "Chris Burnell <me@chrisburnell.com>",
|