@chrisburnell/eleventy-cache-webmentions 0.0.1

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/.gitattributes ADDED
@@ -0,0 +1,14 @@
1
+ # These files are text and should be normalized (convert crlf => lf)
2
+ *.css text
3
+ *.js text
4
+ *.html text
5
+ *.php text
6
+ *.phtml text
7
+ *.json text
8
+
9
+ # Images should be treated as binary
10
+ # (binary is a macro for -text -diff)
11
+ *.png binary
12
+ *.jpeg binary
13
+ *.jpg binary
14
+ *.gif binary
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Chris Burnell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # eleventy-cache-assets
2
+
3
+ > Work-in-progress! ⚠️ Cache webmentions using eleventy-cache-assets and make them available to use in collections, templates, pages, etc.
4
+
5
+ ## Installation
6
+
7
+ - **With npm:** `npm install @chrisburnell/eleventy-cache-webmentions`
8
+ - **Direct download:** [https://github.com/chrisburnell/eleventy-cache-webmentions/archive/master.zip](https://github.com/chrisburnell/eleventy-cache-webmentions/archive/master.zip)
9
+
10
+ ## Learn more
11
+
12
+ I wrote more about **eleventy-cache-webmentions** here: [https://chrisburnell.com/eleventy-cache-webmentions/](https://chrisburnell.com/eleventy-cache-webmentions/).
13
+
14
+ ## Authors
15
+
16
+ So far, it’s just myself, [Chris Burnell](https://chrisburnell.com), but I welcome collaborators with ideas to bring to the table!
17
+
18
+ ## License
19
+
20
+ This project is licensed under an MIT license.
@@ -0,0 +1,181 @@
1
+ const fetch = require("node-fetch")
2
+ const sanitizeHTML = require("sanitize-html")
3
+ const uniqBy = require("lodash/uniqBy")
4
+ const { AssetCache } = require("@11ty/eleventy-cache-assets")
5
+
6
+ // Load .env variables with dotenv
7
+ require("dotenv").config()
8
+ const TOKEN = process.env.WEBMENTION_IO_TOKEN
9
+
10
+ const absoluteURL = (url, domain) => {
11
+ try {
12
+ return new URL(url, domain).toString()
13
+ } catch (e) {
14
+ console.log(`Trying to convert ${url} to be an absolute url with base ${domain} and failed.`)
15
+ return url
16
+ }
17
+ }
18
+
19
+ const baseUrl = (url) => {
20
+ let hashSplit = url.split("#")
21
+ let queryparamSplit = hashSplit[0].split("?")
22
+ return queryparamSplit[0]
23
+ }
24
+
25
+ const fixUrl = (url, urlReplacements) => {
26
+ return Object.entries(urlReplacements).reduce((accumulator, [key, value]) => {
27
+ const regex = new RegExp(key, "g")
28
+ return accumulator.replace(regex, value)
29
+ }, url)
30
+ }
31
+
32
+ const hostname = (value) => {
33
+ if (typeof value === "string" && value.includes("//")) {
34
+ const urlObject = new URL(value)
35
+ return urlObject.hostname
36
+ }
37
+ return value
38
+ }
39
+
40
+ const epoch = (value) => {
41
+ return new Date(value).getTime()
42
+ }
43
+
44
+ module.exports = (config, options = {}) => {
45
+ options = Object.assign(
46
+ {
47
+ duration: "23h",
48
+ key: "webmentions",
49
+ allowedHTML: {
50
+ allowedTags: ["b", "i", "em", "strong", "a"],
51
+ allowedAttributes: {
52
+ a: ["href"],
53
+ },
54
+ },
55
+ urlReplacements: [],
56
+ maximumHtmlLength: 2000,
57
+ },
58
+ options
59
+ )
60
+
61
+ const fetchWebmentions = async () => {
62
+ let asset = new AssetCache(options.key)
63
+ asset.ensureDir()
64
+
65
+ let webmentions = {
66
+ type: "feed",
67
+ name: "Webmentions",
68
+ children: [],
69
+ }
70
+
71
+ // If there is a cached file at all, grab its contents now
72
+ if (asset.isCacheValid("9001y")) {
73
+ webmentions = await asset.getCachedValue()
74
+ }
75
+
76
+ // If there is a cached file but it is outside of expiry, fetch fresh
77
+ // results since the most recent
78
+ if (!asset.isCacheValid(options.duration)) {
79
+ const since = asset._cache.getKey(options.key) ? asset._cache.getKey(options.key).cachedAt : false
80
+ const url = `https://webmention.io/api/mentions.jf2?domain=${hostname(options.domain)}&token=${TOKEN}&per-page=9001${since ? `&since=${since}` : ``}`
81
+ const response = await fetch(url)
82
+ if (response.ok) {
83
+ const feed = await response.json()
84
+ if (feed.children.length) {
85
+ console.log(`[${hostname(options.domain)}] ${feed.children.length} new Webmentions fetched into cache.`)
86
+ }
87
+ webmentions.children = [...feed.children, ...webmentions.children].sort((a, b) => {
88
+ return epoch(b.published || b["wm-received"]) - epoch(a.published || a["wm-received"])
89
+ })
90
+ await asset.save(webmentions, "json")
91
+ return webmentions
92
+ }
93
+ }
94
+
95
+ return webmentions
96
+ }
97
+
98
+ const filteredWebmentions = async () => {
99
+ const rawWebmentions = await fetchWebmentions()
100
+ let webmentions = {}
101
+
102
+ // Sort Webmentions into groups by target
103
+ rawWebmentions.children.forEach((webmention) => {
104
+ // Get the target of the Webmention and fix it up
105
+ let url = baseUrl(fixUrl(webmention["wm-target"].replace(/\/?$/, "/"), options.urlReplacements))
106
+
107
+ if (!webmentions[url]) {
108
+ webmentions[url] = []
109
+ }
110
+
111
+ webmentions[url].push(webmention)
112
+ })
113
+
114
+ // Sort Webmentions in groups by url and remove duplicates by wm-id
115
+ for (let url in webmentions) {
116
+ webmentions[url] = uniqBy(webmentions[url], (item) => {
117
+ return item["wm-id"]
118
+ })
119
+ }
120
+
121
+ return webmentions
122
+ }
123
+
124
+ const getWebmentions = async (url, allowedTypes) => {
125
+ const webmentions = await filteredWebmentions()
126
+ url = absoluteURL(url, options.domain)
127
+
128
+ if (!url || !webmentions || !webmentions[url]) {
129
+ return []
130
+ }
131
+
132
+ const results = webmentions[url]
133
+ // filter webmentions by allowedTypes only if passed
134
+ .filter((entry) => {
135
+ return Array.isArray(allowedTypes) ? allowedTypes.includes(entry["wm-property"]) : true
136
+ })
137
+ // remove webmentions without an author name
138
+ .filter((entry) => {
139
+ const { author } = entry
140
+ return !!author && !!author.name
141
+ })
142
+ // sanitize content of webmentions and check against HTML limit
143
+ .map((entry) => {
144
+ if (!("content" in entry)) {
145
+ return entry
146
+ }
147
+ const { html, text } = entry.content
148
+ if (html && html.length > options.maximumHtmlLength) {
149
+ entry.content.value = `mentioned this in <a href="${entry["wm-source"]}">${entry["wm-source"]}</a>`
150
+ } else {
151
+ entry.content.value = sanitizeHTML(html || text, options.allowedHTML)
152
+ }
153
+ return entry
154
+ })
155
+ // sort by published/wm-received
156
+ .sort((a, b) => {
157
+ return epoch(a.published || a["wm-received"]) - epoch(b.published || b["wm-received"])
158
+ })
159
+
160
+ return results
161
+ }
162
+
163
+ const getWebmentionsFilter = async (url, allowedTypes, callback) => {
164
+ const webmentions = await getWebmentions(url, allowedTypes)
165
+ if (typeof callback === "function") {
166
+ callback(null, webmentions)
167
+ } else {
168
+ return webmentions
169
+ }
170
+ }
171
+
172
+ if (config && options) {
173
+ if (!options.domain) {
174
+ throw new Error("domain is a required option to be passed when adding the plugin to your config using addPlugin.")
175
+ }
176
+
177
+ config.addNunjucksAsyncFilter("getWebmentions", getWebmentionsFilter)
178
+ } else {
179
+ return filteredWebmentions()
180
+ }
181
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@chrisburnell/eleventy-cache-webmentions",
3
+ "version": "0.0.1",
4
+ "description": "Cache webmentions from webmention.io using eleventy-cache-assets.",
5
+ "main": "eleventy-cache-webmentions.js",
6
+ "author": "Chris Burnell <me@chrisburnell.com>",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git@github.com:chrisburnell/chrisburnell.com.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/chrisburnell/chrisburnell.com/issues"
14
+ },
15
+ "engines": {
16
+ "node": ">=10"
17
+ },
18
+ "dependencies": {
19
+ "@11ty/eleventy-cache-assets": "^2.3.0",
20
+ "dotenv": "^10.0.0",
21
+ "lodash": "^4.17.21",
22
+ "node-fetch": "2.6.5",
23
+ "sanitize-html": "^2.5.2"
24
+ },
25
+ "keywords": [
26
+ "eleventy"
27
+ ]
28
+ }