From 89c2f07795ed0d2828ab6602fa17591a1c814fdf Mon Sep 17 00:00:00 2001 From: James King Date: Thu, 18 Mar 2021 01:46:21 +0000 Subject: [PATCH] feat!: implemented webmention receiving and cron-scheduled ci runs --- .circleci/config.yml | 57 ++++++- assets/images/default_avatar.png | Bin 0 -> 428 bytes .../images/default_avatar.png:Zone.Identifier | 0 assets/styles/styles.css | 117 +++++++++++++++ package-lock.json | 23 +++ package.json | 1 + scripts/generateThoughts.ts | 142 +++++++++++++++++- scripts/getWebmentions.ts | 86 +++++++++++ scripts/humble.ts | 4 +- thoughts/template.html | 86 +++++++++++ 10 files changed, 504 insertions(+), 12 deletions(-) create mode 100644 assets/images/default_avatar.png create mode 100644 assets/images/default_avatar.png:Zone.Identifier create mode 100644 scripts/getWebmentions.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 36bfe8a..6bbacfd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,7 +35,7 @@ jobs: # a collection of steps - store_artifacts: path: ~/tmp/reports/eslint - build_without_version: # build the project - requires dependencies to have been installed + build_dev: # build the project - requires dependencies to have been installed executor: node-project steps: - checkout @@ -57,7 +57,32 @@ jobs: # a collection of steps - ~/tmp/firebase.json - ~/tmp/package-lock.json - build_and_version: + build_prod_no_version: + executor: node-project + steps: + - checkout + - restore_cache: + key: dependency-cache-{{ .Environment.CIRCLE_WORKFLOW_ID }} + - run: + name: Add Git User details + command: | + git config --global user.email "ripixel+ci@gmail.com" + git config --global user.name "CircleCi" + - run: # run build + name: Build + command: npm run build + - store_artifacts: # for display in Artifacts: https://circleci.com/docs/2.0/artifacts/ + path: public + prefix: public + - save_cache: # special step to save the public cache and deploy files + key: deploy-cache-{{ .Environment.CIRCLE_WORKFLOW_ID }} + paths: + - ~/tmp/public + - ~/tmp/.firebaserc + - ~/tmp/firebase.json + - ~/tmp/package-lock.json + + build_prod_with_version: executor: node-project steps: - checkout @@ -115,13 +140,13 @@ workflows: - lint: requires: - install_deps - - build_without_version: + - build_dev: filters: branches: ignore: master requires: - install_deps - - build_and_version: + - build_prod_with_version: filters: branches: only: master @@ -133,11 +158,31 @@ workflows: only: master requires: - lint - - build_and_version + - build_prod_with_version - deploy_to_firebase_stg: filters: branches: only: staging requires: - lint - - build_without_version + - build_dev + hourly: + triggers: + - schedule: + cron: '55 * * * *' + filters: + branches: + only: + - master + jobs: + - install_deps + - lint: + requires: + - install_deps + - build_prod_no_version: + requires: + - install_deps + - deploy_to_firebase_prod: + requires: + - lint + - build_prod_no_version diff --git a/assets/images/default_avatar.png b/assets/images/default_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..71f9602ea7a3aa39e6b35b3c27b19030c07ee4b5 GIT binary patch literal 428 zcmV;d0aN~oP)0P@|jOW7g1Qdsq-Z#qHagA=f31~7y2dXjeL&1 zTt>#2GvJ~4{vyjV)9dxjirjl+%<0a=-EP-Nx(0IoY<`lII_Ek`k~ESeX*lOPl2YJ# zAupG7wUYozH-wMi@(-C+CBN8}d_lx{5V^Is1$@L30K7ZrPC~lBi%33q;nuhNHa|xz zssLc@oNJZ&mhOV-bea-=_4=Th=XvUr_*z5v^LQzft$L7*fo2U30DJ<+HLZQXKgBmE Wt^20w|FOvc0000 div { + max-width: 100%; + padding-bottom: 20px; +} + +.mention-links a { + display: inline-block; +} + +.mention-links img { + background-color: #999; + max-width: 30px; + vertical-align: middle; + border-left: 2px solid #fff; +} + +.comments { + max-width: 100%; +} + +.comments .mention-links { + display: flex; + flex-direction: column; +} + +.mention-links .comment { + background: #025a44; + margin-bottom: 20px; + position: relative; +} + +.mention-links .comment:last-child { + margin-bottom: 0; +} + +.mention-links .comment .comment-link { + padding: 5px; + float: right; + margin-right: 0; + margin-bottom: 0; + font-size: 0.8em; +} + +.mention-links .comment .comment-link:hover { + transform: none; +} + +.mention-links .comment p { + padding: 0 10px 10px; +} + +@media only screen and (max-width: 600px) { + .mentions { + flex-wrap: nowrap; + overflow-x: auto; + padding: 20px 40px 10px; + } + + .mention-links { + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + } + + .comments .mention-links { + white-space: normal; + } + + .mention-links .comment { + max-width: 100%; + } + + .mention-links .comment p { + padding: 10px; + } + + .mentions .item { + margin-bottom: 0; + } +} + +@media only screen and (max-width: 420px) { + .mentions { + padding: 20px 30px 10px; + } +} + .details { display: flex; justify-content: flex-start; @@ -332,6 +445,10 @@ main.thoughts.details { background: rgb(1, 65, 49); } +main.thoughts.mentions { + background: rgb(0, 112, 84); +} + main.profile { min-height: 0; background: rgb(17, 50, 143); diff --git a/package-lock.json b/package-lock.json index a664c56..efee773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -677,6 +677,29 @@ "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==", "dev": true }, + "@types/node-fetch": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz", + "integrity": "sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", diff --git a/package.json b/package.json index 04af906..be8dcca 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "homepage": "https://github.com/ripixel/ripixel-website#readme", "devDependencies": { "@types/node": "^13.13.5", + "@types/node-fetch": "^2.5.8", "@types/prettier": "^2.0.1", "@types/showdown": "^1.9.3", "@typescript-eslint/eslint-plugin": "^3.2.0", diff --git a/scripts/generateThoughts.ts b/scripts/generateThoughts.ts index 96805c4..fde566b 100644 --- a/scripts/generateThoughts.ts +++ b/scripts/generateThoughts.ts @@ -4,10 +4,133 @@ import showdownHighlight from 'showdown-highlight'; import { format as dateFormat } from 'date-fns'; import findInDir from './findInDir'; +import { getWebmentions, webmentionsForPage } from './getWebmentions'; const ARTICLES_TO_SHOW = 5; -const generateThoughts = (): void => { +const generateWebmentionBlock = ( + tag: string, + content: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mentions: any[] +): string => { + const startBlockTag = ``; + const endBlockTag = ``; + const startBlockPos = content.indexOf(startBlockTag) + startBlockTag.length; + const endBlockPos = content.indexOf(endBlockTag); + + const mentionBlock = content.substr( + startBlockPos, + endBlockPos - startBlockPos + ); + + let blockToPaste = ''; + + if (mentions.length > 0) { + const repBlocks: string[] = []; + + const startRepBlockTag = ``; + const endRepBlockTag = ``; + const startRepBlockPos = + mentionBlock.indexOf(startRepBlockTag) + startRepBlockTag.length; + const endRepBlockPos = mentionBlock.indexOf(endRepBlockTag); + + const mentionRepBlock = mentionBlock.substr( + startRepBlockPos, + endRepBlockPos - startRepBlockPos + ); + + const isComment = tag === 'COMMENTS'; + + mentions.slice(0, 19).forEach((mention) => { + repBlocks.push( + mentionRepBlock + .replace( + /{mention_link}/g, + !isComment ? mention.url : mention.author.url + ) + .replace( + /{mention_avatar}/g, + (!isComment ? mention.photo : mention.author.photo) ?? + '/default_avatar.png' + ) + .replace( + /{mention_name}/g, + !isComment ? mention.name : mention.author.name + ) + .replace(/{comment}/g, isComment ? mention.content.value : '') + .replace(/{comment_link}/g, isComment ? mention.url : '') + ); + }); + + blockToPaste = + `${mentionBlock}`.substr(0, startRepBlockPos) + + repBlocks.join('') + + `${mentionBlock}`.substr( + endRepBlockPos, + mentionBlock.length - endRepBlockPos + ); + } + + return ( + `${content}`.substr(0, startBlockPos) + + blockToPaste.replace( + `{${tag}}`, + `${mentions.length > 19 ? '19+' : mentions.length}` + ) + + `${content}`.substr(endBlockPos, content.length - endBlockPos) + ); +}; + +const generateWebmentions = ( + webmentions: unknown[], + page: string, + preWebmentionsArticleContents: string +): string => { + const { likes, comments, reposts } = webmentionsForPage( + webmentions, + `thoughts/${page}` + ); + + if (likes.length === 0 && comments.length === 0 && reposts.length === 0) { + const startBlockTag = ``; + const endBlockTag = ``; + const startBlockPos = + preWebmentionsArticleContents.indexOf(startBlockTag) + + startBlockTag.length; + const endBlockPos = preWebmentionsArticleContents.indexOf(endBlockTag); + + return ( + `${preWebmentionsArticleContents}`.substr(0, startBlockPos) + + `${preWebmentionsArticleContents}`.substr( + endBlockPos, + preWebmentionsArticleContents.length - endBlockPos + ) + ); + } + + const contentAfterLikes = generateWebmentionBlock( + 'LIKES', + preWebmentionsArticleContents, + likes + ); + + const contentAfterComments = generateWebmentionBlock( + 'COMMENTS', + contentAfterLikes, + comments + ); + + const conentAfterReposts = generateWebmentionBlock( + 'REPOSTS', + contentAfterComments, + reposts + ); + + return conentAfterReposts; +}; + +const generateThoughts = async (): Promise => { // console.log('/// Beginning generation of thoughts'); const partials = findInDir('./partials', '.html'); @@ -44,10 +167,13 @@ const generateThoughts = (): void => { dateNum: number; }> = []; + const webmentions = await getWebmentions(); + articles.forEach((article) => { const articleWithoutFolder = article .replace('thoughts/articles/', '') .replace('.md', '.html'); + console.log(`Processing article ${articleWithoutFolder}`); const [datestring, titleWithDash] = articleWithoutFolder .replace('.html', '') .split('_'); @@ -58,7 +184,7 @@ const generateThoughts = (): void => { const splitBody = body.split('

