UNPKG

webmention-receiver

Version:
215 lines (182 loc) 6.95 kB
import { WebMentionHandler } from 'webmention-handler' import BlobStorage from 'webmention-handler-netlify-blobs' import isEqual from 'fast-deep-equal' import HTTP from '../lib/HTTPResponse.js' import { translate } from '../lib/convert.js' import fetchWebmentions from '../lib/webmentionio.js' import sendWebhook from '../lib/webhook.js' export default class WebmentionReceiver { #store // Defaults to BlobStorage #token // (Optional) Generate a random string (https://generate-random.org/string-generator) #webhook // (Optional) webhook to send POST request to #handler constructor({ urls, store, token, webhook }) { this.#store = store ? store : new BlobStorage() this.#token = token this.#webhook = webhook this.#handler = new WebMentionHandler({ supportedHosts: urls, // ['example.com', 'www.example.com', 'example.net'] storageHandler: this.#store }) } #validateRequest = token => { if (!this.#token) return HTTP.INTERNAL_SERVER_ERROR('Missing token') if (!token) return HTTP.UNAUTHORIZED() if (this.#token != token) return HTTP.FORBIDDEN() } cleanupHandler = async (req) => { if ('GET' !== req.method) return HTTP.METHOD_NOT_ALLOWED() const params = new URL(req.url).searchParams const token = params.get('token') const error = this.#validateRequest(token) if (error) return error const total = await this.#store.clearAll() return HTTP.OK(`Deleted ${total} items`) } importHandler = async (req) => { if ('GET' !== req.method) return HTTP.METHOD_NOT_ALLOWED() const params = new URL(req.url).searchParams const token = params.get('token') const webmentionio = params.get('webmentionio') const error = this.#validateRequest(token) if (error) return error if (!webmentionio) return HTTP.BAD_REQUEST('Missing "webmentionio" token') try { const webmentions = await fetchWebmentions(webmentionio) console.log(`[INFO] Importing ${webmentions.length} items`) const targets = {} for (const wm of webmentions) { const mention = translate(wm) targets[mention.target] = targets[mention.target] || [] targets[mention.target].push(mention) } for (const [target, mentions] of Object.entries(targets)) { await this.#store.storeMentionsForPage(target, mentions) } return HTTP.OK(`Imported ${webmentions.length} webmentions for ${Object.entries(targets).length} targets`) } catch (error) { console.error('[ERROR]', error.message) return HTTP.BAD_REQUEST(error.message) } } #compareMentions = (m1, m2) => { const a = { ...m1 } const b = { ...m2 } delete a.parsed delete b.parsed return isEqual(a, b) } #processMention = async (mention, processed = {}) => { const results = await this.#handler.processMention(mention, true) if (!results) { // Should delete if it exists await this.#store.deleteMention(mention) return processed } for (const m of results) { if (!processed[m.target]) { processed[m.target] = await this.#handler.getMentionsForPage(m.target) || [] } const prev = processed[m.target].find(p => p.source === m.source) if (this.#compareMentions(prev, m)) continue console.log(`[INFO] Adding ${m.source} for ${m.target}`) processed[m.target].push(m) await sendWebhook(this.#webhook, m, 'processed') } return processed } #storeMentions = async (processed) => { for (const [target, mentions] of Object.entries(processed)) { console.log(`[INFO] Adding ${mentions.length} for ${target}`) await this.#store.storeMentionsForPage(target, mentions) } } processHandler = async () => { const mentions = await this.#store.getNextPendingMentions() let processed = {} for (const mention of mentions) { processed = await this.#processMention(mention, processed) } await this.#storeMentions(processed) return HTTP.OK() } #deleteMention = async (req) => { const params = new URL(req.url).searchParams const token = params.get('token') const error = this.#validateRequest(token) if (error) return error const source = params.get('source') if (!source) return HTTP.BAD_REQUEST('Missing "source"') const target = params.get('target') if (!target) return HTTP.BAD_REQUEST('Missing "target"') await this.#store.deleteMention({ source, target }) return HTTP.OK(`Deleted mention`) } webmentionHandler = async (req) => { if ('DELETE' === req.method) return await this.#deleteMention(req) if ('POST' !== req.method) return HTTP.METHOD_NOT_ALLOWED() const contentType = req.headers.get('content-type') let body if ('application/x-www-form-urlencoded' === contentType) { body = await req.formData() } else { body = new URL(req.url).searchParams } if (!body) return HTTP.BAD_REQUEST('Missing "source" and "target"') const source = body.get('source') if (!source) return HTTP.BAD_REQUEST('Missing "source"') const target = body.get('target') if (!target) return HTTP.BAD_REQUEST('Missing "target"') const recommendedResponse = await this.#handler.addPendingMention(source, target) if ([200, 201, 202].includes(recommendedResponse.code)) { await sendWebhook(this.#webhook, { source, target }) } return new Response('accepted', { status: recommendedResponse.code }) } // https://docs.netlify.com/build/functions/background-functions/ // By default, this always returns ACCEPTED (202) even if `source` or `target` // are missing so it's not ideal. Will revist after background functions come // out of beta. webmentionBackgroundHandler = async (req) => { if ('POST' !== req.method) return HTTP.METHOD_NOT_ALLOWED() const contentType = req.headers.get('content-type') let body if ('application/x-www-form-urlencoded' === contentType) { body = await req.formData() } else { body = new URL(req.url).searchParams } if (!body) return HTTP.BAD_REQUEST('Missing "source" and "target"') const source = body.get('source') if (!source) return HTTP.BAD_REQUEST('Missing "source"') const target = body.get('target') if (!target) return HTTP.BAD_REQUEST('Missing "target"') const processed = await this.#processMention({ source, target }) if (processed) await this.#storeMentions(processed) return HTTP.ACCEPTED() } webmentionsHandler = async (req) => { if ('GET' !== req.method) return HTTP.METHOD_NOT_ALLOWED() const params = new URL(req.url).searchParams const url = params.get('url') const token = params.get('token') if (url) { try { const type = params.get('type') const mentions = await this.#handler.getMentionsForPage(url, type) return HTTP.OK({ [url]: mentions }) } catch (error) { console.error('[ERROR]', error.message) return HTTP.BAD_REQUEST(`${url} is not valid`) } } else if (token) { const error = this.#validateRequest(token) if (error) return error const mentions = await this.#store.getAllMentions() return HTTP.OK(mentions) } return HTTP.BAD_REQUEST('Missing "url"') } }