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}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Thanks for reading! Fancy sharing...?
More thoughts
{COMMENTS} comments
+{comment}
+ View +