'); - const articleContents = thoughtsPageTemplateContents + const preWebmentionsArticleContents = thoughtsPageTemplateContents .replace('{title}', title) .replace('{date}', date) .replace('{body}', body) @@ -77,6 +203,12 @@ const generateThoughts = (): void => { : '' ); + const articleContents = generateWebmentions( + webmentions, + articleWithoutFolder, + preWebmentionsArticleContents + ); + // console.log(`Found article ${title} published ${date}`); fs.writeFileSync( `public/thoughts/${articleWithoutFolder}`, @@ -98,8 +230,10 @@ const generateThoughts = (): void => { // console.log('Updating thoughts page proper'); let thoughtsPageContents = fs.readFileSync('./public/thoughts.html', 'utf8'); - const startRepPos = thoughtsPageContents.indexOf('') + 16; // +16 for length of comment tag - const endRepPos = thoughtsPageContents.indexOf(''); + const startTag = ''; + const endTag = ''; + const startRepPos = thoughtsPageContents.indexOf(startTag) + startTag.length; + const endRepPos = thoughtsPageContents.indexOf(endTag); const repeatableBlock = thoughtsPageContents.substr( startRepPos, diff --git a/scripts/getWebmentions.ts b/scripts/getWebmentions.ts new file mode 100644 index 0000000..16daef4 --- /dev/null +++ b/scripts/getWebmentions.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import fetch from 'node-fetch'; +import { URL } from 'url'; + +const WEBMENTION_BASE_URL = 'https://webmention.io/api/mentions.jf2'; +const DOMAIN = 'www.ripixel.co.uk'; +const WEBMENTION_IO_TOKEN = '09zN_zXeEJBP9eJPc_dDuw'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getWebmentions = async (): Promise => { + const url = `${WEBMENTION_BASE_URL}?domain=${DOMAIN}&token=${WEBMENTION_IO_TOKEN}&per-page=1000`; + + try { + const res = await fetch(url); + if (res.ok) { + const feed = await res.json(); + return feed.children as any[]; + } + } catch (err) { + console.error(err); + return []; + } + return []; +}; + +export const webmentionsForPage = ( + webmentions: any[], + page: string +): { + likes: any[]; + reposts: any[]; + comments: any[]; +} => { + const url = new URL( + page.replace('.html', ''), + `https://${DOMAIN}/` + ).toString(); + + const allowedTypes = { + likes: ['like-of'], + reposts: ['repost-of'], + comments: ['mention-of', 'in-reply-to'], + }; + + const clean = (entry: any) => { + if (entry.content) { + if (entry.content.text.length > 280) { + entry.content.value = `${entry.content.text.substr(0, 280)}…`; + } else { + entry.content.value = entry.content.text; + } + } + return entry; + }; + + const cleanedWebmentions = webmentions + .filter((mention) => mention['wm-target'] === url) + .sort( + (a, b) => + new Date(b.published).getTime() - new Date(a.published).getTime() + ) + .map(clean); + + const likes = cleanedWebmentions + .filter((mention) => allowedTypes.likes.includes(mention['wm-property'])) + .filter((like) => like.author) + .map((like) => like.author); + + const reposts = cleanedWebmentions + .filter((mention) => allowedTypes.reposts.includes(mention['wm-property'])) + .filter((repost) => repost.author) + .map((repost) => repost.author); + + const comments = cleanedWebmentions + .filter((mention) => allowedTypes.comments.includes(mention['wm-property'])) + .filter((comment) => { + const { author, published, content } = comment; + return author && author.name && published && content; + }); + + return { + likes: likes ?? [], + reposts: reposts ?? [], + comments: comments ?? [], + }; +}; diff --git a/scripts/humble.ts b/scripts/humble.ts index 37385d3..71c57d1 100755 --- a/scripts/humble.ts +++ b/scripts/humble.ts @@ -47,8 +47,8 @@ const allTaskList = [ { title: 'Build Repeatable Pages', task: () => - execa(`mkdir`, [`-p`, `${outDir}/thoughts`]).then(() => - generateThoughts() + execa(`mkdir`, [`-p`, `${outDir}/thoughts`]).then( + async () => await generateThoughts() ), }, ]; diff --git a/thoughts/template.html b/thoughts/template.html index 9fc939a..03a9232 100644 --- a/thoughts/template.html +++ b/thoughts/template.html @@ -16,6 +16,92 @@

{title}

+ +
+ + + + + +
+

{REPOSTS} reposts

+ +
+ + + +
+

{COMMENTS} comments

+ +
+ +
+ +

Thanks for reading! Fancy sharing...?

More thoughts