UNPKG

social-butterfly

Version:

Incorporate federated social network protocols easily. Used with Hello, world federated blog.

190 lines (172 loc) 7.69 kB
import { HTTPError } from './util/exceptions'; import React, { createElement as RcE } from 'react'; import { buildUrl } from './util/url_factory'; import { renderToString } from 'react-dom/server'; const LICENSES = { 'http://creativecommons.org/licenses/by/3.0/': { name: 'Creative Commons Attribution 3.0 Unported License', img: 'https://i.creativecommons.org/l/by/3.0/88x31.png', }, 'http://creativecommons.org/licenses/by-sa/3.0/': { name: 'Creative Commons Attribution-ShareAlike 3.0 Unported License', img: 'https://i.creativecommons.org/l/by-sa/3.0/88x31.png', }, 'http://creativecommons.org/licenses/by-nd/3.0/': { name: 'Creative Commons Attribution-NoDerivs 3.0 Unported License', img: 'https://i.creativecommons.org/l/by-nd/3.0/88x31.png', }, 'http://creativecommons.org/licenses/by-nc/3.0/': { name: 'Creative Commons Attribution-NonCommercial 3.0 Unported License', img: 'https://i.creativecommons.org/l/by-nc/3.0/88x31.png', }, 'http://creativecommons.org/licenses/by-nc-sa/3.0/': { name: 'Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License', img: 'https://i.creativecommons.org/l/by-nc-sa/3.0/88x31.png', }, 'http://creativecommons.org/licenses/by-nc-nd/3.0/': { name: 'Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License', img: 'https://i.creativecommons.org/l/by-nc-nd/3.0/88x31.png', }, 'http://purl.org/atompub/license#unspecified': { name: 'Simple Copyright', img: '', }, 'http://www.opensource.org/licenses/mit-license.php': { name: 'MIT License', img: '', }, }; export default (options) => async (req, res, next) => { const contentOwner = await options.getLocalUser(req.query.resource, req); if (!contentOwner) { return res.sendStatus(404); } const feed = await options.getLocalLatestContent(req.query.resource, req); let renderedTree = `<?xml version='1.0' encoding='UTF-8'?>` + renderToString(<ContentFeed req={req} feed={feed} contentOwner={contentOwner} constants={options.constants} />); // XXX(mime): in the feeds I have some attributes that are `ref`. However, ref isn't allowed in React, // so in the DOM they are `refXXX`. Return them to normal here, sigh. renderedTree = renderedTree.replace(/refXXX="([^"]+)"/g, 'ref="$1"'); res.type('xml'); res.send(renderedTree); }; function ContentFeed({ feed, contentOwner, constants, req }) { const updatedAt = feed.length && feed[0].updatedAt; return ( <GenericFeed contentOwner={contentOwner} constants={constants} req={req} updatedAt={updatedAt}> {feed.map((content) => ( <Entry key={content.name} content={content} req={req} /> ))} </GenericFeed> ); } export function GenericFeed({ req, children, constants, contentOwner, updatedAt }) { if (!contentOwner) { throw new HTTPError(404, undefined, 'feed: no content owner'); } const feedUrl = buildUrl({ req, pathname: req.originalUrl }); const salmonUrl = buildUrl({ req, pathname: '/api/social/salmon', searchParams: { resource: contentOwner.url } }); const namespaces = { xmlLang: 'en-US', xmlns: 'http://www.w3.org/2005/Atom', 'xmlns:activity': 'http://activitystrea.ms/spec/1.0/', 'xmlns:poco': 'http://portablecontacts.net/spec/1.0', 'xmlns:media': 'http://purl.org/syndication/atommedia', 'xmlns:thr': 'http://purl.org/syndication/thread/1.0', }; return ( <feed {...namespaces}> <generator uri="https://github.com/mimecuvalo/helloworld">Hello, world.</generator> <id>{feedUrl}</id> <title>{contentOwner.title}</title> <subtitle>a hello world site.</subtitle> <link rel="self" href={feedUrl} /> <link rel="alternate" type="text/html" href={contentOwner.url} /> <link rel="hub" href={constants.webSubHub} /> <link rel="salmon" href={salmonUrl} /> <link rel="http://salmon-protocol.org/ns/salmon-replies" href={salmonUrl} /> <link rel="http://salmon-protocol.org/ns/salmon-mention" href={salmonUrl} /> <link rel="license" href={contentOwner.license} /> {contentOwner.license ? ( <rights> {contentOwner.license === 'http://purl.org/atompub/license#unspecified' ? `Copyright ${new Date().getFullYear()} by ${contentOwner.name}` : `${LICENSES[contentOwner.license]?.['name']}: ${contentOwner.license}`} </rights> ) : null} {updatedAt ? <updated>{new Date(updatedAt).toISOString()}</updated> : null} <Author contentOwner={contentOwner} /> {contentOwner.logo ? <logo>{buildUrl({ req, pathname: contentOwner.logo })}</logo> : null} <icon> {contentOwner.favicon ? buildUrl({ req, pathname: contentOwner.favicon }) : buildUrl({ req, pathname: '/favicon.ico' })} </icon> {children} </feed> ); } const Author = ({ contentOwner }) => ( <author> {RcE('activity:object-type', {}, `http://activitystrea.ms/schema/1.0/person`)} <name>{contentOwner.name}</name> <uri>{contentOwner.url}</uri> <email>{contentOwner.email}</email> {RcE('poco:preferredusername', {}, contentOwner.username)} {RcE('poco:displayname', {}, contentOwner.name)} {RcE('poco:emails', {}, [ RcE('poco:value', { key: 'value' }, contentOwner.email), RcE('poco:type', { key: 'type' }, 'home'), RcE('poco:primary', { key: 'primary' }, 'true'), ])} {RcE('poco:urls', {}, [ RcE('poco:value', { key: 'value' }, contentOwner.url), RcE('poco:type', { key: 'type' }, 'profile'), RcE('poco:primary', { key: 'primary' }, 'true'), ])} </author> ); function Entry({ content, req }) { const statsImgSrc = buildUrl({ req, pathname: '/api/stats', searchParams: { resource: content.url } }); const statsImg = `<img src="${statsImgSrc}" />`; const absoluteUrlReplacement = buildUrl({ req, pathname: '/resource' }); // TODO(mime): this replacement is nite-lite specific... const html = '<![CDATA[' + content.view.replace(/(['"])\/resource/gm, `$1${absoluteUrlReplacement}`) + statsImg + ']]>'; const repliesUrl = buildUrl({ pathname: '/api/social/comments', searchParams: { resource: content.url } }); const repliesAttribs = { 'thr:count': content.comments_count, 'thr:updated': new Date(content.comments_updated).toISOString(), }; return ( <entry> <title>{content.title}</title> <link href={content.url} /> <id>{content.url}</id> <content type="html" dangerouslySetInnerHTML={{ __html: html }} /> <published>{new Date(content.createdAt).toISOString()}</published> <updated>{new Date(content.updatedAt).toISOString()}</updated> {RcE('activity:verb', {}, `http://activitystrea.ms/schema/1.0/post`)} {content.section === 'comments' ? ( /* XXX(mime): we'll never get here currently because we never render main sections in our feed */ <> {RcE('activity:object-type', {}, `http://activitystrea.ms/schema/1.0/comment`)} {/* see endpoint_with_apollo for refXXX transform */} {content.thread ? RcE('thr:in-reply-to', { refXXX: content.thread }) : null} {content.thread_user ? ( <> <link rel="ostatus:attention" href={content.thread_user} /> <link rel="mentioned" href={content.thread_user} /> </> ) : null} </> ) : ( RcE('activity:object-type', {}, `http://activitystrea.ms/schema/1.0/article`) )} {content.comments_count ? ( <link rel="replies" type="application/atom+xml" href={repliesUrl} {...repliesAttribs} /> ) : null} </entry> ); }