Worked on adding timeline posts. Currently the build crashes due to memory though

This commit is contained in:
thepaperpilot 2024-09-13 05:34:34 -05:00
parent 1d40312517
commit 36dbc3c993
96 changed files with 4249 additions and 285 deletions

17
.gitignore vendored
View file

@ -6,4 +6,19 @@ site/public/changelog/
site/changelog/index.md
site/guide-to-incrementals/
site/public/licenses.txt
garden-output/
site/public/posts/
site/tag/
site/tags/
site/timeline/
site/type/
site/types/
garden-output/
reddit-export/
youtube-export/
.env
cache/
site/recent-posts.data.ts
youtube_credentials.json
youtube_token.json
forbidden.txt
reddit_aliases.json

2
Garden

@ -1 +1 @@
Subproject commit 3b02ad93eebf7ee0e710ad7f63ca9b610bbf10da
Subproject commit 04a4f6681acdc1279d09f741447eb9de8c032b00

53
add_like.js Normal file
View file

@ -0,0 +1,53 @@
const fs = require("fs");
const { getLinkPreview } = require("link-preview-js");
const { getAvatar, preparePost, encodeString, processLinkPreview, getArchivePreview, getActionDescription } = require("./utils");
(async () => {
if (!fs.existsSync("./manual-posts")) {
fs.mkdirSync("./manual-posts");
}
const tags = process.argv.slice(2).filter(tag => !tag.startsWith("-") && !tag.startsWith("http"));
await Promise.all(process.argv.slice(2).map(async url => {
if (url.startsWith("-") || !url.startsWith("http")) {
return;
}
let timestamp = new Date().getTime();
while (fs.existsSync("./manual-posts/" + timestamp)) {
timestamp++;
continue;
}
fs.mkdirSync("./manual-posts/" + timestamp);
console.log("Generating preview for", url);
const linkPreview = await getLinkPreview(url, { followRedirects: true }).catch(async () => {
console.log(`Failed to retrieve preview for ${url}. Trying wayback machine...`);
return await getArchivePreview(url, timestamp);
});
const content = await processLinkPreview(linkPreview);
const fd = fs.openSync("./manual-posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: repost
title: ${encodeString(linkPreview.title, 2)}
published: ${timestamp}
next: false
prev: false
---
<div class="post">
${getActionDescription({ timestamp, action: "🔁", verb: "shared" })}
<div class="content-container">
${await getAvatar({
timestamp,
tags,
action: '🔁'
})}
<div class="content e-content h-cite u-repost-of">${content}</div>
</div>
</div>
`));
fs.closeSync(fd);
console.log(`Created post for url ${url}: /manual-posts/${timestamp}`);
}));
})();

View file

@ -6,18 +6,7 @@ const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
const { Feed } = require('feed');
function walk(dir, cb) {
const list = fs.readdirSync(dir);
return Promise.all(list.map(file => {
const resolvedFile = path.resolve(dir, file);
const stat = fs.statSync(resolvedFile);
if (stat.isDirectory()) {
return walk(resolvedFile, cb);
} else {
return new Promise((resolve) => cb(dir, resolvedFile, resolve));
}
}));
}
const { walk } = require("./utils");
function toSlug(string) {
return string.toLowerCase().replaceAll(' ', '-');
@ -52,7 +41,7 @@ function moveImportStatementUp(filePath, times = 1) {
for (const match of data.matchAll(/(.*)\n\s*id:: (.*)/gm)) {
const text = match[1];
const id = match[2];
const link = `/garden/${slug}/index.md#${id}`;
const link = `/garden/${slug}#${id}`;
blockLinks[id] = link;
blockRefs[id] = `[${text}](${link})`;
}
@ -83,7 +72,7 @@ function moveImportStatementUp(filePath, times = 1) {
const name = path.basename(file, ".md").replaceAll('___', '/');
const slug = toSlug(name).replaceAll(/%3F/gi, '').replaceAll('\'', '-');
const link = `/garden/${slug}/index.md`;
const link = `/garden/${slug}`;
pageLinks[name.replaceAll(/%3F/gi, '?')] = link;
for (const match of data.matchAll(/alias:: (.*)/g)) {
@ -140,7 +129,7 @@ function moveImportStatementUp(filePath, times = 1) {
// Replace internal links
data = data.replaceAll(
/]\(\/logseq-pages\/([^\)]*)\)/g,
'](/garden/$1/index.md)');
'](/garden/$1)');
// Replace block links
data = data.replaceAll(
/\(\((.*)\)\)/g,

View file

@ -0,0 +1,44 @@
---
kind: repost
title: ":/ on Instagram: \":/\""
published: 1721393887885
next: false
prev: false
---
<div class="post">
<div class="action-description">
<span class="action">🔁</span>
<a class="p-name u-url h-card" href="/about">The Paper Pilot</a>
<a class="u-url" href="/posts/1721393887885">shared</a>
<span>this post on <time class="dt-published" datetime="7/19/2024, 7:58:07 AM" title="7/19/2024, 7:58:07 AM">
7/19/2024
</time>:</span>
</div>
<div class="content-container">
<div class="avatar p-author h-card">
<a class="u-url " href="https://www.thepaperpilot.org/about/">
<div class="photo">
<img class="u-photo" src="https://www.thepaperpilot.org/me.jpg" />
<div class="action">🔁</div>
</div>
<div class="p-name">The Paper Pilot</div>
</a>
<time class="dt-published" datetime="7/19/2024, 7:58:07 AM" title="7/19/2024, 7:58:07 AM">
7/19/2024
</time>
<ul class="tags">
<li>videos</li>
</ul>
</div>
<div class="content e-content h-cite u-repost-of">
<div class="img-container">
<img src="/media/-1426872413.png" />
<div class="description">
<a class="u-url" href="https://www.instagram.com/reel/C7n1xaPu40g/?igsh=cjlrcXpqenRlYWc1"><h2><img src="/media/-2097278349.png" />:/ on Instagram: ":/"</h2></a>
<div>926K likes, 2,198 comments - unhappiest</div>
<pre>https://www.instagram.com/reel/C7n1xaPu40g/?igsh=cjlrcXpqenRlYWc1</pre>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,44 @@
---
kind: repost
title: "Kaeden ♡ on Instagram: \"If this is real thatd be incredible\""
published: 1721393926645
next: false
prev: false
---
<div class="post">
<div class="action-description">
<span class="action">🔁</span>
<a class="p-name u-url h-card" href="/about">The Paper Pilot</a>
<a class="u-url" href="/posts/1721393926645">shared</a>
<span>this post on <time class="dt-published" datetime="7/19/2024, 7:58:46 AM" title="7/19/2024, 7:58:46 AM">
7/19/2024
</time>:</span>
</div>
<div class="content-container">
<div class="avatar p-author h-card">
<a class="u-url " href="https://www.thepaperpilot.org/about/">
<div class="photo">
<img class="u-photo" src="https://www.thepaperpilot.org/me.jpg" />
<div class="action">🔁</div>
</div>
<div class="p-name">The Paper Pilot</div>
</a>
<time class="dt-published" datetime="7/19/2024, 7:58:46 AM" title="7/19/2024, 7:58:46 AM">
7/19/2024
</time>
<ul class="tags">
<li>videos</li>
</ul>
</div>
<div class="content e-content h-cite u-repost-of">
<div class="img-container">
<img src="/media/1393559065.png" />
<div class="description">
<a class="u-url" href="https://www.instagram.com/p/C9EwpRyBcma/?igsh=eG4ydGM2NWs1b3pt"><h2><img src="/media/-2097278349.png" />Kaeden ♡ on Instagram: "If this is real thatd be incredible"</h2></a>
<div>131K likes, 193 comments - makkaxchin</div>
<pre>https://www.instagram.com/p/C9EwpRyBcma/?igsh=eG4ydGM2NWs1b3pt</pre>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,56 @@
---
kind: repost
title: "VFTB on Instagram: \"Sure thing! Heres the statistics about the Honda Civic Type R:
The Honda Civic Type R is a remarkable performance car renowned for its agile handling and sporty design. Equipped with a robust 2.0-liter turbocharged engine, it delivers over 300 horsepower. 🔧
Acceleration from 0 to 100 km/h takes approximately 5.7 seconds, with an impressive top speed exceeding 270 km/h. 🥇
Incorporating advanced aerodynamics and state-of-the-art stability systems, the Civic Type R ensures exceptional handling and control, especially during spirited driving.💨
Originally priced around $35,000, the Honda Civic Type R offers outstanding value for its performance capabilities.💰
With its limited availability and high demand, the Civic Type R is highly coveted by enthusiasts and collectors alike.🌎
#viral #foryou #reels #meme #viral #goingviral\""
published: 1721393946619
next: false
prev: false
---
<div class="post">
<div class="action-description">
<span class="action">🔁</span>
<a class="p-name u-url h-card" href="/about">The Paper Pilot</a>
<a class="u-url" href="/posts/1721393946619">shared</a>
<span>this post on <time class="dt-published" datetime="7/19/2024, 7:59:06 AM" title="7/19/2024, 7:59:06 AM">
7/19/2024
</time>:</span>
</div>
<div class="content-container">
<div class="avatar p-author h-card">
<a class="u-url " href="https://www.thepaperpilot.org/about/">
<div class="photo">
<img class="u-photo" src="https://www.thepaperpilot.org/me.jpg" />
<div class="action">🔁</div>
</div>
<div class="p-name">The Paper Pilot</div>
</a>
<time class="dt-published" datetime="7/19/2024, 7:59:06 AM" title="7/19/2024, 7:59:06 AM">
7/19/2024
</time>
<ul class="tags">
<li>videos</li>
</ul>
</div>
<div class="content e-content h-cite u-repost-of">
<div class="img-container">
<img src="/media/2135914855.png" />
<div class="description">
<a class="u-url" href="https://www.instagram.com/reel/C8xIjJ_oQIx/?igsh=NDNya2pjdXoxYnZj"><h2><img src="/media/-2097278349.png" />VFTB on Instagram: "Sure thing! Heres the statistics about the Honda Civic Type R:
The Honda Civic Type R is a remarkable performance car renowned for its agile handling and sporty design. Equipped with a robust 2.0-liter turbocharged engine, it delivers over 300 horsepower. 🔧
Acceleration from 0 to 100 km/h takes approximately 5.7 seconds, with an impressive top speed exceeding 270 km/h. 🥇
Incorporating advanced aerodynamics and state-of-the-art stability systems, the Civic Type R ensures exceptional handling and control, especially during spirited driving.💨
Originally priced around $35,000, the Honda Civic Type R offers outstanding value for its performance capabilities.💰
With its limited availability and high demand, the Civic Type R is highly coveted by enthusiasts and collectors alike.🌎
#viral #foryou #reels #meme #viral #goingviral"</h2></a>
<div>2M likes, 5,678 comments - videofortheboys</div>
<pre>https://www.instagram.com/reel/C8xIjJ_oQIx/?igsh=NDNya2pjdXoxYnZj</pre>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,44 @@
---
kind: repost
title: "goat.rest"
published: 1721579861816
next: false
prev: false
---
<div class="post">
<div class="action-description">
<span class="action">🔁</span>
<a class="p-name u-url h-card" href="/about">The Paper Pilot</a>
<a class="u-url" href="/posts/1721579861816">shared</a>
<span>this post on <time class="dt-published" datetime="7/21/2024, 11:37:41 AM" title="7/21/2024, 11:37:41 AM">
7/21/2024
</time>:</span>
</div>
<div class="content-container">
<div class="avatar p-author h-card">
<a class="u-url " href="https://www.thepaperpilot.org/about/">
<div class="photo">
<img class="u-photo" src="https://www.thepaperpilot.org/me.jpg" />
<div class="action">🔁</div>
</div>
<div class="p-name">The Paper Pilot</div>
</a>
<time class="dt-published" datetime="7/21/2024, 11:37:41 AM" title="7/21/2024, 11:37:41 AM">
7/21/2024
</time>
<ul class="tags">
<li>games</li><li>philosophy</li>
</ul>
</div>
<div class="content e-content h-cite u-repost-of">
<div class="img-container">
<img src="/media/1229267454.png" />
<div class="description">
<a class="u-url" href="https://goat.rest"><h2>goat.rest</h2></a>
<div>the night sky is so pretty, isn't it?</div>
<pre>https://goat.rest</pre>
</div>
</div>
</div>
</div>
</div>

View file

@ -4,11 +4,18 @@
"description": "The Paper Pilot Portfolio",
"scripts": {
"serve": "vitepress serve site",
"dev": "set NODE_OPTIONS=\"--max-old-space-size=32768\" && vitepress dev site",
"build": "set NODE_OPTIONS=\"--max-old-space-size=32768\" && rm -rf ./garden-output && yarn run logseq-export && node build_garden.js && vitepress build site",
"dev": "set NODE_OPTIONS=\"--max-old-space-size=32768\" && set DEBUG=\"vitepress:*\" && vitepress dev site",
"build": "set NODE_OPTIONS=\"--max-old-space-size=32768\" && rm -rf ./garden-output && yarn run logseq-export && node build_garden.js && node process_posts.js && vitepress build site",
"build:lite": "set NODE_OPTIONS=\"--max-old-space-size=32768\" && vitepress build site",
"process-youtube": "node process_youtube.js",
"process-reddit": "node process_reddit.js",
"process-itch": "node process_itch.js",
"process-posts": "node process_posts.js",
"logseq-export": "run-script-os",
"logseq-export:win32": "logseq-export/logseq-export.exe --logseqFolder ./Garden --outputFolder ./garden-output",
"logseq-export:linux": "chmod +x logseq-export/logseq-export && logseq-export/logseq-export --logseqFolder ./Garden --outputFolder ./garden-output"
"logseq-export:linux": "chmod +x logseq-export/logseq-export && logseq-export/logseq-export --logseqFolder ./Garden --outputFolder ./garden-output",
"add-like": "node add_like.js",
"dev:all": "rm -r ./site/posts && node process_reddit.js && node process_youtube.js && node process_itch.js && node process_posts.js && yarn run dev"
},
"repository": "git+https://github.com/thepaperpilot/thepaperpilot.github.io.git",
"author": "thepaperpilot",
@ -18,12 +25,22 @@
"@nolebase/vitepress-plugin-highlight-targeted-heading": "^2.1.1",
"@tresjs/core": "^4.0.2",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"csv-parse": "^5.5.6",
"dotenv": "^16.4.5",
"feed": "^4.2.2",
"google-auth-library": "^9.11.0",
"googleapis": "^140.0.1",
"link-preview-js": "^3.0.5",
"run-script-os": "^1.1.6",
"sanitize-html": "^2.13.0",
"snoowrap": "^1.23.0",
"three": "^0.165.0",
"user-agents": "^1.1.261",
"vitepress": "^1.2.2",
"vue": "^3.3.4",
"word-counting": "^1.1.4"
"word-counting": "^1.1.4",
"yaml": "^2.4.5",
"ytmusic-api": "^5.2.2"
},
"private": true,
"devDependencies": {

118
process_itch.js Normal file
View file

@ -0,0 +1,118 @@
const fs = require('fs');
const { getAvatar, preparePost, encodeString, getActionDescription, getMediaUrl } = require("./utils");
require('dotenv').config();
const KEY = process.env.ITCH_API_KEY;
const ITCH_SVG = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Itch.io</title><path d="M3.13 1.338C2.08 1.96.02 4.328 0 4.95v1.03c0 1.303 1.22 2.45 2.325 2.45 1.33 0 2.436-1.102 2.436-2.41 0 1.308 1.07 2.41 2.4 2.41 1.328 0 2.362-1.102 2.362-2.41 0 1.308 1.137 2.41 2.466 2.41h.024c1.33 0 2.466-1.102 2.466-2.41 0 1.308 1.034 2.41 2.363 2.41 1.33 0 2.4-1.102 2.4-2.41 0 1.308 1.106 2.41 2.435 2.41C22.78 8.43 24 7.282 24 5.98V4.95c-.02-.62-2.082-2.99-3.13-3.612-3.253-.114-5.508-.134-8.87-.133-3.362 0-7.945.053-8.87.133zm6.376 6.477a2.74 2.74 0 0 1-.468.602c-.5.49-1.19.795-1.947.795a2.786 2.786 0 0 1-1.95-.795c-.182-.178-.32-.37-.446-.59-.127.222-.303.412-.486.59a2.788 2.788 0 0 1-1.95.795c-.092 0-.187-.025-.264-.052-.107 1.113-.152 2.176-.168 2.95v.005l-.006 1.167c.02 2.334-.23 7.564 1.03 8.85 1.952.454 5.545.662 9.15.663 3.605 0 7.198-.21 9.15-.664 1.26-1.284 1.01-6.514 1.03-8.848l-.006-1.167v-.004c-.016-.775-.06-1.838-.168-2.95-.077.026-.172.052-.263.052a2.788 2.788 0 0 1-1.95-.795c-.184-.178-.36-.368-.486-.59-.127.22-.265.412-.447.59a2.786 2.786 0 0 1-1.95.794c-.76 0-1.446-.303-1.948-.793a2.74 2.74 0 0 1-.468-.602 2.738 2.738 0 0 1-.463.602 2.787 2.787 0 0 1-1.95.794h-.16a2.787 2.787 0 0 1-1.95-.793 2.738 2.738 0 0 1-.464-.602zm-2.004 2.59v.002c.795.002 1.5 0 2.373.953.687-.072 1.406-.108 2.125-.107.72 0 1.438.035 2.125.107.873-.953 1.578-.95 2.372-.953.376 0 1.876 0 2.92 2.934l1.123 4.028c.832 2.995-.266 3.068-1.636 3.07-2.03-.075-3.156-1.55-3.156-3.025-1.124.184-2.436.276-3.748.277-1.312 0-2.624-.093-3.748-.277 0 1.475-1.125 2.95-3.156 3.026-1.37-.004-2.468-.077-1.636-3.072l1.122-4.027c1.045-2.934 2.545-2.934 2.92-2.934zM12 12.714c-.002.002-2.14 1.964-2.523 2.662l1.4-.056v1.22c0 .056.56.033 1.123.007.562.026 1.124.05 1.124-.008v-1.22l1.4.055C14.138 14.677 12 12.713 12 12.713z"/></svg>';
const TAGS_MAP = {
40801: "lifeisstrange",
1826237: "incremental",
969362: "incremental",
938153: "incremental",
2074815: "incremental",
730177: "incremental",
62179: "incremental",
814897: "incremental",
993566: "incremental"
};
(async () => {
if (!fs.existsSync("./site/posts")) {
fs.mkdirSync("./site/posts");
}
const url = `https://itch.io/api/1/${KEY}/my-games`;
const resp = await fetch(url).then(r => r.text());
let response;
try {
response = JSON.parse(resp);
} catch (err) {
console.error("Unexpected response from itch:", url, resp, err);
process.exit(0);
}
if (!response.games?.length) {
console.error("No games received from itch:", url, resp);
process.exit(0);
}
console.log(`Checking ${response.games.length} games`);
for await (const game of response.games) {
if (!game.published) continue;
const timestamp = new Date(game.published_at).getTime();
const tag = TAGS_MAP[game.id] ?? "gaming";
let embed = '';
if (game.embed) {
const uploadsResponse = await fetch(`https://itch.io/api/1/${KEY}/game/${game.id}/uploads`).then(r => r.text());
embed = await getEmbed(game, uploadsResponse);
}
fs.mkdirSync("./site/posts/" + timestamp, { recursive: true });
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: article
title: ${encodeString(game.title)}
published: ${timestamp}
next: false
prev: false
tags: [${encodeString(tag, 2)}]
---
<div class="post">
${getActionDescription({ timestamp, action: "🎮", verb: "released" })}
<div class="content-container">
${await getAvatar({
timestamp,
tags: [tag],
syndications: [{ type: ITCH_SVG, url: game.url }],
action: '🎮'
})}
<div class="content e-content">
<div class="img-container">
<img src="${await getMediaUrl(game.cover_url)}" />
<div class="description">
<h2>${game.title}</h2>
<div>${game.short_text}</div>
</div>
</div>
</div>
</div>
${embed}
</div>
`));
fs.closeSync(fd);
console.log(`Created post for "${game.title}": /posts/${timestamp}/index.md`);
}
})();
async function getEmbed(game, resp) {
// Skip games that don't fit on the paper
// TODO hsve play button that full screens the game
if ([993566, 40801, 184114, 162503, 129905, 87387, 79940, 62179, 60029, 50055].includes(game.id)) {
return "";
}
let response;
try {
response = JSON.parse(resp);
} catch (err) {
console.error("Unexpected response from itch:", game.title, resp, err);
return "";
}
if (!response.uploads?.length ?? 0 < 1) {
console.error("No uploads received for game:", game.title, resp);
return "";
}
const upload = response.uploads?.filter(up => up.type === "html")[0];
if (!upload) {
console.error("No html uploads received for game:", game.title, resp);
return "";
}
const dist = upload.filename === "dist.zip" ? '/dist' : '';
const src = `https://html-classic.itch.zone/html/${upload.id}${dist}/index.html`;
const height = Math.ceil(game.embed.height / 30 + 1) * 30;
const width = height * game.embed.width / game.embed.height;
return `<iframe width="${width}" height="${height}" src="${src}" frameborder="0" scrolling="no" allowfullscreen></iframe>`;
}

246
process_posts.js Normal file
View file

@ -0,0 +1,246 @@
const fs = require("fs");
const path = require("path");
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
const { Feed } = require('feed');
const YAML = require('yaml');
const { walk, preparePost } = require("./utils");
const numFormat = Intl.NumberFormat("en-US");
(async () => {
// We can't load all posts into memory, so focus on organizing them first (just storing timestamps)
const mostRecentPosts = [];
const postsByDate = {};
const postsByTag = {};
const postsByType = {
article: [],
repost: [],
like: [],
favorite: [],
reply: []
};
const feed = new Feed({
title: "The Paper Pilot's Posts",
description: "A feed of my activity across the internet - posts, comments, replies, and reactions!",
id: "https://www.thepaperpilot.org/posts/",
link: "https://www.thepaperpilot.org/posts/",
language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
// image: "http://example.com/image.png",
// favicon: "http://example.com/favicon.ico",
copyright: `All rights reserved ${new Date().getFullYear()}, The Paper Pilot`,
// updated: new Date(2013, 6, 14), // optional, default = today
// generator: "awesome", // optional, default = 'Feed for Node.js'
feedLinks: {
rss: "https://www.thepaperpilot.org/posts/rss",
json: "https://www.thepaperpilot.org/posts/json",
atom: "https://www.thepaperpilot.org/posts/atom"
},
author: {
name: "The Paper Pilot",
email: "thepaperpilot@incremental.social",
link: "https://www.thepaperpilot.org/"
}
});
fs.readdirSync("./manual-posts").forEach(filename => {
if (!fs.existsSync("./site/posts/" + filename)) {
fs.mkdirSync("./site/posts/" + filename);
}
fs.copyFileSync("./manual-posts/" + filename + "/index.md", "./site/posts/" + filename + "/index.md")
});
let processed = 0;
const totalPosts = fs.readdirSync("./site/posts").length;
process.stdout.write(`Processed 0/${totalPosts} posts...`);
await walk("./site/posts", (dir, file, resolve) => {
const filePath = path.resolve(dir, file);
const data = fs.readFileSync(filePath).toString();
try {
let frontmatter = data.match(/---\n([\S\s]*?\n)---/m)[1];
// frontmatter = frontmatter.replaceAll(/\\([a-zA-Z<>\.])/g, '\\\\$1');
const { kind, published, tags, title } = YAML.parse(frontmatter);
const timestamp = parseInt(published);
mostRecentPosts.push(timestamp);
mostRecentPosts.sort((a, b) => b - a);
mostRecentPosts.splice(9, 10);
insertByDate(postsByDate, timestamp);
insertByDate(postsByType[kind], timestamp);
tags?.forEach(tag => {
postsByTag[tag] = postsByTag[tag] ?? [];
insertByDate(postsByTag[tag], timestamp);
})
const content = data.match(/---\n[\S\s]*?\n---\n([\S\s]*)/m)[1];
feed.addItem({
title: title ?? kind,
id: `https://www.thepaperpilot.org/posts/${timestamp}`,
link: `https://www.thepaperpilot.org/posts/${timestamp}`,
content,
date: new Date(published),
category: { name: kind }
});
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(`Processed ${++processed}/${totalPosts} posts...`);
resolve();
} catch (e) {
console.log("\nFailed to process post", filePath, e);
}
});
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
process.stdout.write(`Processed ${totalPosts}/${totalPosts} posts... Processed all posts!\n`);
console.log("Writing recent posts", mostRecentPosts);
fd = fs.openSync("site/recent-posts.data.ts", "w+");
fs.writeSync(fd, `
export default {
async load(): Promise<string[]> {
return ${JSON.stringify(mostRecentPosts.map(getContentFromTimestamp))}
}
};`);
fs.closeSync(fd);
console.log("Writing index pages organized by date");
await writePostsByDate(postsByDate, "timeline");
console.log("Writing index pages by tag");
if (fs.existsSync("./site/tag")) {
fs.rmSync("./site/tag", { recursive: true });
}
let tagIndex = `---
kind: index
title: Tags
prev: false
next: false
---
# Tags\n\n`;
for (const tag of Object.keys(postsByTag).toSorted()) {
console.log("-- " + tag);
const totalPosts = await writePostsByDate(postsByTag[tag], "tag/" + tag, `Posts tagged "${tag}"`);
tagIndex += `## ${tag}
There are ${numFormat.format(totalPosts)} post${totalPosts === 1 ? '' : 's'} tagged "[${tag}](/tag/${tag}/1)".\n\n`;
}
if (!fs.existsSync("./site/tags")) {
fs.mkdirSync("./site/tags");
}
fd = fs.openSync("./site/tags/index.md", "w+");
fs.writeSync(fd, preparePost(tagIndex));
fs.closeSync(fd);
console.log("Writing index pages by type");
let kindIndex = `---
kind: index
title: Types
prev: false
next: false
---
# Types\n\n`;
await Promise.all(Object.keys(postsByType).map(async kind => {
console.log("-- " + kind);
const title = `${kind.charAt(0).toUpperCase()}${kind === "reply" ? "eplie" : kind.slice(1)}s`;
const totalPosts = await writePostsByDate(postsByType[kind], "type/" + kind, title);
kindIndex += `## ${title}
There are ${numFormat.format(totalPosts)} [${title.toLowerCase()}](/type/${kind}/1) post${totalPosts === 1 ? '' : 's'}.\n\n`;
}));
if (!fs.existsSync("./site/types")) {
fs.mkdirSync("./site/types");
}
fd = fs.openSync("./site/types/index.md", "w+");
fs.writeSync(fd, preparePost(kindIndex));
fs.closeSync(fd);
if (!fs.existsSync("./site/public/posts")) {
fs.mkdirSync("./site/public/posts");
}
console.log("Writing rss...");
fd = fs.openSync("site/public/posts/rss", "w+");
fs.writeSync(fd, feed.rss2());
fs.closeSync(fd);
console.log("Writing atom...");
fd = fs.openSync("site/public/posts/atom", "w+");
fs.writeSync(fd, feed.atom1());
fs.closeSync(fd);
console.log("Writing json...");
fd = fs.openSync("site/public/posts/json", "w+");
fs.writeSync(fd, feed.json1());
fs.closeSync(fd);
})();
function insertByDate(posts, timestamp) {
const d = new Date(timestamp);
const year = d.getFullYear();
const month = d.getMonth();
posts[year] = posts[year] ?? {};
posts[year][month] = posts[year][month] ?? [];
posts[year][month].push(timestamp);
}
function getContentFromTimestamp(timestamp) {
const filePath = `./site/posts/${timestamp}/index.md`;
const data = fs.readFileSync(filePath).toString();
return data.match(/---\n[\S\s]*?\n---\n([\S\s]*)/m)[1]
.replace(/<iframe.*<\/iframe>/, '')
.replace(/<div class="content-container u-comment h-cite">.*/s, '</div>');
}
async function writePostsByDate(posts, baseUrl, title = "Posts") {
await exec(`rm -rf ./site/${baseUrl}`);
const allPosts = Object.keys(posts).map(y => parseInt(y)).toSorted().reduce((acc, year) => {
const sortedMonths = Object.keys(posts[year]).map(m => parseInt(m)).toSorted();
const sortedPosts = sortedMonths.reduce((acc, month) =>
[...acc, ...posts[year][month].toSorted()], []);
return [...acc, ...sortedPosts];
}, []).toReversed();
const pages = Math.ceil(allPosts.length / 20);
new Array(pages).fill(0).forEach((_, i) => {
const folderPath = `./site/${baseUrl}/${i + 1}`;
fs.mkdirSync(folderPath, { recursive: true });
fd = fs.openSync(folderPath + "/index.md", "w+");
fs.writeSync(fd, `---
kind: index
title: '${title}'
${adjacentLinks(i + 1, pages, baseUrl)}
---
# ${title}
${allPosts.slice(i * 20, (i + 1) * 20).map(getContentFromTimestamp).join("\n")}
`);
fs.closeSync(fd);
});
return allPosts.length;
}
function adjacentLinks(index, pages, baseUrl) {
let links = '';
if (index === 1) {
links += 'prev: false\n';
} else {
links += `prev:
link: ${baseUrl}/${index - 1}
text: Page ${index - 1}\n`;
}
if (index === pages) {
links += 'next: false';
} else {
links += `next:
link: ${baseUrl}/${index + 1}
text: Page ${index + 1}`;
}
return links;
}

684
process_reddit.js Normal file
View file

@ -0,0 +1,684 @@
const fs = require("fs");
const { parse } = require("csv-parse");
const snoowrap = require('snoowrap');
const { getLinkPreview } = require("link-preview-js");
const { sanitize, getAvatar, preparePost, slugify, encodeString, processLinkPreview, getActionDescription } = require("./utils");
const UserAgent = require("user-agents");
require('dotenv').config();
const REDDIT_SVG = `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Reddit</title><path d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"/></svg>`;
let r;
function setupReddit() {
try {
r = new snoowrap({
userAgent: new UserAgent().toString(),
clientId: process.env.REDDIT_CLIENT,
clientSecret: process.env.REDDIT_SECRET,
username: process.env.REDDIT_USERNAME,
password: process.env.REDDIT_PASSWORD
});
return true;
} catch (error) {
console.error("Failed to setup reddit:", error);
return false;
}
}
setupReddit();
let forbidden_ids = new Set();
let unavailable_urls = new Set();
let forbidden_subreddits = new Set();
let aliases_map = {};
let skippedPosts = 0;
let createdPosts = 0;
(async () => {
if (!fs.existsSync("./site/posts")) {
fs.mkdirSync("./site/posts");
}
if (!fs.existsSync("./cache")) {
fs.mkdirSync("./cache");
}
if (fs.existsSync("./cache/forbidden.json")) {
forbidden_ids = new Set(JSON.parse(fs.readFileSync("./cache/forbidden.json").toString()));
console.log(`Loaded ${forbidden_ids.size} forbidden IDs`);
}
if (fs.existsSync("./cache/unavailable.json")) {
unavailable_urls = new Set(JSON.parse(fs.readFileSync("./cache/unavailable.json").toString()));
console.log(`Loaded ${unavailable_urls.size} unavailable URLs`);
}
if (fs.existsSync("./forbidden.txt")) {
forbidden_subreddits = new Set(fs.readFileSync("./forbidden.txt").toString().split("\n"));
console.log(`Loaded ${forbidden_subreddits.size} forbidden subreddits`);
}
if (fs.existsSync("./reddit_aliases.json")) {
aliases_map = JSON.parse(fs.readFileSync("./reddit_aliases.json").toString());
console.log(`Loaded ${Object.keys(aliases_map).length} subreddit aliases`);
}
// Posts
const posts = fs
.createReadStream("./reddit-export/posts.csv")
.pipe(parse({ columns: true }));
for await (const { id, permalink, date, ip, subreddit, gildings, title, url, body } of posts) {
if (!url) {
// Removed and lost to time ;_;
skippedPosts++;
continue;
}
let timestamp = new Date(date).getTime();
const submission = await getSubmission(id);
if (isError(submission)) {
skippedPosts++;
continue;
}
const replies = submission.comments.map(comment => ({
content: processContent(comment.body_html),
published: comment.created * 1000,
url: `https://reddit.com${comment.permalink}`,
author: {
name: extractUsername(comment.author),
url: `https://reddit.com/u/${extractUsername(comment.author)}`,
photo: getRedditAvatar(comment.author)
}
})) ?? [];
let content;
if (submission.selftext_html) {
content = processContent(submission.selftext_html, title);
} else {
let processedUrl = url;
if (url.startsWith("/r/")) {
processedUrl = "https://www.reddit.com" + url;
}
content = await generateLinkPreview(processedUrl, timestamp, await getSubmissionFallback(submission), title);
}
while (fs.existsSync("./site/posts/" + timestamp)) {
timestamp++;
continue;
}
fs.mkdirSync("./site/posts/" + timestamp);
const tag = slugify(aliases_map[subreddit.toLowerCase()] ?? subreddit);
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: ${body ? 'article' : 'repost'}
title: ${encodeString(title, 2)}
published: ${timestamp}
next: false
prev: false
tags: [${encodeString(tag, 2)}]
---
<div class="post">
${getActionDescription({ timestamp, action: body ? "📝" : "🔁", verb: body ? "wrote" : "shared" })}
<div class="content-container">
${await getAvatar({
timestamp,
tags: [tag],
syndications: [{ type: REDDIT_SVG, url: permalink }],
action: body ? '📝' : '🔁'
})}
<div class="content e-content ${body ? '' : 'h-cite u-repost-of'}">${content}</div>
</div>
${(await Promise.all(replies.filter(c => c.author.name !== '[deleted]').map(async ({ content, published, url, author }) => `
<div class="content-container u-comment h-cite">
${await getAvatar({
...author,
timestamp: published,
syndications: [{ type: REDDIT_SVG, url }]
})}
<div class="content e-content">${content}</div>
</div>`))).join('')}
</div>
`));
fs.closeSync(fd);
createdPosts++;
if (createdPosts % 100 === 0) {
console.log(`Created ${createdPosts} reddit activity posts (currently submitting)`);
}
}
// Comments
const comments = fs
.createReadStream("./reddit-export/comments.csv")
.pipe(parse({ columns: true }));
for await (const { id, permalink, date, ip, subreddit, gildings, link, parent, body } of comments) {
if (body === "[removed]") {
skippedPosts++;
continue;
}
if (forbidden_subreddits.has(subreddit.toLowerCase())) {
continue;
}
const comment = await getComment(id);
if (isError(comment)) {
skippedPosts++;
continue;
}
let timestamp = new Date(date).getTime();
const content = processContent(comment.body_html);
let parentInfo;
if (parent) {
const parentComment = await getComment(parent);
if (isError(parentComment)) {
skippedPosts++;
continue;
}
parentInfo = {
content: processContent(await parentComment.body_html),
published: await parentComment.created * 1000,
url: `https://reddit.com${await parentComment.permalink}`,
author: {
name: extractUsername(await parentComment.author),
url: `https://reddit.com/u/${extractUsername(await parentComment.author)}`,
photo: getRedditAvatar(await parentComment.author)
}
};
} else {
const submission = await getSubmission(await comment.link_id.replace(/t._/, ''));
if (isError(submission)) {
skippedPosts++;
continue;
}
let content;
if (await submission.selftext_html) {
content = processContent(await submission.selftext_html, await submission.title);
} else if (await submission.url) {
content = await generateLinkPreview(await submission.url, timestamp, await getSubmissionFallback(submission), await submission.title);
} else {
console.warn("Unsure how to handle like to this submission", submission);
}
parentInfo = {
content,
published: await submission.created * 1000,
url: `https://reddit.com${await submission.permalink}`,
author: {
name: extractUsername(await submission.author),
url: `https://reddit.com/u/${extractUsername(await submission.author)}`,
photo: getRedditAvatar(await submission.author)
}
};
}
const replies = await Promise.all(await comment.replies.map(async comment => ({
content: processContent(await comment.body_html),
published: await comment.created * 1000,
url: `https://reddit.com${await comment.permalink}`,
author: {
name: extractUsername(await comment.author),
url: `https://reddit.com/u/${extractUsername(await comment.author)}`,
photo: getRedditAvatar(await comment.author)
}
})) ?? []);
if (!fs.existsSync("./site/posts/" + timestamp)) {
fs.mkdirSync("./site/posts/" + timestamp);
}
const tag = slugify(aliases_map[subreddit.toLowerCase()] ?? subreddit);
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: reply
title: I wrote a comment on r/${subreddit}
published: ${timestamp}
next: false
prev: false
tags: [${encodeString(tag, 2)}]
---
<div class="post">
${getActionDescription({ timestamp, action: "💬", verb: "replied to" })}
<div class="content-container u-in-reply-to">
${await getAvatar({
...parentInfo.author,
syndications: [{ type: REDDIT_SVG, url: parentInfo.url }],
timestamp: parentInfo.published,
tags: [tag]
})}
<div class="content e-content">${parentInfo.content}</div>
</div>
<div class="content-container">
${await getAvatar({
timestamp,
syndications: [{ type: REDDIT_SVG, url: permalink }]
})}
<div class="content e-content">${content}</div>
</div>
${(await Promise.all(replies.filter(c => c.author.name !== '[deleted]').map(async ({ content, published, url, author }) => `
<div class="content-container u-comment h-cite">
${await getAvatar({
...author,
timestamp: published,
syndications: [{ type: REDDIT_SVG, url }]
})}
<div class="content e-content">${content}</div>
</div>`))).join('')}
</div>
`));
fs.closeSync(fd);
createdPosts++;
if (createdPosts % 100 === 0) {
console.log(`Created ${createdPosts} reddit activity posts (currently replying)`);
}
}
// Votes
const post_votes = fs
.createReadStream("./reddit-export/post_votes.csv")
.pipe(parse({ columns: true }))
for await (const { id, permalink, direction } of post_votes) {
if (direction !== "up") {
continue;
}
await like_post(id, permalink);
}
const comment_votes = fs
.createReadStream("./reddit-export/comment_votes.csv")
.pipe(parse({ columns: true }));
for await (const { id, permalink, direction } of comment_votes) {
if (direction !== "up") {
continue;
}
await like_comment(id, permalink);
}
// Saved
const saved_posts = fs
.createReadStream("./reddit-export/saved_posts.csv")
.pipe(parse({ columns: true }));
for await (const { id, permalink } of saved_posts) {
await like_post(id, permalink, "favorite");
}
const saved_comments = fs
.createReadStream("./reddit-export/saved_comments.csv")
.pipe(parse({ columns: true }));
for await (const { id, permalink } of saved_comments) {
await like_comment(id, permalink, "favorite");
}
const fd = fs.openSync("./cache/forbidden.json", "w+");
fs.writeSync(fd, JSON.stringify(Array.from(forbidden_ids.keys())));
fs.closeSync(fd);
console.log(`Created ${createdPosts} posts. ${skippedPosts} posts skipped due to being removed.`);
})();
async function generateLinkPreview(url, timestamp, fallback, title) {
if (url.match(/^\/?[ru]\//)) {
url = "https://www.reddit.com/" + url;
}
// Dorkly's CDN now serves viruses :(
if (url.includes("dorkly.com")) {
return undefined;
}
const linkPreviewCachePath = `./cache/${cyrb53(url)}.json`;
let linkPreview;
if (fs.existsSync(linkPreviewCachePath)) {
linkPreview = JSON.parse(fs.readFileSync(linkPreviewCachePath).toString());
} else if (!unavailable_urls.has(url)) {
// getLinkPreview makes the process exit without throwing an error on this url
if (url === "http://www.phrack.org/issues.html?issue=7&id=3&mode=txt") {
linkPreview = await getArchivePreview(url, timestamp, fallback);
} else {
linkPreview = await getLinkPreview(url, { followRedirects: true }).catch(async () => {
console.log(`Failed to retrieve preview for ${url}. Trying wayback machine...`);
return await getArchivePreview(url, timestamp, fallback);
});
}
if (linkPreview) {
const fd = fs.openSync(linkPreviewCachePath, "w+");
fs.writeSync(fd, JSON.stringify(linkPreview));
fs.closeSync(fd);
} else {
console.log("Not available on wayback machine. No preview available.");
markLinkUnavailable(url);
}
}
if (linkPreview?.mediaType === "website" && (linkPreview.images ?? []).length === 0 && fallback) {
linkPreview.images = [fallback];
}
return linkPreview ? processLinkPreview(linkPreview, title) : undefined;
}
async function like_post(id, permalink, action = "like") {
const submission = await getSubmission(id);
if (isError(submission)) {
skippedPosts++;
return;
}
let content;
if (await submission.selftext_html) {
content = processContent(await submission.selftext_html, await submission.title);
} else if (await submission.url) {
content = await generateLinkPreview(await submission.url, await submission.created,
await getSubmissionFallback(submission), await submission.title);
} else {
console.warn("Unsure how to handle like to this submission", submission);
}
if (!content) {
skippedPosts++;
return;
}
let timestamp = await submission.created_utc * 1000;
while (fs.existsSync("./site/posts/" + timestamp)) {
timestamp++;
}
const author = extractUsername(await submission.author);
const subreddit = await extractSubreddit(submission.subreddit);
if (!fs.existsSync("./site/posts/" + timestamp)) {
fs.mkdirSync("./site/posts/" + timestamp);
}
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: ${action}
title: ${encodeString(submission.title, 2)}
published: ${timestamp}
next: false
prev: false
tags: [${encodeString(subreddit, 2)}]
---
<div class="post">
${getActionDescription({ timestamp, action: action === "favorite" ? "⭐" : "❤️", verb: action === "favorite" ? "favorited" : "liked" })}
<div class="content-container h-cite u-like-of">
${await getAvatar({
photo: getRedditAvatar(author),
name: author,
url: `https://www.reddit.com/u/${author}`,
syndications: [{ type: REDDIT_SVG, url: permalink }],
timestamp,
tags: [subreddit]
})}
<div class="content e-content">${content}</div>
</div>
</div>
`));
fs.closeSync(fd);
createdPosts++;
if (createdPosts % 100 === 0) {
console.log(`Created ${createdPosts} reddit activity posts (currently ${action.slice(0, -1)}ing submissions)`);
}
}
async function like_comment(id, permalink, action = "like") {
const comment = await getComment(id);
if (isError(comment)) {
skippedPosts++;
return;
}
const submission = await getSubmission(await comment.link_id.replace(/t._/, ''));
if (isError(submission)) {
skippedPosts++;
return;
}
const content = processContent(await comment.body_html);
let timestamp = await comment.created_utc * 1000;
while (fs.existsSync("./site/posts/" + timestamp)) {
timestamp++;
}
const author = extractUsername(await comment.author);
const subreddit = await extractSubreddit(submission.subreddit);
if (!fs.existsSync("./site/posts/" + timestamp)) {
fs.mkdirSync("./site/posts/" + timestamp);
}
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: ${action}
title: I liked a comment on r/${subreddit}
published: ${timestamp}
next: false
prev: false
tags: [${encodeString(subreddit, 2)}]
---
<div class="post">
${getActionDescription({ timestamp, action: action === "favorite" ? "⭐" : "❤️", verb: action === "favorite" ? "favorited" : "liked" })}
<div class="content-container h-cite u-like-of">
${await getAvatar({
photo: getRedditAvatar(author),
name: author,
url: `https://www.reddit.com/u/${author}`,
syndications: [{ type: REDDIT_SVG, url: permalink }],
timestamp,
tags: [subreddit]
})}
<div class="content e-content">${content}</div>
</div>
</div>
`));
fs.closeSync(fd);
createdPosts++;
if (createdPosts % 100 === 0) {
console.log(`Created ${createdPosts} reddit activity posts (currently ${action.slice(0, -1)}ing comments)`);
}
}
async function getSubmission(id) {
if (forbidden_ids.has(id)) {
return { error: 403 }
}
const submissionCachePath = `./cache/${id}.json`;
if (fs.existsSync(submissionCachePath)) {
const submission = JSON.parse(fs.readFileSync(submissionCachePath).toString());
if (submission.author === "[deleted]" || submission.selftext === "[removed]") {
return new Error();
}
if (forbidden_subreddits.has(submission.subreddit.toLowerCase())) {
return new Error();
}
if (Object.keys(submission).length > 0) {
return submission;
}
}
// I think we're perma rate-limited. It's okay, I got most of the posts already
skippedPosts++;
return new Error();
console.log("Querying submission", getLinkToSubmission(id));
let submission = r.getSubmission(id).expandReplies({ limit: Infinity, depth: 1 });
try {
const fetchedSubmission = await new Promise(async (resolve, reject) => {
setTimeout(() => reject({ error: { error: 429 } }), 2000);
resolve(await submission.fetch());
});
const fd = fs.openSync(submissionCachePath, "w+");
fs.writeSync(fd, JSON.stringify(fetchedSubmission));
fs.closeSync(fd);
} catch (error) {
if (error.error?.error === 403) {
console.log("Failed to retrieve submission - Forbidden", getLinkToSubmission(id));
markForbidden(id);
} else if (isRateLimited(error)) {
if (await rateLimited()) {
return getSubmission(id);
}
} else {
console.log("Failed to retrieve submission - Not sure how to handle", getLinkToSubmission(id), error.name, error.message, error.constructor.name, error.error?.error);
}
skippedPosts++;
return error;
}
return submission;
}
async function getComment(id, expandReplies = true) {
if (forbidden_ids.has(id)) {
return { error: 403 }
}
const commentCachePath = `./cache/${id}.json`;
if (fs.existsSync(commentCachePath)) {
const comment = JSON.parse(fs.readFileSync(commentCachePath).toString());
if (comment.author === "[deleted]") {
return new Error();
}
if (Object.keys(comment).length > 0) {
return comment;
}
}
// I think we're perma rate-limited. It's okay, I got most of the posts already
skippedPosts++;
return new Error();
console.log("Querying comment", getLinkToComment(id), expandReplies ? "" : "without expanding replies");
let comment = r.getComment(id);
if (expandReplies) {
comment = comment.expandReplies({ limit: Infinity, depth: 1 });
}
try {
const fetchedComment = await new Promise(async (resolve, reject) => {
setTimeout(() => reject({ error: { error: 429 } }), 2000);
resolve(await comment.fetch());
});
const fd = fs.openSync(commentCachePath, "w+");
fs.writeSync(fd, JSON.stringify(fetchedComment));
fs.closeSync(fd);
return fetchedComment;
} catch (error) {
if (error.error?.error === 403) {
console.log("Failed to retrieve comment - Forbidden", getLinkToComment(id));
markForbidden(id);
} else if (isRateLimited(error)) {
if (await rateLimited()) {
return getComment(id, expandReplies);
}
} else if (expandReplies) {
// Try again without expanding replies
// Because expanding can fail on deleted posts
return getComment(id, false);
} else if (error.name === "TypeError") {
console.log("Failed to retrieve comment - Probably a private community", getLinkToComment(id), error.name, error.message);
markForbidden(id);
} else {
console.log("Failed to retrieve comment - Not sure how to handle", getLinkToComment(id), error.name, "|", error.message, "|", error.constructor.name, "|", error.error?.error);
}
skippedPosts++;
return error;
}
}
function getLinkToComment(id) {
return `https://www.reddit.com/api/info?id=${getFullId(id, "t1_")}`;
}
function getLinkToSubmission(id) {
return `https://www.reddit.com/comments/${id}`;
}
function getFullId(id, type) {
if (id.startsWith(type)) {
return id;
}
return type + id;
}
function markForbidden(id) {
forbidden_ids.add(id);
const fd = fs.openSync("./cache/forbidden.json", "w+");
fs.writeSync(fd, JSON.stringify(Array.from(forbidden_ids.keys())));
fs.closeSync(fd);
}
function markLinkUnavailable(url) {
unavailable_urls.add(url);
const fd = fs.openSync("./cache/unavailable.json", "w+");
fs.writeSync(fd, JSON.stringify(Array.from(unavailable_urls.keys())));
fs.closeSync(fd);
}
function extractUsername(usernameOrUser) {
if (typeof usernameOrUser === "string") {
return usernameOrUser;
}
return usernameOrUser.name;
}
async function extractSubreddit(subreddit) {
while (typeof subreddit === "object" && "then" in subreddit) {
// Continue unwrapping promises
// Not sure why, but one await isn't always enough
subreddit = await subreddit;
}
if (typeof subreddit !== "string") {
subreddit = subreddit.display_name;
}
subreddit = aliases_map[subreddit.toLowerCase()] ?? subreddit;
return slugify(subreddit);
}
function getRedditAvatar(user) {
return `https://www.redditstatic.com/avatars/defaults/v2/avatar_default_${cyrb53(extractUsername(user)) % 8}.png`
}
function isRateLimited(error) {
return error.constructor.name === "RateLimitError" || error.error?.error === 429;
}
function isError(error) {
const hasError = error instanceof Error || !!error.error;
if (hasError) {
skippedPosts++;
}
return hasError;
}
function processContent(content, title) {
content = sanitize(content)
.replaceAll(
/<a href="\/?([ur]\/[A-Za-z0-9_-]+)">\/?[ur]\/[A-Za-z0-9_-]+<\/a>/g,
'<a href="https://www.reddit.com/$1">$1</a>');
title = title ? `<h2>${sanitize(title).replaceAll(/</g, '&lt;').replaceAll(/>/g, '&gt;')}</h2>` : '';
return `<div class="img-container"><div class="description">${title}${content}</div></div>`;
}
// https://stackoverflow.com/a/52171480/4376101
const cyrb53 = (str, seed = 0) => {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for(let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
async function rateLimited() {
console.log(`Rate-limited by reddit. Waiting 1 minute and trying again...`);
await new Promise(resolve => setTimeout(resolve, 60 * 1000));
// Roll a new UA
return setupReddit();
}
async function getSubmissionFallback(submission) {
return await submission.preview?.images?.[0]?.resolutions?.slice(-1)[0]?.url;
}

740
process_youtube.js Normal file
View file

@ -0,0 +1,740 @@
const fs = require('fs');
const { parse } = require("csv-parse");
const readline = require('node:readline');
const { stdin: input, stdout: output } = require('node:process');
const { preparePost, encodeString, getAvatar, getActionDescription, processLinkPreview, getMediaUrl, linkify, sanitize } = require("./utils");
const { google } = require('googleapis');
const OAuth2 = google.auth.OAuth2;
const YTMusic = require("ytmusic-api");
const SCOPES = [
'https://www.googleapis.com/auth/youtube.readonly',
'https://www.googleapis.com/auth/youtube.force-ssl'
];
const TOKEN_DIR = "./";
const TOKEN_PATH = TOKEN_DIR + 'youtube_token.json';
const YOUTUBE_SVG = `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>`;
const VIDEO_CATEGORIES = {
1: "entertainment",
2: "entertainment",
10: "music",
15: "entertainment",
17: "entertainment",
18: "entertainment",
19: "blogs",
20: "gaming",
21: "blogs",
22: "blogs",
23: "comedy",
24: "entertainment",
25: "news",
26: "education",
27: "education",
28: "science",
29: "politics",
30: "movies",
31: "television",
32: "entertainment",
33: "movies",
34: "comedy",
35: "education",
36: "entertainment",
37: "blogs",
38: "entertainment",
39: "entertainment",
40: "entertainment",
41: "entertainment",
42: "entertainment",
43: "television",
44: "entertainment"
};
let createdPosts = 0;
/** @type import('googleapis').youtube_v3.Youtube */
let service;
let unavailable_urls = new Set();
let YOUTUBE_THUMBNAIL_URL;
// Load client secrets from a local file.
fs.readFile('youtube_credentials.json', function processClientSecrets(err, content) {
if (err) {
console.log('Error loading client secret file: ' + err);
return;
}
// Authorize a client with the loaded credentials, then call the YouTube API.
authorize(JSON.parse(content), process);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
*
* @param {Object} credentials The authorization client credentials.
* @param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
const clientSecret = credentials.installed.client_secret;
const clientId = credentials.installed.client_id;
const redirectUrl = credentials.installed.redirect_uris[0];
const oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, function(err, token) {
if (err) {
getNewToken(oauth2Client, callback);
} else {
oauth2Client.credentials = JSON.parse(token);
callback(oauth2Client);
}
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
*
* @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
* @param {getEventsCallback} callback The callback to call with the authorized
* client.
*/
function getNewToken(oauth2Client, callback) {
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
console.log('Authorize this app by visiting this url: ', authUrl);
const rl = readline.createInterface({ input, output });
rl.question('Enter the code from that page here: ', function(code) {
rl.close();
oauth2Client.getToken(code, function(err, token) {
if (err) {
console.log('Error while trying to retrieve access token', err);
return;
}
oauth2Client.credentials = token;
storeToken(token);
callback(oauth2Client);
});
});
}
/**
* Store token to disk be used in later program executions.
*
* @param {Object} token The token to store to disk.
*/
function storeToken(token) {
try {
fs.mkdirSync(TOKEN_DIR);
} catch (err) {
if (err.code != 'EEXIST') {
throw err;
}
}
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) throw err;
console.log('Token stored to ' + TOKEN_PATH);
});
}
async function process(auth) {
service = google.youtube('v3');
if (!fs.existsSync("./site/posts")) {
fs.mkdirSync("./site/posts");
}
if (fs.existsSync("./cache/videos-unavailable.json")) {
unavailable_urls = new Set(JSON.parse(fs.readFileSync("./cache/videos-unavailable.json").toString()));
console.log(`Loaded ${unavailable_urls.size} unavailable URLs`);
}
YOUTUBE_THUMBNAIL_URL = await getMediaUrl("https://www.youtube.com/s/desktop/86d8f362/img/favicon_32x32.png");
// Comments
const posts = fs
.createReadStream("./youtube-export/comments.csv")
.pipe(parse({ columns: true, skip_empty_lines: true }));
for await (const { "Comment ID": commentId, "Channel ID": channelId, "Comment Create Timestamp": timestampDate, "Price": price, "Parent Comment ID": parentCommentId, "Video ID": videoId, "Comment Text": commentJson } of posts) {
const comment = JSON.parse(commentJson);
let timestamp = new Date(timestampDate).getTime();
while (fs.existsSync("./site/posts/" + timestamp)) {
timestamp++;
continue;
}
fs.mkdirSync("./site/posts/" + timestamp);
const permalink = `https://www.youtube.com/watch?v=${videoId}&lc=${commentId}`;
const video = await getVideo(videoId, auth);
if (video.error || video.snippet == null) {
continue;
}
const tag = VIDEO_CATEGORIES[video.snippet.categoryId];
let parentInfo;
if (parentCommentId) {
const parentComment = await getComment(parentCommentId, auth);
if (!parentComment.error) {
const { textDisplay, authorDisplayName, authorProfileImageUrl, authorChannelUrl, publishedAt } = parentComment;
parentInfo = {
content: `<div class="img-container"><div class="description">${linkify(sanitize(textDisplay))}</div></div>`,
published: new Date(publishedAt).getTime(),
url: `https://www.youtube.com/watch?v=${videoId}&lc=${parentCommentId}`,
author: {
name: authorDisplayName,
url: `https://www.youtube.com/channel/${authorChannelUrl}`,
photo: authorProfileImageUrl
}
}
}
}
if (!parentInfo) {
const channel = await getChannel(video.snippet.channelId, auth);
if (channel.error) {
continue;
}
parentInfo = {
content: await processLinkPreview({
url: `https://www.youtube.com/watch?v=${videoId}`,
images: [video.snippet.thumbnails?.high?.url ?? ''],
favicons: [YOUTUBE_THUMBNAIL_URL],
title: video.snippet.title,
description: video.snippet.description
}),
published: new Date(video.snippet.publishedAt).getTime(),
url: `https://www.youtube.com/watch?v=${videoId}`,
author: {
name: channel.title,
url: channel.customUrl ?
`https://www.youtube.com/${channel.customUrl}` :
`https://www.youtube.com/channel/${channelId}`,
photo: channel.thumbnails?.high?.url ?? ""
}
}
}
const replies = await getReplies(commentId, auth);
if (replies.error) {
continue;
}
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: reply
title: I wrote a comment on a video by ${video.snippet.channelTitle ?? ""}
published: ${timestamp}
next: false
prev: false
tags: [${encodeString(tag, 2)}]
---
<div class="post">
${getActionDescription({ timestamp, action: "💬", verb: "replied to" })}
<div class="content-container u-in-reply-to">
${await getAvatar({
...parentInfo.author,
syndications: [{ type: YOUTUBE_SVG, url: parentInfo.url }],
timestamp: parentInfo.published,
tags: [tag]
})}
<div class="content e-content">${parentInfo.content}</div>
</div>
<div class="content-container">
${await getAvatar({
timestamp,
syndications: [{ type: YOUTUBE_SVG, url: permalink }],
})}
<div class="content e-content">
<div class="img-container">
<div class="description">${linkify(sanitize(comment.text))}</div>
</div>
</div>
</div>
${(await Promise.all(replies.map(async reply => `
<div class="content-container u-comment h-cite">
${await getAvatar({
author: {
name: reply.authorDisplayName,
url: reply.authorChannrlUrl,
photo: reply.authorProfileImageUrl
},
timestamp: new Date(reply.publishedAt).getTime(),
syndications: [{ type: YOUTUBE_SVG, url: `https://www.youtube.com/watch?v=${videoId}&lc=${reply.id}` }]
})}
<div class="content e-content">
<div class="img-container">
<div class="description">${linkify(reply.textDisplay)}</div>
</div>
</div>
</div>`))).join('')}
</div>
`));
fs.closeSync(fd);
createdPosts++;
if (createdPosts % 100 === 0) {
console.log(`Created ${createdPosts} youtube activity posts (currently commenting)`);
}
}
// Favorites
let nextPageToken;
do {
const favorite_videos = await service.playlistItems.list({
auth,
playlistId: "FLg1YH1wAWH7JF2-64XYio0A",
part: "snippet",
maxResults: 50,
pageToken: nextPageToken
}).catch(err => {
if (err.status === 403) {
console.log("Failed to retrieve videos - Forbidden", JSON.stringify(err.response));
return 403;
} else if (err.status === 404) {
console.log("Failed to retrieve videos - Not Found", JSON.stringify(err.response));
return 404;
} else if (err.status === 400) {
console.log("Failed to retrieve videos - Bad Request", JSON.stringify(err.response));
process.exit(0);
} else {
console.log("Failed to retrieve videos", err.status, JSON.stringify(err.response));
return err.status;
}
});
if (typeof favorite_videos === "number") {
break;
}
await Promise.all(favorite_videos.data.items.map(async video => {
// Don't save video to file because its missing properties like categoryId
await likeVideo({ videoId: video.snippet.resourceId.videoId, auth, kind: "favorite" });
}));
nextPageToken = favorite_videos.data.nextPageToken;
} while (nextPageToken);
// Liked songs
const liked_songs = fs
.createReadStream("./youtube-export/music-library-songs.csv")
.pipe(parse({ columns: true, skip_empty_lines: true }));
const ytmusic = new YTMusic();
await ytmusic.initialize();
for await (const { "Song URL": permalink, "Song Title": title, "Album Title": album, "Artist Names": artists } of liked_songs) {
const songId = permalink.match(/https:\/\/music\.youtube\.com\/watch\?v\=(.*)/)[1];
const song = await getSong(songId, ytmusic);
const lastModified = song.formats[0]?.lastModified ?? song.adaptiveFormats[0]?.lastModified;
if (!lastModified) {
// TODO Try searching for the song (and dedupe) (and handle this case in getSong)
continue;
}
let timestamp = new Date(parseInt(lastModified.slice(0, -3))).getTime();
while (fs.existsSync("./site/posts/" + timestamp)) {
timestamp++;
continue;
}
fs.mkdirSync("./site/posts/" + timestamp);
const channel = await getChannel(song.artist.artistId, auth);
if (channel.error) {
continue;
}
let url;
if (channel.customUrl) {
if (channel.customUrl.startsWith('https')) {
url = channel.customUrl;
} else {
url = `https://www.youtube.com/${channel.customUrl}`;
}
} else {
url = `https://www.youtube.com/channel/${song.artist.artistId}`;
}
const author = {
name: channel.title,
url,
photo: channel.thumbnails?.high?.url ?? ""
};
const content = `<div class="img-container">
<img src="${await getMediaUrl(song.thumbnails[song.thumbnails.length - 1].url)}" />
<div class="description">
<a class="u-url" href="${permalink}"><h2><img src="https://music.youtube.com/img/favicon_32.png" />${title}</h2></a>
<pre>${permalink}</pre>
</div>
</div>`;
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: like
title: ${encodeString(title, 2)}
published: ${timestamp}
next: false
prev: false
tags: ["music"]
---
<div class="post">
${getActionDescription({ timestamp, action: "❤️", verb: "liked" })}
<div class="content-container u-in-reply-to">
${await getAvatar({
...author,
syndications: [{ type: YOUTUBE_SVG, url: permalink }],
timestamp,
tags: ["music"]
})}
<div class="content e-content">${content}</div>
</div>
</div>`));
fs.closeSync(fd);
createdPosts++;
if (createdPosts % 100 === 0) {
console.log(`Created ${createdPosts} youtube activity posts (currently liking songs)`);
}
}
// Liked videos
nextPageToken = undefined;
const allLikedVideos = new Set(JSON.parse(fs.readFileSync("./youtube-export/liked_videos.json").toString()));
console.log(`Loaded ${allLikedVideos.size} liked videos`);
do {
const liked_videos = await service.playlistItems.list({
auth,
playlistId: "LL",
part: "snippet",
maxResults: 50,
pageToken: nextPageToken
}).catch(err => {
if (err.status === 403) {
console.log("Failed to retrieve videos - Forbidden", JSON.stringify(err.response));
return 403;
} else if (err.status === 404) {
console.log("Failed to retrieve videos - Not Found", JSON.stringify(err.response));
return 404;
} else if (err.status === 400) {
console.log("Failed to retrieve videos - Bad Request", JSON.stringify(err.response));
process.exit(0);
} else {
console.log("Failed to retrieve videos", err.status, JSON.stringify(err.response));
return err.status;
}
});
if (typeof liked_videos === "number") {
break;
}
let hasInsertedNone = true;
liked_videos.data.items.map(video => {
if (!allLikedVideos.has(video.snippet.resourceId.videoId)) {
console.log("Found newly liked video", `https://www.youtube.com/watch?v=${video.snippet.resourceId.videoId}`);
}
hasInsertedNone &= !allLikedVideos.has(video.snippet.resourceId.videoId);
allLikedVideos.add(video.snippet.resourceId.videoId);
// Don't save video to file because its missing properties like categoryId
});
// Early exit if a page had all duplicates
if (hasInsertedNone) {
break;
}
nextPageToken = liked_videos.data.nextPageToken;
} while (nextPageToken);
const fd = fs.openSync("./youtube-export/liked_videos.json", "w+");
fs.writeSync(fd, JSON.stringify(Array.from(allLikedVideos)));
fs.closeSync(fd);
for await (const videoId of allLikedVideos) {
await likeVideo({ videoId, auth, kind: "like" });
}
console.log(`Created ${createdPosts} posts.`);
}
/** @returns {import('googleapis').youtube_v3.Schema$CommentSnippet} */
async function getComment(commentId, auth) {
const commentCachePath = `./cache/comment-${commentId}.json`;
if (fs.existsSync(commentCachePath)) {
const comment = JSON.parse(fs.readFileSync(commentCachePath).toString());
if (Object.keys(comment).length > 0) {
return comment;
}
}
if (unavailable_urls.has(commentId)) {
return { error: "Unavailable" };
}
console.log("Querying comment", commentId);
const comment = await service.comments.list({
auth,
part: "snippet",
id: [commentId]
}).catch(err => {
if (err.status === 403) {
console.log("Failed to retrieve comment - Forbidden", commentId, JSON.stringify(err.response));
return 403;
} else if (err.status === 404) {
console.log("Failed to retrieve comment - Not Found", commentId, JSON.stringify(err.response));
return 404;
} else if (err.status === 400) {
console.log("Failed to retrieve comment - Bad Request", commentId, JSON.stringify(err.response));
process.exit(0);
} else {
console.log("Failed to retrieve comment", commentId, err.status, JSON.stringify(err.response));
return err.status;
}
});
if (typeof comment === "number") {
return { error: comment };
}
const snippet = comment?.data.items[0]?.snippet;
if (snippet == null) {
console.log("Could not parse comment", JSON.stringify(comment?.data));
markLinkUnavailable(commentId);
return { error: "Could not parse comment" };
}
const fd = fs.openSync(commentCachePath, "w+");
fs.writeSync(fd, JSON.stringify(snippet));
fs.closeSync(fd);
return snippet;
}
/** @returns {import('googleapis').youtube_v3.Schema$CommentSnippet[]} */
async function getReplies(commentId, auth) {
const commentCachePath = `./cache/replies-${commentId}.json`;
if (fs.existsSync(commentCachePath)) {
return JSON.parse(fs.readFileSync(commentCachePath).toString());
}
console.log("Querying replies", commentId);
const comments = await service.comments.list({
auth,
part: "snippet",
parentId: [commentId]
}).catch(err => {
if (err.status === 403) {
console.log("Failed to retrieve replies - Forbidden", commentId, JSON.stringify(err.response));
return 403;
} else if (err.status === 404) {
console.log("Failed to retrieve replies - Not Found", commentId, JSON.stringify(err.response));
return 404;
} else if (err.status === 400) {
console.log("Failed to retrieve replies - Bad Request", commentId, JSON.stringify(err.response));
process.exit(0);
} else {
console.log("Failed to retrieve replies", commentId, err.status, JSON.stringify(err.response));
return err.status;
}
});
if (typeof comments === "number") {
return { error: comments };
}
const snippets = comments?.data.items.map(item => ({ ...item.snippet, id: item.id }));
if (snippets == null) {
console.log("Could not parse replies", JSON.stringify(comments?.data));
return { error: "Could not parse replies" };
}
const fd = fs.openSync(commentCachePath, "w+");
fs.writeSync(fd, JSON.stringify(snippets));
fs.closeSync(fd);
return snippets;
}
/** @returns {import('googleapis').youtube_v3.Schema$Video} */
async function getVideo(videoId, auth) {
const videoCachePath = `./cache/video-${videoId}.json`;
if (fs.existsSync(videoCachePath)) {
const video = JSON.parse(fs.readFileSync(videoCachePath).toString());
if (Object.keys(video).length > 0 && video.snippet.categoryId != null) {
return video;
}
}
if (unavailable_urls.has(videoId)) {
return { error: "Unavailable" };
}
console.log("Querying video", videoId);
const video = await service.videos.list({
auth,
part: "snippet",
id: [videoId]
}).catch(err => {
if (err.status === 403) {
console.log("Failed to retrieve video - Forbidden", videoId, JSON.stringify(err.response));
return 403;
} else if (err.status === 404) {
console.log("Failed to retrieve video - Not Found", videoId, JSON.stringify(err.response));
return 404;
} else if (err.status === 400) {
console.log("Failed to retrieve video - Bad Request", videoId, JSON.stringify(err.response));
process.exit(0);
} else {
console.log("Failed to retrieve video", videoId, err.status, JSON.stringify(err.response));
return err.status;
}
});
if (typeof video === "number") {
return { error: video };
}
const item = video?.data.items[0];
if (item == null) {
console.log("Could not parse video", JSON.stringify(video?.data));
markLinkUnavailable(videoId);
return { error: "Could not parse video" };
}
const fd = fs.openSync(videoCachePath, "w+");
fs.writeSync(fd, JSON.stringify(item));
fs.closeSync(fd);
return item;
}
/** @returns {import('googleapis').youtube_v3.Schema$ChannelSnippet} */
async function getChannel(channelId, auth) {
const channelCachePath = `./cache/channel-${channelId}.json`;
if (fs.existsSync(channelCachePath)) {
const channel = JSON.parse(fs.readFileSync(channelCachePath).toString());
if (Object.keys(channel).length > 0) {
return channel;
}
}
if (unavailable_urls.has(channelId)) {
return { error: "Unavailable" };
}
console.log("Querying channel", channelId);
const channel = await service.channels.list({
auth,
part: "snippet",
id: [channelId]
}).catch(err => {
if (err.status === 403) {
console.log("Failed to retrieve channel - Forbidden", channelId, JSON.stringify(err.response));
return 403;
} else if (err.status === 404) {
console.log("Failed to retrieve channel - Not Found", channelId, JSON.stringify(err.response));
return 404;
} else if (err.status === 400) {
console.log("Failed to retrieve channel - Bad Request", channelId, JSON.stringify(err.response));
process.exit(0);
} else {
console.log("Failed to retrieve channel", channelId, err.status, JSON.stringify(err.response));
return err.status;
}
});
if (typeof channel === "number") {
return { error: channel };
}
if (!channel?.data.items?.length) {
return { error: "Could not find channel" };
}
const item = channel?.data.items[0]?.snippet;
if (item == null) {
console.log("Could not parse channel", JSON.stringify(channel?.data));
markLinkUnavailable(channelId);
return { error: "Could not parse channel" };
}
const fd = fs.openSync(channelCachePath, "w+");
fs.writeSync(fd, JSON.stringify(item));
fs.closeSync(fd);
return item;
}
async function getSong(songId, ytmusic) {
const songCachePath = `./cache/song-${songId}.json`;
if (fs.existsSync(songCachePath)) {
const song = JSON.parse(fs.readFileSync(songCachePath).toString());
if (Object.keys(song).length > 0) {
return song;
}
}
console.log("Querying song", songId);
const song = await ytmusic.getSong(songId)
.catch(err => {
console.log("Failed to retrieve song", songId, JSON.stringify(err));
return err;
});
if (song instanceof Error) {
return { error: song };
}
const fd = fs.openSync(songCachePath, "w+");
fs.writeSync(fd, JSON.stringify(song));
fs.closeSync(fd);
return song;
}
async function likeVideo({ videoId, auth, kind }) {
const permalink = `https://www.youtube.com/watch?v=${videoId}`;
const video = await getVideo(videoId, auth);
if (video.error || video.snippet == null) {
return;
}
let timestamp = new Date(video.snippet.publishedAt).getTime();
while (fs.existsSync("./site/posts/" + timestamp)) {
timestamp++;
continue;
}
fs.mkdirSync("./site/posts/" + timestamp);
const tag = VIDEO_CATEGORIES[video.snippet.categoryId];
const channel = await getChannel(video.snippet.channelId, auth);
if (channel.error) {
return;
}
const author = {
name: channel.title,
url: channel.customUrl ?
`https://www.youtube.com/${channel.customUrl}` :
`https://www.youtube.com/channel/${video.snippet.channelId}`,
photo: channel.thumbnails?.high?.url ?? ""
};
const content = `<div class="img-container">
<img src="${await getMediaUrl(video.snippet.thumbnails?.high?.url ?? '')}" />
<div class="description">
<a class="u-url" href="https://www.youtube.com/watch?v=${videoId}"><h2><img width="30px" height="30px" src="${YOUTUBE_THUMBNAIL_URL}" />${video.snippet.title}</h2></a>
<div>${linkify(video.snippet.description.replaceAll(/\n/g, '<br />'))}</div>
<pre>https://www.youtube.com/watch?v=${videoId}</pre>
</div>
</div>`;
const fd = fs.openSync("./site/posts/" + timestamp + "/index.md", "w+");
fs.writeSync(fd, preparePost(`---
kind: ${kind}
title: ${encodeString(video.snippet.title, 2)}
published: ${timestamp}
next: false
prev: false
tags: [${encodeString(tag, 2)}]
---
<div class="post">
${getActionDescription({ timestamp, action: kind === "favorite" ? "⭐" : "❤️", verb: kind === "favorite" ? "favorited" : "liked" })}
<div class="content-container h-cite u-like-of">
${await getAvatar({
...author,
syndications: [{ type: YOUTUBE_SVG, url: permalink }],
timestamp,
tags: [tag]
})}
<div class="content e-content">${content}</div>
</div>
</div>
`));
fs.closeSync(fd);
createdPosts++;
if (createdPosts % 100 === 0) {
console.log(`Created ${createdPosts} youtube activity posts (currently ${kind.slice(0, -1)}ing videos)`);
}
}
function markLinkUnavailable(url) {
unavailable_urls.add(url);
const fd = fs.openSync("./cache/videos-unavailable.json", "w+");
fs.writeSync(fd, JSON.stringify(Array.from(unavailable_urls.keys())));
fs.closeSync(fd);
}

View file

@ -10,6 +10,22 @@ for (const match of data.matchAll(/:favorites \["([^\]]+)"\]/g)) {
favorites = match[1].split("\" \"").map(page => ({ text: page, link: `/garden/${page.toLowerCase().replaceAll(' ', '-')}` }));
}
function collectPosts(path, text) {
const years = fs.readdirSync(path);
if (years.length === 1 && years[0] === "index.md") {
return { text, link: path.slice(6) };
}
const items: any[] = [];
for (const year of years) {
items.push({
text: year,
link: `${path.slice(6)}/${year}`
});
}
return { text, items, collapsed: true };
}
export default {
lang: "en-US",
title: 'The Paper Pilot',
@ -42,6 +58,9 @@ export default {
['link', { rel: 'alternate', type: "application/rss+xml", title: 'Changelog', href: '/changelog/rss' }],
['link', { rel: 'alternate', type: "application/atom+xml", title: 'Changelog', href: '/changelog/atom' }],
['link', { rel: 'alternate', type: "application/json+xml", title: 'Changelog', href: '/changelog/json' }],
['link', { rel: 'alternate', type: "application/rss+xml", title: 'Posts', href: '/posts/rss' }],
['link', { rel: 'alternate', type: "application/atom+xml", title: 'Posts', href: '/posts/atom' }],
['link', { rel: 'alternate', type: "application/json+xml", title: 'Posts', href: '/posts/json' }],
['link', { rel: 'me', href: 'mailto:thepaperpilot@incremental.social' }],
['link', { rel: 'me', href: 'https://incremental.social/u/thepaperpilot' }],
['link', { rel: 'me', href: 'https://matrix.to/#/@thepaperpilot:incremental.social' }],
@ -75,10 +94,17 @@ export default {
provider: 'local',
options: {
_render(src, env, md) {
const html = md.render(src, env);
if (env.frontmatter?.search === false) return '';
if (env.relativePath.startsWith('public')) return '';
if (env.relativePath.startsWith('guide-to-incrementals')) return '';
if (env.relativePath.startsWith('tags')) return '';
if (env.relativePath.startsWith('types')) return '';
if (env.relativePath.startsWith('timeline')) return '';
if (env.relativePath.startsWith('posts')) return '';
if (env.relativePath.match(/type\/.*\/[^1]/)) return '';
if (env.relativePath.match(/tag\/.*\/[^1]/)) return '';
const html = md.render(src, env);
if (env.frontmatter?.search === false) return '';
console.log(env.relativePath)
return html;
}
}
@ -99,9 +125,22 @@ export default {
text: "Recommended Pages",
items: favorites
},
{ text: "About Me", link: "/about" },
{ text: "/now", link: "/now" },
{ text: "Garden Changelog", link: "/changelog" }
{
text: "Posts",
items: [
{ text: "Timeline", link: "/timeline/1" },
{ text: "Tags", link: "/tags" },
{ text: "Types", link: "/types" }
]
},
{
text: "Meta",
items: [
{ text: "About Me", link: "/about" },
{ text: "/now", link: "/now" },
{ text: "Garden Changelog", link: "/changelog" }
]
}
]
},
contentProps: {

View file

@ -14,13 +14,18 @@
</div>
</ClientOnly>
</template>
<template #doc-before>
<div class="old-warning" v-if="yearsDiff > 2">
This is an old post made over {{Math.floor(yearsDiff)}} years ago!<br/>My views <a href="/garden/my-political-journey">change over time</a> and my older timeline posts may not reflect my current views!
</div>
</template>
<template #layout-bottom>
<footer class="vp-doc">
<div>CC {{ new Date().getFullYear() }} <a class="h-card" rel="me" href="/about"><img src="/me.jpg" alt="" />The Paper Pilot</a>. <a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.</div>
<div>Any and all opinions listed here are my own and not representative of my employers; future, past and present.</div>
<div><a href="https://resume.incremental.social/thepaperpilot/thepaperpilot">Resume</a> (not actively seeking new opportunities).</div>
<div>Site built from <a href="COMMIT_LINK">this commit</a> on <time>COMMIT_TIME</time>. <a href="https://www.thepaperpilot.org/licenses.txt">Legal disclaimers</a>.</div>
</footer>
<div>CC {{ new Date().getFullYear() }} <a class="h-card" rel="me" href="/about"><img src="/me.jpg" alt="" />The Paper Pilot</a>. <a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>.</div>
<div>Any and all opinions listed here are my own and not representative of my employers; future, past and present.</div>
<div><a href="https://resume.incremental.social/thepaperpilot/thepaperpilot">Resume</a> (not actively seeking new opportunities).</div>
<div>Site built from <a href="COMMIT_LINK">this commit</a> on <time>COMMIT_TIME</time>. <a href="https://www.thepaperpilot.org/licenses.txt">Legal disclaimers</a>.</div>
</footer>
</template>
</DefaultTheme.Layout>
</template>
@ -29,11 +34,14 @@
import { NolebaseHighlightTargetedHeading } from '@nolebase/vitepress-plugin-highlight-targeted-heading/client'
import { TresCanvas } from '@tresjs/core'
import '@nolebase/vitepress-plugin-highlight-targeted-heading/client/style.css'
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import { computed } from 'vue'
import Background from './Background.vue'
import Camera from './Camera.vue'
import OrbitControls from './OrbitControls.vue'
import './custom.css'
const { frontmatter } = useData();
const yearsDiff = computed(() => frontmatter.value.published ? Math.round((Date.now() - frontmatter.value.published) / (1000 * 60 * 60 * 24 * 365)) : 0);
</script>
<style scoped>

View file

@ -93,7 +93,7 @@ ul > li > ul::before {
margin: auto;
}
.img-container img {
.img-container > img {
clip-path: polygon(18px 0%, 100% 0, 100% calc(100% - 18px), calc(100% - 18px) 100%, 0 100%, 0% 18px);
height: 100%;
object-fit: contain;
@ -153,7 +153,8 @@ ul > li > ul::before {
background-size: 100% 30px;
}
.vp-doc a {
.vp-doc a,
.old-warning a {
color: unset;
text-decoration: unset;
font-weight: unset;
@ -172,6 +173,10 @@ ul > li > ul::before {
box-decoration-break: clone;
}
._tags a {
white-space: nowrap;
}
.vp-doc a:hover {
color: unset;
background-image: linear-gradient(
@ -181,7 +186,7 @@ ul > li > ul::before {
rgba(255, 225, 0, 0.3)
);
}
.vp-doc a[href^="http"]:after {
.vp-doc a[href^="http"]:not(:has(> .e-content))::after {
content: "";
display: inline-block;
margin-top: -1px;
@ -518,17 +523,37 @@ a.title {
}
}
.VPContent .container {
.NotFound {
padding: 60px 24px 90px !important;
}
@media (min-width: 768px) {
.NotFound {
padding: 90px 32px 150px !important;
}
}
.VPContent .container,
.NotFound {
background: linear-gradient(to bottom, var(--vp-sidebar-bg-color) 29px, var(--vp-c-divider) 1px);
background-size: 100% 30px;
line-height: 30px;
box-shadow: 0 0 10px 1px #0003 !important;
}
.NotFound .quote {
margin: 42px auto !important;
}
.VPContent .content {
padding: 0 30px !important;
}
.NotFound .divider,
.NotFound .action {
display: none;
}
.content-container {
border-left: double 7px var(--vp-c-divider);
padding: 30px 0 30px 8px;
@ -610,12 +635,8 @@ hr {
}
.group + .group {
/* Adapted from https://github.com/chr15m/DoodleCSS */
border-top-style: solid !important;
border-top-width: 10px !important;
border-image: url(/button.svg) 10 10 10 10 stretch stretch !important;
padding-top: 23px !important;
margin-top: -3px !important;
border-top-style: none !important;
margin-top: 30px !important;
}
table {
@ -689,7 +710,7 @@ footer img {
}
.title-icon {
line-height: 30px
line-height: 30px;
}
.excerpt {
@ -730,3 +751,317 @@ footer img {
border-top: solid 1px var(--vp-c-border) !important;
cursor: pointer !important;
}
.post .content {
padding: 0 !important;
flex-grow: 1;
}
.post .content-container {
border-left: none;
padding: 0 30px 0 0;
display: flex;
}
.post .content-container .h-card {
width: 120px;
margin-top: 15px;
margin-right: 30px;
flex-shrink: 0;
}
.post .content-container .avatar .u-url {
display: flex;
flex-direction: column;
padding: 0;
}
.post .content-container .u-url {
line-height: 30px;
display: block;
padding: 0 .4em;
}
.post .content-container .h-card a::after,
.post .content-container .description a::after {
display: none !important;
}
.post .photo {
position: relative;
}
.post .photo img {
border-radius: 50%;
}
.post .action-description .photo img {
width: 30px;
height: 30px;
}
.post .content-container .photo img {
width: 105px;
height: 105px;
margin: 15px;
}
.post .photo .action {
position: absolute;
bottom: 0;
right: 10px;
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--vp-c-bg);
display: flex;
justify-content: center;
z-index: 1;
}
.post .avatar .p-name {
text-align: center;
overflow-wrap: break-word;
}
.post:not(.content) > .h-card {
bottom: unset;
top: 0;
}
.post .syndications {
display: flex;
justify-content: center;
}
.post .syndications a:not(.u-url) {
width: 30px;
padding: 0;
background-image: none;
}
.post .syndications a:not(.u-url) svg {
stroke: black;
fill: rgba(255, 225, 0, 0.35);
stroke-width: 1;
}
.post .avatar .photo {
position: relative;
}
.post .avatar time {
line-height: 30px;
height: 30px;
display: block;
text-align: center;
}
.post .tags {
list-style: none;
padding-left: 0;
text-align: center;
}
.post .tags li::before {
content: "#";
}
.u-url > .e-content h2 {
vertical-align: middle;
margin-top: -7px !important;
margin-bottom: 0 !important;
}
.u-url > .e-content img {
display: inline;
margin: 0 10px;
transform: translateY(7px);
}
.u-url > .e-content div:last-child {
margin-bottom: -30px;
}
.post .u-in-reply-to .h-card,
.post .u-comment .h-card {
margin-top: 0;
}
.post .u-in-reply-to .h-card .u-photo,
.post .u-comment .h-card .u-photo {
width: 60px;
height: 60px;
margin: 0 auto;
}
.u-comment + .u-comment {
margin-top: 30px;
}
:not(.u-comment) + .u-comment {
margin-top: 60px;
position: relative;
}
:not(.u-comment) + .u-comment::before {
content: "Comments";
position: absolute;
top: -45px;
font-size: 24px;
font-weight: 600;
}
.u-in-reply-to + .content-container .avatar {
margin-bottom: 30px;
}
h2:has(+ .post) {
margin-bottom: 0 !important;
}
h1 ~ .post .u-comment {
display: none;
}
.post blockquote {
border-left: none;
}
.post .img-container {
height: unset;
background: white;
clip-path: polygon(-30px -30px, calc(100% + 30px) -30px, calc(100% + 30px) calc(100% - 75px), calc(100% - 75px) calc(100% + 30px), -30px calc(100% + 30px));
margin-bottom: 15px;
width: 100%;
}
.post .img-container a,
.post .img-container pre {
white-space: break-spaces;
word-break: break-word;
}
.post .img-container > img {
clip-path: none;
max-height: 11lh;
}
.post h2 > img {
display: inline-block;
vertical-align: bottom;
margin-right: 10px;
width: 30px;
height: 30px;
}
.post .img-container:after {
border: 75px solid transparent;
}
.post .img-container:before {
visibility: hidden;
}
.post .img-container:before {
top: -105px;
left: -105px;
}
.post .img-container:after {
bottom: -105px;
right: -105px;
}
.img-container > h2:first-child {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
h1 ~ .post .description {
max-height: 180px;
mask-image: linear-gradient(to bottom, white 150px, transparent);
overflow: hidden;
}
h1 ~ .post iframe {
display: none;
}
.post .description {
word-break: break-word;
}
.post .description:not(:first-child) {
margin-top: 15px;
}
.post .description > div:first-child > p:first-child {
margin: 0 !important;
}
.post .description h2:first-child,
.post .description pre {
margin-top: 0 !important;
margin-bottom: 0 !important;
display: block;
}
.action-description > a + a {
margin: 0 2px;
}
.action-description > .action {
margin-right: 4px;
}
.yearLink {
margin-right: 4px;
}
p > a:has(+ a) {
margin-right: 10px !important;
}
.img-container > div:first-child > p:first-child {
margin-top: 0 !important;
}
.old-warning {
background: red;
padding: 15px;
margin-bottom: 30px;
font-size: 24px;
background: white;
border: red dashed 10px;
position: relative;
padding-left: 130px;
min-height: 150px;
}
.old-warning::before {
content: "STOP";
position: absolute;
--o: calc(50%* tan(-22.5deg));
clip-path: polygon(var(--o) 50%, 50% var(--o), calc(100% - var(--o)) 50%, 50% calc(100% - var(--o)));
width: 100px;
height: 100px;
background: red;
left: 15px;
top: 15px;
text-align: center;
color: white;
padding-top: 32px;
font-size: 32px;
font-family: 'Roboto Mono';
font-weight: 900;
}
.u-in-reply-to ~ .content-container > .e-content {
padding-top: 30px !important;
}
.u-in-reply-to ~ .content-container > .avatar {
margin-top: 30px;
margin-bottom: 15px;
}

View file

@ -14,7 +14,14 @@ next: false
<div class="img-container"><img class="u-photo" src="/me.jpg" /></div>
I live in <span class="p-locality">Dallas</span>, <span class="p-region">Texas</span>, <span class="p-country-name">USA</span> with my wife and baby son. <span class="p-org h-card">I work at <span class="p-name">Topaz Labs LLC</span> as a <span class="p-job-title">Product Engineer</span>. <span class="p-role">I develop their flagship product Topaz Photo AI</span>.</span>
<div>
<div class="img-container small"><img class="u-photo" src="/avatar.jpg" /></div>
<div class="img-container small"><img class="u-photo" src="/haleyDrawing.jpg" /></div>
<div class="img-container small"><img class="u-photo" src="/headshot.jpg" /></div>
<div class="img-container small"><img class="u-photo" src="/profile.png" /></div>
</div>
I live in <span class="p-locality">Dallas</span>, <span class="p-region">Texas</span>, <span class="p-country-name">USA</span> with <a href="https://linktr.ee/myotherheart">my wife</a> and baby son. <span class="p-org h-card">I work at <span class="p-name">Topaz Labs LLC</span> as a <span class="p-job-title">Product Engineer</span>. <span class="p-role">I develop their flagship product Topaz Photo AI</span>.</span>
Reach out to me:
<!-- If updating these links, make sure to add them to the site's <head> as well -->

View file

@ -15,7 +15,7 @@ const pageData = useData();
<p>1493 words, ~8 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/anarchism/index.md">Anarchism</a><a href="/garden/consensus-democracy/index.md">Consensus Democracy</a></details>
<details><summary>Referenced by:</summary><a href="/garden/anarchism">Anarchism</a><a href="/garden/consensus-democracy">Consensus Democracy</a><a href="/garden/my-political-beliefs">My Political Beliefs</a></details>
I'm a supporter of the police abolition movement, which calls for police and prisons to be abolished. It argues that there are many inherent problems with policing and incarcerating people that cannot be fixed with just further training or restrictions - the entire system must be entirely abolished. In this way, it is a more extreme version of the police reform or defund the police movements. The movement also posits that there are alternatives to policing and incarceration that can be more effective at reducing crime.

View file

@ -15,8 +15,8 @@ const pageData = useData();
<p>8 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/fediverse/index.md">Fediverse</a></details>
<details><summary>Referenced by:</summary><a href="/garden/fediverse">Fediverse</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized/index.md">Decentralized</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized">Decentralized</a></details>
[ActivityPub](https://activitypub.rocks) is a protocol for [Federated Social Media](/garden/fediverse/index.md)
[ActivityPub](https://activitypub.rocks) is a protocol for [Federated Social Media](/garden/fediverse)

View file

@ -15,11 +15,11 @@ const pageData = useData();
<p>104 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a><a href="/garden/profectus/index.md">Profectus</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a><a href="/garden/profectus">Profectus</a></details>
Play it [here](https://thepaperpilot.org/advent)!
An [Open Source](/garden/open-source/index.md) game made in [Profectus](/garden/profectus/index.md) over the course of 1 month by myself and other devs I know in the Incremental Games community!
An [Open Source](/garden/open-source) game made in [Profectus](/garden/profectus) over the course of 1 month by myself and other devs I know in the Incremental Games community!
I had the idea of an advent-style game that unlocked new pieces of content every real-life day a couple days before December started.

View file

@ -12,16 +12,18 @@ import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">Anarchism</h1>
<p>966 words, ~5 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<p>1043 words, ~6 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/individualism/index.md">Individualism</a><a href="/garden/local-communities/index.md">Local Communities</a><a href="/garden/representative-democracy/index.md">Representative Democracy</a></details>
<details><summary>Referenced by:</summary><a href="/garden/individualism">Individualism</a><a href="/garden/local-communities">Local Communities</a><a href="/garden/my-political-beliefs">My Political Beliefs</a><a href="/garden/representative-democracy">Representative Democracy</a></details>
I'm not a full-blown anarchist, but closely align to anarchist values and would like to see them influence policy. Anarchists believe that states are inherently immoral, and societies should be structured to have as minimal of a hierarchy as possible. This entails focusing on [Local Communities](/garden/local-communities/index.md) and spreading power as thinly as possible, to avoid the possibility of individuals becoming corrupt and abusing their power.
I like and appreciate a lot of the anarchist values and would like to see them influence policy. Anarchists believe that states are inherently immoral, and societies should be structured to have as minimal of a hierarchy as possible. This entails focusing on [Local Communities](/garden/local-communities) and spreading power as thinly as possible, to avoid the possibility of individuals becoming corrupt and abusing their power.
Anarchism is anti-authoritarian, and explicitly denounces any use of violence to enforce rules, thus requiring [Police Abolition](/garden/abolitionism/index.md). By similar logic, anarchists tend to oppose imperialism and capitalism and the respective hierarchies they create. There are those who consider themselves "anarcho-capitalists" without realizing (or are ignoring) the hierarchies created by wealth inequality. These are incompatible views, and the person is likely actually authoritarian.
Anarchism is anti-authoritarian, and explicitly denounces any use of violence to enforce rules, thus requiring [Police Abolition](/garden/abolitionism). By similar logic, anarchists tend to oppose imperialism and capitalism and the respective hierarchies they create. There are those who consider themselves "anarcho-capitalists" without realizing (or are ignoring) the hierarchies created by wealth inequality. These are incompatible views, and the person is likely actually authoritarian.
Democracy is a form of electoralism that is typically compatible with Anarchism, although some definitions of anarchism disallow any form of rules, even when agreed upon unanimously. There are different forms of democracy, with [Direct Democracy](/garden/direct-democracy/index.md) and [Consensus Democracy](/garden/consensus-democracy/index.md) being the most popular variants that are compatible with anarchism. The US government is a [Representative Democracy](/garden/representative-democracy/index.md), which is NOT anarchistic. Representatives abstract policy making from the views of the people. If we're supposed to vote on the representative that will most closely vote to how we feel on all issues, then the theoretical perfect representative would just be ourselves - and at that point, we should just be voting on the issues directly. Therefore if striving for anarchism, you should not use a representative democracy as in its theoretical ideal its still only just as good as any other variant of Democracy, and in practice will be much worse.
Democracy is a form of electoralism that is typically compatible with Anarchism, although some definitions of anarchism disallow any form of rules, even when agreed upon unanimously. There are different forms of democracy, with [Direct Democracy](/garden/direct-democracy) and [Consensus Democracy](/garden/consensus-democracy) being the most popular variants that are compatible with anarchism. The US government is a [Representative Democracy](/garden/representative-democracy), which is NOT anarchistic. Representatives abstract policy making from the views of the people. If we're supposed to vote on the representative that will most closely vote to how we feel on all issues, then the theoretical perfect representative would just be ourselves - and at that point, we should just be voting on the issues directly. Therefore if striving for anarchism, you should not use a representative democracy as in its theoretical ideal its still only just as good as any other variant of Democracy, and in practice will be much worse.
A core principle of anarchism is "free association", referring to how individuals should be able to freely move between anarchist organizations to find one they're compatible with, or even frequently move between several communities they like. This can cause concerns of encouraging segregation, so I think its important for these communities to encourage diversity as much as they can. They can also refuse to associate with other bigoted communities, theoretically discouraging those bigoted views through social and material isolation.
Anarchistic organizations can still appoint roles to people. For example, if a nation like America were to be made anarchistic, it would likely maintain some roles of the President, such as that of Commander-In-Chief. It is primarily the law making and enforcing that would need to be democratized, and of course making sure those appointed roles are elected democratically.
@ -29,7 +31,7 @@ Anarchism relies on the idea that there are enough individuals motivated to syst
Democracies where the people vote on individual issues are often criticized by citing the US' current low turnout rates during elections. I believe the rates are more indicative of a lack of faith in electoralism, and in any case its not a reason to be alarmed that policies would be dictated by a minority of the population. The low turnout can work in favor of direct and consensus democracies, as it means it only takes a few motivated individuals to improve society or block proposals that would worsen it. The fact getting engaged in politics takes time and effort means you're less likely to see people blocking policies in bad faith out of contrarianism. In theory, any consolidation of power would also negatively affect most people, which would motivate them to block the proposal. That makes anarchism very stable.
In contrast to [Neoliberalism](/garden/neoliberalism/index.md), anarchism calls for systemic solutions to problems, rather than reliance on individual charity. In America, charity has never been sufficient to end hunger or homelessness. Anarchists and leftists believe we need systemic issues to these problems, such as making food, shelter, and healthcare freely accessible to all. Technology has made it trivial to provide for everyone. In America, there is more food waste than it would take to feed all the hungry, and enough vacant houses to shelter all the homeless. The scarcity is artificial, created by those at the top of the hierarchy.
In contrast to [Neoliberalism](/garden/neoliberalism), anarchism calls for systemic solutions to problems, rather than reliance on individual charity. In America, charity has never been sufficient to end hunger or homelessness. Anarchists and leftists believe we need systemic issues to these problems, such as making food, shelter, and healthcare freely accessible to all. Technology has made it trivial to provide for everyone. In America, there is more food waste than it would take to feed all the hungry, and enough vacant houses to shelter all the homeless. The scarcity is artificial, created by those at the top of the hierarchy.
Places of work can also be democratized! Typical American corporations are very hierarchical, with a few hands at the top having ultimate say over the company - what it does, how much it pays its employees, who it fires, etc. Worker's co-operatives are alternatives to corporations that are entirely worker owned and operated, with a flat hierarchy. This makes technology work in employees' favor, rather than owners' (since the employees are the owners). For example, lets say some technological innovation made employees twice as productive. Under a capitalist structure, the owners would have no reason to increase compensation based on the increased production, and in fact would be discouraged from doing so. They'd likely either use the increased productivity to sell more products, or half the workforce to cut down on significant expenditures. Under a socialist structure, the needs and desires of the employees are most important, so workers are likely to either see increased compensation due to their increased productivity, or reduced hours without a reduction in compensation. The co-operative could still decide to also just utilize the increased productivity without reducing hours nor increasing compensation, but the decision to do so would have been consensually made by the workers themselves, not their boss.

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>92 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/guide-to-incrementals/what-is-content/index.md">Guide to Incrementals/What is Content?</a></details>
<details><summary>Referenced by:</summary><a href="/garden/guide-to-incrementals/what-is-content">Guide to Incrementals/What is Content?</a></details>
> Art is never finished, only abandoned.
> \- Leonardo Da Vinci

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>101 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/command-palettes/index.md">Command Palettes</a></details>
<details><summary>Referenced by:</summary><a href="/garden/command-palettes">Command Palettes</a></details>
Catch all term that refers to many different things

View file

@ -16,12 +16,12 @@ const pageData = useData();
<p>31 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/fediverse/index.md">Fediverse</a></details>
<details><summary>Referenced by:</summary><a href="/garden/fediverse">Fediverse</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized/index.md">Decentralized</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized">Decentralized</a></details>
The [AT Protocol](https://atproto.com) is a protocol for [Federated Social Media](/garden/fediverse/index.md)
The [AT Protocol](https://atproto.com) is a protocol for [Federated Social Media](/garden/fediverse)
Currently only used by [Bluesky](https://bsky.app)
In comparison to other [Fediverse](/garden/fediverse/index.md) protocols, ATProto is designed for a small number of large instances
In comparison to other [Fediverse](/garden/fediverse) protocols, ATProto is designed for a small number of large instances

View file

@ -15,7 +15,7 @@ const pageData = useData();
<p>113 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
[Babble Buds](http://babblebuds.xyz) is a tool for creating puppets and interacting with puppets controlled by others on a shared stage
@ -27,5 +27,5 @@ Intended for use in RPG Campaigns
The renderer was separated into its own project, [babble.js](https://github.com/thepaperpilot/babble.js), so it could be used for stuff like cutscenes
I ported the engine to C# and used it for the cutscenes in [Dice Armor](/garden/dice-armor/index.md)
I ported the engine to C# and used it for the cutscenes in [Dice Armor](/garden/dice-armor)
- I don't believe I ever separated it out into its own project, but you can find the code [here](https://github.com/sreynoldsdesign/dice_armor/tree/master/Assets/Scripts/babble.cs)

View file

@ -15,7 +15,7 @@ const pageData = useData();
<p>39 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
A 3D VR re-envisioning of a Slay the Spire-style game by Anthony Lawn and Grant Barbee for their VR class in college's final project.

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>23 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/commune/index.md">Commune</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/the-small-web/index.md">The Small Web</a></details>
<details><summary>Referenced by:</summary><a href="/garden/commune">Commune</a><a href="/garden/the-small-web">The Small Web</a></details>
A theoretical chat system designed to solve the problems of transcribing branching conversations into linear timelines.

View file

@ -0,0 +1,19 @@
---
public: "true"
slug: "chromatic-lattice"
title: "Chromatic Lattice"
prev: false
next: false
---
<script setup>
import { data } from '../../git.data.ts';
import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">Chromatic Lattice</h1>
<p>7 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/fedi-v2">Fedi v2</a><a href="/garden/incremental-social">Incremental Social</a><a href="/now/index">/now</a></details>
A multiplayer game I have in development :)

View file

@ -14,16 +14,16 @@ const pageData = useData();
<p>73 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/freeform-vs-chronological-dichotomy/index.md">Freeform vs Chronological Dichotomy</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/freeform-vs-chronological-dichotomy">Freeform vs Chronological Dichotomy</a></details>
A collection of information that is tied to its creation or edit date
Part of the [Freeform vs Chronological Dichotomy](/garden/freeform-vs-chronological-dichotomy/index.md)
Part of the [Freeform vs Chronological Dichotomy](/garden/freeform-vs-chronological-dichotomy)
Anything with a "timeline" or "feed" is considered chronological
- Even if there's algorithmic sortings that take things other than creation or edit date into account!
Chronological displays are less suitable as stores of knowledge ([Digital Gardens](/garden/digital-gardens/index.md))
Chronological displays are less suitable as stores of knowledge ([Digital Gardens](/garden/digital-gardens))
Social media overuses timelines and feeds

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>3 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social/index.md">Incremental Social</a></details>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social">Incremental Social</a></details>
[Cinny](https://cinny.in) is an [Open Source](/garden/open-source/index.md) web client for the [Matrix](/garden/matrix/index.md) messaging protocol
[Cinny](https://cinny.in) is an [Open Source](/garden/open-source) web client for the [Matrix](/garden/matrix) messaging protocol

View file

@ -20,7 +20,7 @@ Typing what you want is almost certainly easier and faster than finding the acti
- Especially with fuzzy search that also looks through descriptions of actions
- Command palettes scale very well with large amounts of actions
[Artificial Intelligence](/garden/artificial-intelligence/index.md) will make command palettes increasingly powerful
[Artificial Intelligence](/garden/artificial-intelligence) will make command palettes increasingly powerful
- Eventually these may become conversational interfaces
Maggie Appleton discusses this pattern in her article on [Command K Bars](https://maggieappleton.com/command-bar)
@ -28,5 +28,5 @@ Maggie Appleton discusses this pattern in her article on [Command K Bars](https:
Many softwares I use have some form of command palette
- Linear
- [Logseq](/garden/logseq/index.md)
- [Logseq](/garden/logseq)
- Visual Studio Code

View file

@ -14,24 +14,24 @@ const pageData = useData();
<p>144 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/federated-identity/index.md">Federated Identity</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/now/index">/now</a><a href="/garden/webrings/index.md">Webrings</a><a href="/garden/weird/index.md">Weird</a></details>
<details><summary>Referenced by:</summary><a href="/garden/federated-identity">Federated Identity</a><a href="/now/index">/now</a><a href="/garden/webrings">Webrings</a><a href="/garden/weird">Weird</a></details>
An [Open Source](/garden/open-source/index.md) [Matrix](/garden/matrix/index.md) web client built to be better for communities than anything else out there
An [Open Source](/garden/open-source) [Matrix](/garden/matrix) web client built to be better for communities than anything else out there
- Currently in development
- Exposes certain channels such that they are web indexable
- Will include features like [Chat Glue](/garden/chat-glue/index.md) and communal [Digital Gardens](/garden/digital-gardens/index.md)
- Will include features like [Chat Glue](/garden/chat-glue) and communal [Digital Gardens](/garden/digital-gardens)
Created by [Erlend Sogge Heggen](https://writing.exchange/@erlend), a ex-employee from Discourse
- Maintains the [Commune Blog](https://blog.commune.sh) with great write ups on the issues of the modern web, social media, etc. and how they can be improved (by Commune or related projects)
- Also maintains a [Personal Blog](https://blog.erlend.sh) about similar topics
The Commune community is very interested in various topics and how they can relate together:
- [Federated Identity](/garden/federated-identity/index.md)
- [Personal Web](/garden/the-small-web/index.md)
- [Digital Gardens](/garden/digital-gardens/index.md)
- [Social Media](/garden/social-media/index.md)
- The common themes here are they want these things [Decentralized](/garden/decentralized/index.md) and [Freeform](/garden/freeform/index.md)
- They're also building [Weird](/garden/weird/index.md) to make several of these more accessible
- [Federated Identity](/garden/federated-identity)
- [Personal Web](/garden/the-small-web)
- [Digital Gardens](/garden/digital-gardens)
- [Social Media](/garden/social-media)
- The common themes here are they want these things [Decentralized](/garden/decentralized) and [Freeform](/garden/freeform)
- They're also building [Weird](/garden/weird) to make several of these more accessible
Related projects:
- [@laxla@tech.lgbt](https://tech.lgbt/@laxla) is creating Gimli, a federated discord alternative

View file

@ -14,12 +14,12 @@ const pageData = useData();
<p>162 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/anarchism/index.md">Anarchism</a></details>
<details><summary>Referenced by:</summary><a href="/garden/anarchism">Anarchism</a><a href="/garden/my-political-beliefs">My Political Beliefs</a></details>
A form of democracy similar to [Direct Democracy](/garden/direct-democracy/index.md) but with higher requirements for passing policies, typically requiring unanimity or near-unanimity. This helps reduce (although doesn't eliminate) the possibility of a majority group oppressing a minority group.
A form of democracy similar to [Direct Democracy](/garden/direct-democracy) but with higher requirements for passing policies, typically requiring unanimity or near-unanimity. This helps reduce (although doesn't eliminate) the possibility of a majority group oppressing a minority group.
Consensus democracy encourages and requires innovative solutions to problems (similar to how [Police Abolition](/garden/abolitionism/index.md)) and pragmatic compromises. However, this can make them susceptible to "design by committee" and can make policies impossibly difficult to pass for large groups of people.
Consensus democracy encourages and requires innovative solutions to problems (similar to how [Police Abolition](/garden/abolitionism)) and pragmatic compromises. However, this can make them susceptible to "design by committee" and can make policies impossibly difficult to pass for large groups of people.
Since consensus democracy doesn't scale well, larger governments could be structured as a federation of smaller governments. The smaller governments still use consensus democracy, but the federation only adopts policies that a super-majority of the smaller governments have agreed upon. Alternatively, the federation could specifically ask the local governments for policy proposals, then use [Direct Democracy](/garden/direct-democracy/index.md) to decide whether to approve it or not, still requiring a super-majority.
Since consensus democracy doesn't scale well, larger governments could be structured as a federation of smaller governments. The smaller governments still use consensus democracy, but the federation only adopts policies that a super-majority of the smaller governments have agreed upon. Alternatively, the federation could specifically ask the local governments for policy proposals, then use [Direct Democracy](/garden/direct-democracy) to decide whether to approve it or not, still requiring a super-majority.
For policies that still are unable to pass federally, local governments could form coalitions that organize larger-scale initiatives between several districts. For example, this could empower efforts like transit systems between districts.

View file

@ -14,14 +14,14 @@ const pageData = useData();
<p>37 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/ivy-road/index.md">Ivy Road</a><a href="/garden/the-beginner-s-guide/index.md">The Beginner's Guide</a></details>
<details><summary>Referenced by:</summary><a href="/garden/ivy-road">Ivy Road</a><a href="/garden/the-beginner-s-guide">The Beginner's Guide</a></details>
<details><summary>Tagged by:</summary><a href="/garden/ivy-road/index.md">Ivy Road</a><a href="/garden/the-beginner-s-guide/index.md">The Beginner's Guide</a><a href="/garden/wanderstop/index.md">Wanderstop</a></details>
<details><summary>Tagged by:</summary><a href="/garden/ivy-road">Ivy Road</a><a href="/garden/the-beginner-s-guide">The Beginner's Guide</a><a href="/garden/wanderstop">Wanderstop</a></details>
Projects:
- The Stanley Parable
- [The Beginner's Guide](/garden/the-beginner-s-guide/index.md)
- [Ivy Road](/garden/ivy-road/index.md)
- [The Beginner's Guide](/garden/the-beginner-s-guide)
- [Ivy Road](/garden/ivy-road)
Talks and Interviews:
- LATER [Tone Control 20: Davey Wreden](https://www.idlethumbs.net/tonecontrol/episodes/davey-wreden-1)

View file

@ -15,16 +15,16 @@ const pageData = useData();
<p>80 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/commune/index.md">Commune</a><a href="/garden/fedi-v2/index.md">Fedi v2</a><a href="/garden/matrix/index.md">Matrix</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/commune">Commune</a><a href="/garden/fedi-v2">Fedi v2</a><a href="/garden/matrix">Matrix</a><a href="/garden/social-media">Social Media</a></details>
<details><summary>Tagged by:</summary><a href="/garden/activitypub/index.md">ActivityPub</a><a href="/garden/atproto/index.md">ATProto</a><a href="/garden/federated-identity/index.md">Federated Identity</a><a href="/garden/fediverse/index.md">Fediverse</a><a href="/garden/nostr/index.md">Nostr</a></details>
<details><summary>Tagged by:</summary><a href="/garden/activitypub">ActivityPub</a><a href="/garden/atproto">ATProto</a><a href="/garden/federated-identity">Federated Identity</a><a href="/garden/fediverse">Fediverse</a><a href="/garden/nostr">Nostr</a></details>
Something with no central source of authority
Common examples:
- RSS
- Email
- The [Fediverse](/garden/fediverse/index.md)
- The [Fediverse](/garden/fediverse)
In practice, the "pick a server" problem causes email and the fediverse to trend towards a handful of large servers that still suffer from some of the issues of centralization

View file

@ -15,9 +15,9 @@ const pageData = useData();
<p>963 words, ~5 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/babble-buds/index.md">Babble Buds</a></details>
<details><summary>Referenced by:</summary><a href="/garden/babble-buds">Babble Buds</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
Download it [here](https://drive.google.com/open?id=18rwqEIdMChdGtB-9LdI4wiqeM5C5ViOL)
@ -61,4 +61,4 @@ The dice rolling uses the physics engine and detects once the dice have stopped
During certain events like winning the game or having the face of a die broken, the players' portraits will flash an emotion for a second. After winning, a random living die from the winning player is chosen to play their "finisher move", a flashy and dramatic effect to end the game. Shown is the arcane mechana's finisher, "Missile Storm".
After development stopped, the project became [Open Source](/garden/open-source/index.md) - check it out [here](https://github.com/sreynoldsdesign/dice_armor/tree/master/Assets/Scripts/babble.cs)
After development stopped, the project became [Open Source](/garden/open-source) - check it out [here](https://github.com/sreynoldsdesign/dice_armor/tree/master/Assets/Scripts/babble.cs)

View file

@ -15,13 +15,13 @@ const pageData = useData();
<p>67 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/chronological/index.md">Chronological</a><a href="/garden/commune/index.md">Commune</a><a href="/garden/garden-rss/index.md">Garden-RSS</a><a href="/garden/the-cozy-web/index.md">The Cozy Web</a><a href="/garden/the-small-web/index.md">The Small Web</a><a href="/garden/this-knowledge-hub/index.md">This Knowledge Hub</a></details>
<details><summary>Referenced by:</summary><a href="/garden/chronological">Chronological</a><a href="/garden/commune">Commune</a><a href="/garden/garden-rss">Garden-RSS</a><a href="/now/index">/now</a><a href="/garden/the-cozy-web">The Cozy Web</a><a href="/garden/the-small-web">The Small Web</a><a href="/garden/this-knowledge-hub">This Knowledge Hub</a></details>
Digital Gardens are [Freeform](/garden/freeform/index.md) collections of information made by an individual or community
- Alternatives to [Chronological](/garden/chronological/index.md) personal blogs
- Exist in a middleground between the dark forest and [The Cozy Web](/garden/the-cozy-web/index.md)
Digital Gardens are [Freeform](/garden/freeform) collections of information made by an individual or community
- Alternatives to [Chronological](/garden/chronological) personal blogs
- Exist in a middleground between the dark forest and [The Cozy Web](/garden/the-cozy-web)
[This Knowledge Hub](/garden/this-knowledge-hub/index.md) is a digital garden
[This Knowledge Hub](/garden/this-knowledge-hub) is a digital garden
Collections of digital gardens and resources for creating them:
- **[https://github.com/MaggieAppleton/digital-gardeners](https://github.com/MaggieAppleton/digital-gardeners)**

View file

@ -15,6 +15,6 @@ const pageData = useData();
<p>40 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/anarchism/index.md">Anarchism</a><a href="/garden/consensus-democracy/index.md">Consensus Democracy</a></details>
<details><summary>Referenced by:</summary><a href="/garden/anarchism">Anarchism</a><a href="/garden/consensus-democracy">Consensus Democracy</a></details>
A form of democracy where every voter gets to vote on every issue directly, and the majority rules. This form of voting is often criticized for having no safe guards to prevent a majority group from oppressing a minority group.

View file

@ -16,18 +16,18 @@ const pageData = useData();
<p>68 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/commune/index.md">Commune</a><a href="/garden/fedi-v2/index.md">Fedi v2</a><a href="/garden/weird/index.md">Weird</a></details>
<details><summary>Referenced by:</summary><a href="/garden/commune">Commune</a><a href="/garden/fedi-v2">Fedi v2</a><a href="/garden/weird">Weird</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized/index.md">Decentralized</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized">Decentralized</a></details>
Allow for validating one's identity without relying on a specific centralized server
Implementations:
- Private and public keypairs
- [IndieAuth](https://indieweb.org/IndieAuth) by [The IndieWeb](/garden/the-small-web/index.md)
- Supported by [Rauthy](https://github.com/sebadob/rauthy) which the [Commune](/garden/commune/index.md) community endorses
- [IndieAuth](https://indieweb.org/IndieAuth) by [The IndieWeb](/garden/the-small-web)
- Supported by [Rauthy](https://github.com/sebadob/rauthy) which the [Commune](/garden/commune) community endorses
Self hosted identity providers are NOT enough to be considered federated identity
- OIDC and OAuth require the service owner to have pre-configured with explicitly allowed identity providers
[Incremental Social](/garden/incremental-social/index.md) uses Zitadel which does NOT support IndieAuth and probably won't
[Incremental Social](/garden/incremental-social) uses Zitadel which does NOT support IndieAuth and probably won't

View file

@ -1,4 +1,5 @@
---
alias: "Agentic Fediverse"
public: "true"
slug: "fedi-v2"
title: "Fedi v2"
@ -11,16 +12,16 @@ import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">Fedi v2</h1>
<p>2566 words, ~14 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<p>3376 words, ~18 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/social-media/index.md">Social Media</a><a href="/garden/the-indieweb/signature-blocks/index.md">The IndieWeb/Signature Blocks</a><a href="/garden/weird/index.md">Weird</a></details>
<details><summary>Referenced by:</summary><a href="/garden/social-media">Social Media</a><a href="/garden/the-indieweb/signature-blocks">The IndieWeb/Signature Blocks</a><a href="/garden/weird">Weird</a></details>
A placeholder name for a theoretical new federated network that is client-centric, in contrast to the server-centric [Fediverse](/garden/fediverse/index.md). Many of the ideas here will be implemented as described or similarly by people much smarter than me as part of [Agentic Federation on Iroh](https://github.com/commune-os/weird/discussions/32), an initiative by the [Weird](/garden/weird/index.md) developers.
A placeholder name for a theoretical new federated network that is client-centric, in contrast to the server-centric [Fediverse](/garden/fediverse). Many of the ideas here will be implemented as described or similarly by people much smarter than me as part of [Agentic Federation on Iroh](https://github.com/commune-os/weird/discussions/32), an initiative by the [Weird](/garden/weird) developers.
## Motivation
The current fediverse, while in theory fully [Decentralized](/garden/decentralized/index.md), in practice suffers many of the issues associated with centralization. This is primarily caused by the friction of having to pick a server and the non feasibility of individuals buying a domain and setting up a single user instance - both of these causes lead to a handful of large servers with the bulk of the users. You can see this in action by looking up the relative sizes of lemmy and mastodon instances. [Single-user Mastodon Instance is a Bad Idea](https://mull.net/mastodon) goes over the non feasibility of self hosting and how it contributes to a handful of servers having the majority of the users.
The current fediverse, while in theory fully [Decentralized](/garden/decentralized), in practice suffers many of the issues associated with centralization. This is primarily caused by the friction of having to pick a server and the non feasibility of individuals buying a domain and setting up a single user instance - both of these causes lead to a handful of large servers with the bulk of the users. You can see this in action by looking up the relative sizes of lemmy and mastodon instances. [Single-user Mastodon Instance is a Bad Idea](https://mull.net/mastodon) goes over the non feasibility of self hosting and how it contributes to a handful of servers having the majority of the users.
The promise of federation is the ability to interact with the whole network, while being able to fully choose and customize how you yourself interact with the network. In practice though, clients are severely limited to what they can do based on the server software. Of particular note, Lemmy and Mastodon show content in different formats (threads vs microblogs), and no clients allow changing how they're displayed, or respecting the format of the source of the content. Clients also are unable to change sorting algorithms or how downvotes are handled - those are all dependent on the server. [A Plan for Social Media - Rethinking Federation](https://raphael.lullis.net/a-plan-for-social-media-less-fedi-more-webby/) similarly criticizes how much of the decisions are dependent on the server, which most people won't be able to or willing to self host.
@ -30,9 +31,11 @@ The pick a server problem is such a problem because not only do you have to pick
[ATProto](https://atproto.com/) by bluesky offers a version of federation built for a handful of large instances, but allowing smaller servers to be spun up that can implement custom sorting algorithms, views, etc. This fixes a couple of the problems where you're unable to change certain things dictated by the servers, but doesn't quite go far enough - and of particular note, you still have to associate your identity with a specific server.
[NextGraph](https://docs.nextgraph.org/en/introduction/) looks very similar to what we're trying to build. My only note is really that it gives a bit of a crypto vibe through decisions like calling identities "wallets" that I think may make it fall into the same problems nostr has, but conceptually its _really_ similar to everything discussed here, which is great! It should be incredibly easy to interoperate, at the very least.
<span id="66527b3e-58af-41c4-8345-5c0951e42f54"><h2>Identity</h2></span>
The new fediverse should have a fully [Decentralized Identity](/garden/federated-identity/index.md), where it's completely attached to the client rather than any server(s). This means you don't have to pick a server, worry about your chosen server going down, or that yout identity will become associated with an undesired community. It can properly allow you to engage in your variety of interests without having to associate any as core enough to attach your identity to.
The new fediverse should have a fully [Decentralized Identity](/garden/federated-identity), where it's completely attached to the client rather than any server(s). This means you don't have to pick a server, worry about your chosen server going down, or that yout identity will become associated with an undesired community. It can properly allow you to engage in your variety of interests without having to associate any as core enough to attach your identity to.
This identity can be accomplished by the client merely generating a private and public key, which can then be stored however the user pleases. Reasonably, there's be default options to back up the private key to Google drive or alternatives, as most users will not desire to go the extra effort of backing up their identity without relying on big storage websites. But for those who wish, you could keep the identity solely on the device, or choose your own method of storage and backup.
@ -64,7 +67,7 @@ Edit replies could be sent by people other than the original poster as well. Per
Groups/communities could also be specially flagged messages, effectively allowing for subreddit-style content. Posting to the community is just replying to the message. Subscribing to that community is just subscribing to that message. The original message creator can send edits to update stuff like the description of the community. Perhaps they can also send a message detailing other identities to trust for editing or moderating the community.
A bot could fairly easily be setup to make [IndieWeb](/garden/the-small-web/index.md) posts and web mentions use this protocol. Indeed, this protocol is very POSSE-friendly because you could have your original content on the website, and the messages can be spread across the network while allowing clients to verify it was untampered with and definitely came from that website. I plan on writing a proposal for IndieWeb posts to include [The IndieWeb/Signature Blocks](/garden/the-indieweb/signature-blocks/index.md) to enable this. Within this framework, Fedi v2 would not just be a other social media silo. It would be the source of truth, fully controlled by the author. Even if the author cross posts to other social media (silos), we'd effectively still be the original copy.
A bot could fairly easily be setup to make [IndieWeb](/garden/the-small-web) posts and web mentions use this protocol. Indeed, this protocol is very POSSE-friendly because you could have your original content on the website, and the messages can be spread across the network while allowing clients to verify it was untampered with and definitely came from that website. I plan on writing a proposal for IndieWeb posts to include [The IndieWeb/Signature Blocks](/garden/the-indieweb/signature-blocks) to enable this. Within this framework, Fedi v2 would not just be a other social media silo. It would be the source of truth, fully controlled by the author. Even if the author cross posts to other social media (silos), we'd effectively still be the original copy.
## Moderation
@ -98,6 +101,9 @@ Here's some initial ideas for components I currently plan on proposing and perha
- **Editors**: Describes a list of identities who have the power to edit this message, or accept edit requests to this message.
- **Deleters**: Describes a list of identities who have the power to delete this message, or accept deletion requests to this message.
- **No Discovery**: Marks that this message should not be included in any global feeds or search results. Servers should only send it to servers and clients that subscribe to messages like this one.
- **Timestamp Requested**: Marks that this message would like to receive a response from a trusted server (optionally defined in the component data) once it is delivered. May also include a schema ID that represents what the timestamp represents. Defaults to referring to the published date.
- **Calendar Event**: Describes a calendar event.
- **RSVP**: Marks that you [are, might be, or aren't] participating in whatever a linked entity is describing.
### Chatting
@ -111,10 +117,32 @@ Here are some of the components that could be used to represent a chat room:
The agentic fediverse could support sharing games using a Game component that includes a url or raw html required to play a game. In theory they could even support "cloud saves" by signing a message of their save data that only they can decrypt and sending it as a reply to the game message. Clients could handle displaying the game alongside the usual filtering and sorting features.
I'd also be excited in seeing a sort of MMO style game on the agentic fediverse. So you see other players and there's a shared game state, calculated on the client based on the actions recorded by the various different players. And since the rules would have to be defined by the components, people could create their own copies of the world (e.g. to play with a friend group or solo), or even make their own mods of the game. I'd like to look into that. I'll perhaps rethink [Chromatic Lattice](/garden/chromatic-lattice) to work on such a framework, although it may be too complicated for this idea.
Having the game state be calculatable by the client like that would also allow trophies and achievements to work verifiably. People could probably still write software to copy someone else's events at the right times and effectively replicate their save, but I think that won't happen commonly enough to matter.
## Local identity and contact management
If I have multiple apps that use the agentic fediverse (e.g. one for reddit like content, one for Twitter, discord, Google drive, etc.), I'd like to easily have them all use the same identity(s), as well as a shared contact list (so I know the person I saw do something on one app is the same as the person that did something on another app).
To that end, there should be an app/program that manages your identities and contacts on that device. It sets up your initial identity, any cloud backups, etc., and the other apps talk to it as needed. That could be sending it individual messages to sign or asking for a key that can be used to do limited functionality.
Contacts could be signed such that they're only readable by us, and then sent over the network so I can have multiple devices that keep their contact list synced between them
Contacts could be signed such that they're only readable by us, and then sent over the network so I can have multiple devices that keep their contact list synced between them
An identity management app could also work as a link handler for the `leaf` protocol. It could take a schema ID as another path component, which then describes the purpose of the URI and the expected remaining data in the URI. The identity management app can then pass the message along to any app that has specified it knows how to handle that schema.
## Sustainability
Servers are expensive, especially as they get popular. Most current fediverse instances are free and funded by donations. Things like ads or paying for an account are difficult to do due to the nature of federation. This is a pretty major problem because if a server becomes too expensive to host, it will shut down, along with all the accounts associated with it. Fedi v2 makes individual servers going down not be an issue anymore, since identities aren't attached to them. However, it's an issue if _all_ the instances go down, and if there's no way to pay for them still, why would _any_ instance stay up?
Since instance nodes do not have to do filtering, sorting, or really any other processing, but rather just serving the events and sending out notifications to clients, the cost will be cheaper than the current fediverse. It's really just a file server, which is cheap. For example, idrive charges $40 per tb per year, which is enough for a LOT of content. So I expect some instance nodes to have fairly generous free tiers that will suffice for a lot of users. Idrive also doesn't have egress charges, so the cost only scales with how much content is being published, not downloaded.
For power users, instance nodes could accept payments to store data above the free quota. This would likely most often happen for people wishing to upload high resolution images or videos. A user could also switch nodes after filling a quota on one node - you don't have to delete your content on the old instance. You could also do this to backup your content on multiple nodes (although you should also keep a local copy of all your content).
I assume this aspect of Fedi v2 will be the most controversial - people really like free services, and are expecting it. Knowing they might eventually need to pay to post more will perhaps require a cultural shift. I think it's worth it to not have ads or tracking, and in general we should be supporting sustainable services.
## What about Incremental Social?
Well, the agentic fediverse is a long ways out. But eventually I'd probably like to replace mbin with an Iroh node and ActivityPub <=> Fedi v2 bridge, transitioning all existing accounts.
The bridge would work by looking for a signed message to register a handle on the server. If that handle is not taken nor reserved, the AP actor is created and posts from the identity get bridged. The actor will have some metadata linking to the Fedi v2 identity, thus allowing both fediverses to know the identities are linked.

View file

@ -16,15 +16,15 @@ const pageData = useData();
<p>29 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/activitypub/index.md">ActivityPub</a><a href="/garden/atproto/index.md">ATProto</a><a href="/garden/decentralized/index.md">Decentralized</a><a href="/garden/fedi-v2/index.md">Fedi v2</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/mbin/index.md">Mbin</a><a href="/garden/nostr/index.md">Nostr</a><a href="/garden/social-media/index.md">Social Media</a><a href="/garden/the-small-web/index.md">The Small Web</a><a href="/garden/weird/index.md">Weird</a></details>
<details><summary>Referenced by:</summary><a href="/garden/activitypub">ActivityPub</a><a href="/garden/atproto">ATProto</a><a href="/garden/decentralized">Decentralized</a><a href="/garden/fedi-v2">Fedi v2</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/mbin">Mbin</a><a href="/garden/nostr">Nostr</a><a href="/garden/social-media">Social Media</a><a href="/garden/the-small-web">The Small Web</a><a href="/garden/weird">Weird</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized/index.md">Decentralized</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized">Decentralized</a></details>
A collection of [Social Media](/garden/social-media/index.md) websites that can all talk to each other by virtue of a shared protocol
A collection of [Social Media](/garden/social-media) websites that can all talk to each other by virtue of a shared protocol
Typically refers to sites implementing [ActivityPub](/garden/activitypub/index.md)
Typically refers to sites implementing [ActivityPub](/garden/activitypub)
Implementations:
- [ActivityPub](/garden/activitypub/index.md)
- [ATProto](/garden/atproto/index.md)
- [Nostr](/garden/nostr/index.md)
- [ActivityPub](/garden/activitypub)
- [ATProto](/garden/atproto)
- [Nostr](/garden/nostr)

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>5 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social/index.md">Incremental Social</a></details>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social">Incremental Social</a></details>
[Forgejo](https://forgejo.org) is an [Open Source](/garden/open-source/index.md) code repository hosting software
[Forgejo](https://forgejo.org) is an [Open Source](/garden/open-source) code repository hosting software

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>10 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/chronological/index.md">Chronological</a><a href="/garden/freeform/index.md">Freeform</a></details>
<details><summary>Referenced by:</summary><a href="/garden/chronological">Chronological</a><a href="/garden/freeform">Freeform</a></details>
Describes a dichotomy between displaying information in a [Freeform](/garden/freeform/index.md) vs [Chronological](/garden/chronological/index.md) manner
Describes a dichotomy between displaying information in a [Freeform](/garden/freeform) vs [Chronological](/garden/chronological) manner

View file

@ -14,13 +14,13 @@ const pageData = useData();
<p>46 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/commune/index.md">Commune</a><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/freeform-vs-chronological-dichotomy/index.md">Freeform vs Chronological Dichotomy</a><a href="/garden/garden-rss/index.md">Garden-RSS</a></details>
<details><summary>Referenced by:</summary><a href="/garden/commune">Commune</a><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/freeform-vs-chronological-dichotomy">Freeform vs Chronological Dichotomy</a><a href="/garden/garden-rss">Garden-RSS</a></details>
A collection of information that is not tied to when it was created or edited
Part of the [Freeform vs Chronological Dichotomy](/garden/freeform-vs-chronological-dichotomy/index.md)
Part of the [Freeform vs Chronological Dichotomy](/garden/freeform-vs-chronological-dichotomy)
Anything wiki-style is considered freeform
- A collection of living documents
[Garden-RSS](/garden/garden-rss/index.md), a theoretical alternative to RSS that's better for freeform content
[Garden-RSS](/garden/garden-rss), a theoretical alternative to RSS that's better for freeform content

View file

@ -15,12 +15,12 @@ const pageData = useData();
<p>34 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
Play it [here](https://thepaperpilot.org/gamedevtree)!
My first (good) incremental game! (My actual first was [Shape Tycoon](https://thepaperpilot.itch.io/shape-tycoon) - I don't recommend it!)
It's [Open Source](/garden/open-source/index.md)!
It's [Open Source](/garden/open-source)!
The [TV Tropes](https://tvtropes.org/pmwiki/pmwiki.php/VideoGame/TheGameDevTree) page on this game mentions some of the cool things about this game

View file

@ -14,13 +14,13 @@ const pageData = useData();
<p>59 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/freeform/index.md">Freeform</a><a href="/garden/the-small-web/index.md">The Small Web</a><a href="/garden/this-knowledge-hub/index.md">This Knowledge Hub</a></details>
<details><summary>Referenced by:</summary><a href="/garden/freeform">Freeform</a><a href="/garden/the-small-web">The Small Web</a><a href="/garden/this-knowledge-hub">This Knowledge Hub</a></details>
A theoretical alternative to RSS that's better for [Freeform](/garden/freeform/index.md) websites (and [Digital Gardens](/garden/digital-gardens/index.md) specifically )
A theoretical alternative to RSS that's better for [Freeform](/garden/freeform) websites (and [Digital Gardens](/garden/digital-gardens) specifically )
Why is it useful?
- [Feeds are not fit for gardening](https://v5.chriskrycho.com/essays/feeds-are-not-fit-for-gardening/)
- Describes the issues with RSS for [Digital Gardens](/garden/digital-gardens/index.md)
- Describes the issues with RSS for [Digital Gardens](/garden/digital-gardens)
- Proposes creating an alternative, which they call `grdn`
How should it work?

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>636 words, ~3 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/my-political-journey">My Political Journey</a><a href="/now/index">/now</a><a href="/garden/social-media">Social Media</a></details>
There are a lot of developers in the incremental games community - the genre seems to draw them in, and convert a lot of players _into_ developers. Let's explore the reasons why this genre appeals to developers.
@ -36,6 +36,6 @@ Having your games be played can be incredibly motivating, and the community make
## Monetization
I'd like to clarify that everything I've said above mainly applies to _web-based incrementals_. Incremental games are also _incredibly_ popular on mobile, but with a much different culture and community. Many mobile gamers will still participate in the web-focused community _for_ the culture. This web-focused community has a culture that has been criticized for being "anti-monetization". Ads, IAPs, and similar forms of monetization are often criticized, mainly due to the abundance of completely non-monetized games available from hobbyist developers. There are exceptions, like paid games often being considered fine, like Increlution or Stuck in Time, or donation ware games like kittens game, but even popular games that have IAP see some level of regular criticism, like NGU Idle, Idle Skilling, or Idle Pins. A large part of this can be explained by the community being hyper-aware of the [addictive](/garden/guide-to-incrementals/appeal-to-players/index.md#665ceed1-72a9-49f2-9215-dd690f89aee3)) nature of this genre and its susceptibility to exploiting players.
I'd like to clarify that everything I've said above mainly applies to _web-based incrementals_. Incremental games are also _incredibly_ popular on mobile, but with a much different culture and community. Many mobile gamers will still participate in the web-focused community _for_ the culture. This web-focused community has a culture that has been criticized for being "anti-monetization". Ads, IAPs, and similar forms of monetization are often criticized, mainly due to the abundance of completely non-monetized games available from hobbyist developers. There are exceptions, like paid games often being considered fine, like Increlution or Stuck in Time, or donation ware games like kittens game, but even popular games that have IAP see some level of regular criticism, like NGU Idle, Idle Skilling, or Idle Pins. A large part of this can be explained by the community being hyper-aware of the [addictive](/garden/guide-to-incrementals/appeal-to-players#665ceed1-72a9-49f2-9215-dd690f89aee3)) nature of this genre and its susceptibility to exploiting players.
On mobile, however, monetization is the norm and expected. If an incremental game is available on mobile, it almost _certainly_ will be monetized, and mobile players are aware and accepting of that. Mobile incremental games, due to their addictive nature, tend to make a _lot_ of money. It's very lucrative, and therefore these games are quite abundant on mobile storefronts.

View file

@ -14,13 +14,13 @@ const pageData = useData();
<p>2166 words, ~12 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/my-political-journey">My Political Journey</a><a href="/now/index">/now</a><a href="/garden/social-media">Social Media</a></details>
This is something that has been discussed and analyzed by many people, and to some extent, I feel like everything that can be said on the topic already has. However, a lot of these analyses are from the perspective of those with not as much experience and involvement within the genre as I'd argue would be necessary for a fully contextualized answer. I'm interested in ludology and part of that includes interpreting games as art, and to that end what constitutes a game, let alone a "good game". Incremental games are oft criticized, unfairly in my biased opinion, of not even constituting games, such as was posited by [this polygon article](https://www.polygon.com/2013/9/30/4786780/the-cult-of-the-cookie-clicker-when-is-a-game-not-a-game).
## Numbers Going Up
This is a very common response to why people enjoy incremental games, although it's not one I find compels me personally, and I suspect it might be a stand-in for [progression](/garden/guide-to-incrementals/appeal-to-players/index.md#665ceed1-704e-4cd0-8263-9a1756b09f4a)) or [Guide to Incrementals/What is Content?](/garden/guide-to-incrementals/what-is-content/index.md). But reportedly, some people do just like _seeing_ big numbers. I must reiterate I suspect the actual cause is seeing big numbers _in context_ though - if you start at 1e1000 of a currency and get to 1e1001, that isn't going to feel as satisfying as going from 1e10 to 1e100, and in any case, I don't think a button that just adds a zero to your number will feel quite satisfying - I believe its the sense of having made progress, and comparing where you are to where you started and feeling like you've earned your way here that is enjoyable.
This is a very common response to why people enjoy incremental games, although it's not one I find compels me personally, and I suspect it might be a stand-in for [progression](/garden/guide-to-incrementals/appeal-to-players#665ceed1-704e-4cd0-8263-9a1756b09f4a)) or [Guide to Incrementals/What is Content?](/garden/guide-to-incrementals/what-is-content). But reportedly, some people do just like _seeing_ big numbers. I must reiterate I suspect the actual cause is seeing big numbers _in context_ though - if you start at 1e1000 of a currency and get to 1e1001, that isn't going to feel as satisfying as going from 1e10 to 1e100, and in any case, I don't think a button that just adds a zero to your number will feel quite satisfying - I believe its the sense of having made progress, and comparing where you are to where you started and feeling like you've earned your way here that is enjoyable.
<span id="665ceed1-704e-4cd0-8263-9a1756b09f4a"><h2>Progression</h2></span>
@ -36,7 +36,7 @@ If you look at the higher-level play of most games, you'll see them perform diff
<span id="665ceed1-72a9-49f2-9215-dd690f89aee3"><h2>Addiction</h2></span>
A lot of these reasons for why incremental games appeal may have reminded you of why _gambling_ appeals to people, particularly those prone to addiction. Indeed, incremental games are quite often criticized for their similarity to a [skinner box](https://www.youtube.com/watch?v=tWtvrPTbQ_c). Some have gone as far as to say incremental games as a genre are commenting that [all games are skinner boxes](/garden/guide-to-incrementals/defining-the-genre/index.md#665cea25-b1e5-40bc-8c82-2296982ce1d1)). The argument goes that some games are not fun, but rather condition players into continuing to play without actually getting anything from the experience. When tied to real-world money this is seen as predatory, and to a lesser extent, even free games may be feeding the addictive sides of people and making them more prone to seek out gambling or micro-transaction heavy games.
A lot of these reasons for why incremental games appeal may have reminded you of why _gambling_ appeals to people, particularly those prone to addiction. Indeed, incremental games are quite often criticized for their similarity to a [skinner box](https://www.youtube.com/watch?v=tWtvrPTbQ_c). Some have gone as far as to say incremental games as a genre are commenting that [all games are skinner boxes](/garden/guide-to-incrementals/defining-the-genre#665cea25-b1e5-40bc-8c82-2296982ce1d1)). The argument goes that some games are not fun, but rather condition players into continuing to play without actually getting anything from the experience. When tied to real-world money this is seen as predatory, and to a lesser extent, even free games may be feeding the addictive sides of people and making them more prone to seek out gambling or micro-transaction heavy games.
> While incremental games can be fun and even healthy in certain contexts, they can exacerbate video game addiction more than other genres. If you feel like playing incremental games is taking priority over other things in your life, or manipulating your sleep schedule, it may be prudent to seek help. See [r/StopGaming](https://www.reddit.com/r/StopGaming) for resources.
@ -44,7 +44,7 @@ Since incremental games are often built on extrinsic motivations in the form of
## Strategy
Incremental games could be considered a subset of [strategy games](/garden/guide-to-incrementals/defining-the-genre/index.md#665cea25-437a-49a4-8445-00422fb9ded1)), and inherit the appeals of strategy games. This includes the appeal of feeling like you've found a good solution to a puzzle, or that you're learning more about the game and are improving at making decisions within it.
Incremental games could be considered a subset of [strategy games](/garden/guide-to-incrementals/defining-the-genre#665cea25-437a-49a4-8445-00422fb9ded1)), and inherit the appeals of strategy games. This includes the appeal of feeling like you've found a good solution to a puzzle, or that you're learning more about the game and are improving at making decisions within it.
Note that strategy games are not all the same difficulty, as well. Cookie Clicker is probably easier than Starcraft 2 (although late game may beg to differ). Plenty of incremental games can be used as evidence that "easier" strategies may have their separate appeal to harder strategy games - players like to feel smart and that they figured the game out and have optimized or mastered it, and the game being easier doesn't detract from that sense of accomplishment as much as it allows more and more users to be able to reach the point where they gain that sense.
@ -66,6 +66,6 @@ To bring the conversation back to incrementals, I'm _incredibly_ opinionated on
The discussion of whether video games are art has resulted in a pretty universal "yes, they are", but with some games the argument may still crop up. The reason why Incremental games are sometimes questioned is due to their perceived lack of complexity. However, even setting aside the fact that if players are having fun then it's not time wasted, I think games can have artistic merit that supersedes the necessity of having (any / engaging / "deep") gameplay. Incremental games are no less legitimate of a game or the "art" label because of any lack perceived lack of depth. For what it's worth, most art can be consumed with more ease than any video game - any painting, movie, sculpture, etc.
A lot of incrementals have a narrative context that can similarly qualify them as art. Cookie Clicker is, as has been pointed out numerous times before, commenting on excess and increasing production beyond any reasonable limits - devolving into increasing production for its own sake. Indeed, a lot of incremental games are written to comment upon various concepts like capitalism or tropes in games, as discussed when [defining Incrementals](/garden/guide-to-incrementals/defining-the-genre/index.md#665cea25-b1e5-40bc-8c82-2296982ce1d1)). However, I'd like to argue _most_ incremental games are still art, even without any narrative context. "Art" as a concept is pretty nebulous already, but I personally like those who define it as an act of expression more than any physical result. The creator and the context within which they created the art, and any meaning they put into it, are all relevant and a part of the art itself. Most incremental games have artistic merit from things like _why_ the creator made it, why they chose to make it an incremental game, and why they made any particular design decision. Hell, even if you play through an entire incremental game without a single thought or feeling, that very fact it elicited nothing can itself be artistic merit!
A lot of incrementals have a narrative context that can similarly qualify them as art. Cookie Clicker is, as has been pointed out numerous times before, commenting on excess and increasing production beyond any reasonable limits - devolving into increasing production for its own sake. Indeed, a lot of incremental games are written to comment upon various concepts like capitalism or tropes in games, as discussed when [defining Incrementals](/garden/guide-to-incrementals/defining-the-genre#665cea25-b1e5-40bc-8c82-2296982ce1d1)). However, I'd like to argue _most_ incremental games are still art, even without any narrative context. "Art" as a concept is pretty nebulous already, but I personally like those who define it as an act of expression more than any physical result. The creator and the context within which they created the art, and any meaning they put into it, are all relevant and a part of the art itself. Most incremental games have artistic merit from things like _why_ the creator made it, why they chose to make it an incremental game, and why they made any particular design decision. Hell, even if you play through an entire incremental game without a single thought or feeling, that very fact it elicited nothing can itself be artistic merit!
I'm not an art major, and I may be taking a somewhat extreme take on what is art and what has artistic merit, but I'd argue the overall point stands that games, and incremental games specifically, _can_ have artistic merit, which appeals to many gamers.

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>3429 words, ~19 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/my-political-journey">My Political Journey</a><a href="/now/index">/now</a><a href="/garden/social-media">Social Media</a></details>
Video games are placed into genres for a variety of reasons. They can give a mental shorthand to set the player's expectations up, they can help a game market itself by its similarities to other, already popular games, and honestly, people just love categorization for its own sake. For this guide, it's important to define the genre so it is clear what games it's even talking about.
@ -40,7 +40,7 @@ To understand what that means, think of how a casino uses skinner boxes to emoti
> While incremental games can be fun and even healthy in certain contexts, they can exacerbate video game addiction more than other genres. If you feel like playing incremental games is taking priority over other things in your life, or manipulating your sleep schedule, it may be prudent to seek help. See [r/StopGaming](https://www.reddit.com/r/StopGaming) for resources.
This "undressing" tends to go hand in hand with a reduced focus on aesthetics, often just printing the game state directly to the screen as text. This makes incremental games much easier to develop, particularly for those with programming skills but not art skills, but that's a tangent for why Incremental Games [Guide to Incrementals/Appeal to Developers](/garden/guide-to-incrementals/appeal-to-developers/index.md).
This "undressing" tends to go hand in hand with a reduced focus on aesthetics, often just printing the game state directly to the screen as text. This makes incremental games much easier to develop, particularly for those with programming skills but not art skills, but that's a tangent for why Incremental Games [Guide to Incrementals/Appeal to Developers](/garden/guide-to-incrementals/appeal-to-developers).
Before I continue, I'd like to make my stance clear that I love games and incremental games, and do not think they should be considered inherently bad or manipulative with the above logic. Skinner boxes are just a way of manipulating behavior _via rewards_. The games are still fun - that's the reward! I'd believe the real criticism here is that it is "empty fun", or "empty dopamine", that doesn't offer any additional value or sense of fulfillment. I don't think that's inherently bad in moderation, although it can become a problem if the game is manipulating you for profit-seeking, or if you play the game to the detriment of the other parts of your life.
@ -101,7 +101,7 @@ I chose a variety of games here, biasing towards newer games, purposefully to av
The Paradigm Shift is probably the _highest_ possible value factor for an incremental. It's so common that for a while people referred to incrementals that exhibit this trait as "unfolding" games, to the point of trying to _replace_ the term incremental due to their popularity. Paradigm shifts refer to when the gameplay significantly changes. There are too many examples to list here, but notably, every single reset mechanic is typically going to be a paradigm shift. Examples of games with paradigm shifts that _aren't_ tied to reset mechanics include [Universal Paperclips](https://www.decisionproblem.com/paperclips/) and [A Dark Room](http://adarkroom.doublespeakgames.com/).
There are many reasons for the appeal of paradigm shifts. Oftentimes each mechanic builds on top of the existing mechanics, increasing the complexity of the game in steps so the player can follow along. They provide a sense of mystery, with the player anticipating what will happen next. They shake up the gameplay before it gets too stale - allowing the game to entertain for longer before the sense of [Guide to Incrementals/What is Content?](/garden/guide-to-incrementals/what-is-content/index.md) dissipates. Of the canon games selected above, I would argue _every single one_ contains a paradigm shift (although I could see someone disagreeing with that statement wrt Increlution).
There are many reasons for the appeal of paradigm shifts. Oftentimes each mechanic builds on top of the existing mechanics, increasing the complexity of the game in steps so the player can follow along. They provide a sense of mystery, with the player anticipating what will happen next. They shake up the gameplay before it gets too stale - allowing the game to entertain for longer before the sense of [Guide to Incrementals/What is Content?](/garden/guide-to-incrementals/what-is-content) dissipates. Of the canon games selected above, I would argue _every single one_ contains a paradigm shift (although I could see someone disagreeing with that statement wrt Increlution).
I should take a moment to say that while I'm hyping up this specific factor, we cannot just reduce the genre definition to "does it have paradigm shifts". Many games have paradigm shifts that are not incremental, so it's just an _indicator_ of incrementalness. Additionally, it can become quite hard to determine how large of a shift is a "paradigm" shift. Take, for example, any game with a skill tree. In some games, each skill node might have a large impact on how you play with the game, and qualify as a paradigm shift for some players. In other games, each skill node might just be a small percentage modifier on some stat that doesn't really impact much more than a slight bias towards an already established mechanic that's newly buffed. Every single canon game may show that it's common amongst incremental games, but could just as easily indicate that they're common in games in general.

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>230 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/my-personal-website/index.md">My Personal Website</a></details>
<details><summary>Referenced by:</summary><a href="/garden/my-personal-website">My Personal Website</a></details>
This is a comprehensive guide to Incremental Games, a genre of video games. It will explore defining the genre, why it's appealing, and how to design and build your own incremental game. Along the way will be ~~interactive examples~~, snippets from other creators, and relevant material to contextualize everything.
@ -22,15 +22,15 @@ This is a comprehensive guide to Incremental Games, a genre of video games. It w
## Why am I making this?
That's a good question! What authority do I have to be making this guide? I haven't made the best incremental games, nor the most incremental games, certainly not the most popular ones either. But I do have some formal education in game development, know a lot of incremental game devs (as well as other game devs), and have a passionate interest in ludology, classifying genres, etc. I've also made [a couple of incremental games](/garden/my-projects/index.md)) myself.
That's a good question! What authority do I have to be making this guide? I haven't made the best incremental games, nor the most incremental games, certainly not the most popular ones either. But I do have some formal education in game development, know a lot of incremental game devs (as well as other game devs), and have a passionate interest in ludology, classifying genres, etc. I've also made [a couple of incremental games](/garden/my-projects)) myself.
If you have any additional questions about my credentials or anything on this site, feel free to reach out!
## Ludology
- [Guide to Incrementals/Defining the Genre](/garden/guide-to-incrementals/defining-the-genre/index.md)
- [Guide to Incrementals/Appeal to Players](/garden/guide-to-incrementals/appeal-to-players/index.md)
- [Guide to Incrementals/Appeal to Developers](/garden/guide-to-incrementals/appeal-to-developers/index.md)
- [Guide to Incrementals/What is Content?](/garden/guide-to-incrementals/what-is-content/index.md)
- [Guide to Incrementals/Defining the Genre](/garden/guide-to-incrementals/defining-the-genre)
- [Guide to Incrementals/Appeal to Players](/garden/guide-to-incrementals/appeal-to-players)
- [Guide to Incrementals/Appeal to Developers](/garden/guide-to-incrementals/appeal-to-developers)
- [Guide to Incrementals/What is Content?](/garden/guide-to-incrementals/what-is-content)
## Making an Incremental
- [Guide to Incrementals/Navigating Criticism](/garden/guide-to-incrementals/navigating-criticism/index.md)
- [Guide to Incrementals/Navigating Criticism](/garden/guide-to-incrementals/navigating-criticism)

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>747 words, ~4 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/my-political-journey">My Political Journey</a><a href="/now/index">/now</a><a href="/garden/social-media">Social Media</a></details>
Developing games is fun and exciting and teaches a lot of wonderful skills - I enthusiastically encourage anyone with an interest in game development to try it out - and incremental games are a wonderful way to get started. However, there are many challenges young and inexperienced developers have to face, and I think the hardest one - harder than coding, debugging, balancing, etc. - is handling criticism. When you put your heart and soul into a game it is natural to feel very vulnerable. While I think there's a lot communities can do to ensure they're welcoming, positive and constructive with their criticisms, inevitably you will eventually read some, and potentially a lot, of comments that can deeply affect you. No one is immune to this, from young incremental game developers to the largest content creators you can think of. That's why it's important to be able to process and navigate criticism, because ultimately collecting feedback is essential to the journey to becoming a better developer. On this page, we'll explore how to embrace criticism, grow from it, and continue to post your games publicly with confidence.

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>2272 words, ~12 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/my-political-journey">My Political Journey</a><a href="/now/index">/now</a><a href="/garden/social-media">Social Media</a></details>
If you've been in the incremental games community for any amount of time, you'll quickly find the number one thing players want is _content_. They want as much of it as possible! The most popular incremental games have tons of content, so they just keep stretching on and on and on, introducing mechanic after mechanic, and players love it. In fact, players seem to value the _amount_ of content over the quality of any _specific_ content. However, there's a bit of a lack of understanding concerning _what_ content is, and I'd like to explore what counts as content, and how we measure it. As a baseline definition, I think "content" can just be described as the parts of the game that engage the player, but to truly understand it we need to contextualize what that means and how it affects the gameplay experience.
@ -30,7 +30,7 @@ Let's take a look at the opposite end of the spectrum - interaction that is so f
### Repeatable Purchases
Imagine an entity in a game that you can purchase multiple times, each time it performs the same thing but for a higher cost. These are incredibly common, from the buildings in [cookie clicker](https://orteil.dashnet.org/cookieclicker/) to the units in [swarm sim](https://www.swarmsim.com/) to the IP and EP multipliers in [antimatter dimensions](https://ivark.github.io/). However, how much content is each specific purchase? Is it content beyond the first purchase? Does it have diminishing returns? What if you are oscillating between two different repeatable purchases? How much content is lost when you [automate](/garden/guide-to-incrementals/what-is-content/index.md#665cf570-e3d3-48f6-9fde-aa94e68a8682)) away a repeatable purchase?
Imagine an entity in a game that you can purchase multiple times, each time it performs the same thing but for a higher cost. These are incredibly common, from the buildings in [cookie clicker](https://orteil.dashnet.org/cookieclicker/) to the units in [swarm sim](https://www.swarmsim.com/) to the IP and EP multipliers in [antimatter dimensions](https://ivark.github.io/). However, how much content is each specific purchase? Is it content beyond the first purchase? Does it have diminishing returns? What if you are oscillating between two different repeatable purchases? How much content is lost when you [automate](/garden/guide-to-incrementals/what-is-content#665cf570-e3d3-48f6-9fde-aa94e68a8682)) away a repeatable purchase?
I don't want to take too harsh a stance against repeatable purchases. They're useful tools and can be used in a myriad of interesting ways. I feel they do become "stale" or less meaningful content over time, and this happens exponentially quickly the more frequently it can be purchased. A classic example that I believe goes too far is the IP/EP multipliers in Antimatter Dimensions. I would go as far as to say they are a chore and do not provide any meaningful content after you've bought them a couple of times. It's a method for inflating numbers (effectively making every OOM a 5x step instead of 10x), that punishes the player progression-wise whenever they forget to max it again, and eventually gets automated away as a _reward_ to the player for making enough progress.
@ -56,12 +56,12 @@ A recent example is [Really Grass Cutting Incremental](https://mrredshark77.gith
## Ending the Game
Incremental games do not often have a planned out narrative or ending,, such that each content update is approached as its own unit of narrative and gameplay. This prevents content updates from wrapping up the game nicely - it always has to leave something open for another content layer; be it another mechanic, reset layer, etc. This cycle will continue until the updates just stop, at which point the game will just have an unsatisfying conclusion that will never get the next thing it was supposed to be leading into. This reminds me of a Leonardo Da Vinci quote about how [Art is Never Complete](/garden/art-is-never-complete/index.md):
Incremental games do not often have a planned out narrative or ending,, such that each content update is approached as its own unit of narrative and gameplay. This prevents content updates from wrapping up the game nicely - it always has to leave something open for another content layer; be it another mechanic, reset layer, etc. This cycle will continue until the updates just stop, at which point the game will just have an unsatisfying conclusion that will never get the next thing it was supposed to be leading into. This reminds me of a Leonardo Da Vinci quote about how [Art is Never Complete](/garden/art-is-never-complete):
> Art is never finished, only abandoned.
> \- Leonardo Da Vinci
For what its worth, there are exceptions here (including several of [My Projects](/garden/my-projects/index.md)). I believe this practice is actually fairly reasonable, considering how many incremental game developers are learning game design and programming - keeping the scope small and expanding if it still interests you is a great way to keep learning without letting things like perfectionism or sunk cost fallacies get in the way.
For what its worth, there are exceptions here (including several of [My Projects](/garden/my-projects)). I believe this practice is actually fairly reasonable, considering how many incremental game developers are learning game design and programming - keeping the scope small and expanding if it still interests you is a great way to keep learning without letting things like perfectionism or sunk cost fallacies get in the way.
## Tips for Developers

View file

@ -14,12 +14,12 @@ const pageData = useData();
<p>20 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/federated-identity/index.md">Federated Identity</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/now/index">/now</a><a href="/garden/webrings/index.md">Webrings</a></details>
<details><summary>Referenced by:</summary><a href="/garden/federated-identity">Federated Identity</a><a href="/garden/my-personal-website">My Personal Website</a><a href="/now/index">/now</a><a href="/garden/webrings">Webrings</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
[Incremental Social](https://incremental.social/) is a [Fediverse](/garden/fediverse/index.md) website hosted by me!
[Incremental Social](https://incremental.social/) is a [Fediverse](/garden/fediverse) website hosted by me!
Made explicitly for the incremental games community
Most notably hosts an instance of [Mbin](/garden/mbin/index.md), [Forgejo](/garden/forgejo/index.md), and [Synapse](/garden/synapse/index.md) (and [Cinny](/garden/cinny/index.md))
Most notably hosts an instance of [Mbin](/garden/mbin), [Forgejo](/garden/forgejo), and [Synapse](/garden/synapse) (and [Cinny](/garden/cinny))

View file

@ -14,10 +14,10 @@ const pageData = useData();
<p>194 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/local-communities/index.md">Local Communities</a><a href="/garden/neoliberalism/index.md">Neoliberalism</a></details>
<details><summary>Referenced by:</summary><a href="/garden/local-communities">Local Communities</a><a href="/garden/neoliberalism">Neoliberalism</a></details>
Individualism is a value system centered around independence and self sufficiency. It argues for taking care of oneself before others, and that it's wrong for people to be forced to take care of others before their selves, i.e. via wealth redistribution. This value system is antithetical to the [Anarchist](/garden/anarchism/index.md) values of community and mutual aid. I personally am against individualism and see it as against humans nature of cooperation. We're a social people and have for our entire existence relied upon each other.
Individualism is a value system centered around independence and self sufficiency. It argues for taking care of oneself before others, and that it's wrong for people to be forced to take care of others before their selves, i.e. via wealth redistribution. This value system is antithetical to the [Anarchist](/garden/anarchism) values of community and mutual aid. I personally am against individualism and see it as against humans nature of cooperation. We're a social people and have for our entire existence relied upon each other.
As a personal anecdote, I'm a recent parent and the whole "it takes a village" adage makes a lot of sense, and has made me hyper aware of how individualism has made it very hard to raise a kid these days. There's no 3 generations living in a house anymore, and suburbs are spread out and isolating, preventing strong [Local Communities](/garden/local-communities/index.md) from forming. To sum up, the "village" doesn't exist anymore.
As a personal anecdote, I'm a recent parent and the whole "it takes a village" adage makes a lot of sense, and has made me hyper aware of how individualism has made it very hard to raise a kid these days. There's no 3 generations living in a house anymore, and suburbs are spread out and isolating, preventing strong [Local Communities](/garden/local-communities) from forming. To sum up, the "village" doesn't exist anymore.
Hyper individualism is a modern invention, not a "good ole traditional value" we should all aspire to. It was explicitly created by capitalist values, and replaced pre-existing value systems that prioritized co-operation.

View file

@ -15,8 +15,8 @@ const pageData = useData();
<p>6 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/davey-wreden/index.md">Davey Wreden</a><a href="/garden/wanderstop/index.md">Wanderstop</a></details>
<details><summary>Referenced by:</summary><a href="/garden/davey-wreden">Davey Wreden</a><a href="/garden/wanderstop">Wanderstop</a></details>
<details><summary>Tags:</summary><a href="/garden/davey-wreden/index.md">Davey Wreden</a></details>
<details><summary>Tags:</summary><a href="/garden/davey-wreden">Davey Wreden</a></details>
[Ivy Road](https://www.ivyroad.fun/) is a indie game studio created by [Davey Wreden](/garden/davey-wreden/index.md), Karla Kimonja, and C418
[Ivy Road](https://www.ivyroad.fun/) is a indie game studio created by [Davey Wreden](/garden/davey-wreden), Karla Kimonja, and C418

View file

@ -15,9 +15,9 @@ const pageData = useData();
<p>60 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/v-ecs/index.md">V-ecs</a></details>
<details><summary>Referenced by:</summary><a href="/now/index">/now</a><a href="/garden/v-ecs">V-ecs</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a><a href="/garden/profectus/index.md">Profectus</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a><a href="/garden/profectus">Profectus</a></details>
My largest and most ambitious incremental game I've ever made
- A magnum opus, of sorts ;P

View file

@ -66,4 +66,4 @@ This game was announced to come with premium editions that get access to the gam
- I think this is a horrible anti-consumer practice
- Since this game is narratively driven, it is prone to spoilers
- They're getting more money from their biggest fans without providing any tangible value to them, other than making the experience worse for everyone who plays but doesn't cough up that money
- I hope other narratively driven games find other ways to do [Video Game Monetization](/garden/video-game-monetization/index.md)
- I hope other narratively driven games find other ways to do [Video Game Monetization](/garden/video-game-monetization)

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>302 words, ~2 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/anarchism/index.md">Anarchism</a><a href="/garden/individualism/index.md">Individualism</a></details>
<details><summary>Referenced by:</summary><a href="/garden/anarchism">Anarchism</a><a href="/garden/individualism">Individualism</a></details>
Strongly connected local communities are important to have. They satisfy our social needs for in-person connections, and help organize mutual aid. These needs cannot be sufficiently satisfied exclusively by online friends/communities - of particular note, new parents need help raising their kid.
@ -22,6 +22,6 @@ Historically, society has had these strongly connected local communities, via th
The religious aspect of churches was never a requirement for the benefits they contributed to their local communities, and in fact there are mega-churches today that do not confer these benefits despite retaining the religious aspect.
There are several reasons for why local communities have since weakened. The car has weakened them by making the people physically more spread out and reducing the number of "third places". The internet created a convenient alternative whose communities were not immediately recognized as insufficient imitations of in person communities. Newer generations trend towards irreligiousness, making churches decreasingly popular. Combined, these changes have led to a cultural shift towards [Individualism](/garden/individualism/index.md) and [Neoliberalism](/garden/neoliberalism/index.md) that has further cemented our weakened local communities.
There are several reasons for why local communities have since weakened. The car has weakened them by making the people physically more spread out and reducing the number of "third places". The internet created a convenient alternative whose communities were not immediately recognized as insufficient imitations of in person communities. Newer generations trend towards irreligiousness, making churches decreasingly popular. Combined, these changes have led to a cultural shift towards [Individualism](/garden/individualism) and [Neoliberalism](/garden/neoliberalism) that has further cemented our weakened local communities.
The way to "fix" our local communities and make them more strongly connected is to support multi-generational households, increasing population density, and using or creating entities that can replace the community-building role of the church. Such alternatives could be community centers or HOAs. HOAs get a bad reputation due to their tendency to attract those who want power to micro-manage the community, but there are ways to organize them to mitigate that issue (see [Anarchism](/garden/anarchism/index.md)).
The way to "fix" our local communities and make them more strongly connected is to support multi-generational households, increasing population density, and using or creating entities that can replace the community-building role of the church. Such alternatives could be community centers or HOAs. HOAs get a bad reputation due to their tendency to attract those who want power to micro-manage the community, but there are ways to organize them to mitigate that issue (see [Anarchism](/garden/anarchism)).

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>3 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/command-palettes/index.md">Command Palettes</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/this-knowledge-hub/index.md">This Knowledge Hub</a></details>
<details><summary>Referenced by:</summary><a href="/garden/command-palettes">Command Palettes</a><a href="/garden/my-personal-website">My Personal Website</a><a href="/garden/this-knowledge-hub">This Knowledge Hub</a></details>
[Logseq](https://logseq.com) is an [Open Source](/garden/open-source/index.md) outlining software
[Logseq](https://logseq.com) is an [Open Source](/garden/open-source) outlining software

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>2 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/cinny/index.md">Cinny</a><a href="/garden/commune/index.md">Commune</a><a href="/garden/synapse/index.md">Synapse</a></details>
<details><summary>Referenced by:</summary><a href="/garden/cinny">Cinny</a><a href="/garden/commune">Commune</a><a href="/garden/synapse">Synapse</a></details>
[Matrix](https://matrix.org) is a protocol for [Decentralized](/garden/decentralized/index.md) messaging
[Matrix](https://matrix.org) is a protocol for [Decentralized](/garden/decentralized) messaging

View file

@ -14,8 +14,8 @@ const pageData = useData();
<p>12 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social/index.md">Incremental Social</a></details>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social">Incremental Social</a></details>
[Mbin](https://github.com/MbinOrg/mbin) is an [Open Source](/garden/open-source/index.md) [Fediverse](/garden/fediverse/index.md) software
[Mbin](https://github.com/MbinOrg/mbin) is an [Open Source](/garden/open-source) [Fediverse](/garden/fediverse) software
Can show both twitter-style posts and reddit-style threads

View file

@ -15,6 +15,6 @@ const pageData = useData();
<p>10 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/premium-currency/index.md">Premium Currency</a><a href="/garden/video-game-monetization/index.md">Video Game Monetization</a></details>
<details><summary>Referenced by:</summary><a href="/garden/premium-currency">Premium Currency</a><a href="/garden/video-game-monetization">Video Game Monetization</a></details>
Purchaseable items in video games that cost real life currencies

View file

@ -14,16 +14,16 @@ const pageData = useData();
<p>422 words, ~2 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/the-small-web/index.md">The Small Web</a></details>
<details><summary>Referenced by:</summary><a href="/garden/the-small-web">The Small Web</a></details>
A [Personal Websites](/garden/the-small-web/index.md) for myself, available at https://thepaperpilot.org
A [Personal Websites](/garden/the-small-web) for myself, available at https://thepaperpilot.org
## Tech Stack
I use [Logseq](/garden/logseq/index.md) to journal and collect my thoughts on various topics that interest me
I use [Logseq](/garden/logseq) to journal and collect my thoughts on various topics that interest me
- Seafile syncs my logseq files between my devices
- Git syncs my logseq files to a private repo on [Incremental Social](/garden/incremental-social/index.md) for purposes of version control and using as a submodule
- The seafile files and all repos on [Incremental Social](/garden/incremental-social/index.md) are independently backed up daily to backblaze
- Git syncs my logseq files to a private repo on [Incremental Social](/garden/incremental-social) for purposes of version control and using as a submodule
- The seafile files and all repos on [Incremental Social](/garden/incremental-social) are independently backed up daily to backblaze
My logseq files are synced to a private git repo which is added as a submodule to [my website repo](https://code.incremental.social/thepaperpilot/pages)
@ -32,12 +32,12 @@ A [Node.js script](https://code.incremental.social/thepaperpilot/pages/src/branc
- Adds lists of tags and references to pages
- Adds `<h1 />` titles, word counts, update commits, etc. to each page
- Moves the /now page to [/now](https://thepaperpilot.org/now) instead of /garden/now
- Copies some of the [Guide to Incrementals](/garden/guide-to-incrementals/index.md) pages to [/guide-to-incrementals](https://thepaperpilot.org/guide-to-incrementals/) so as to not break links made before the current site iteration
- Copies some of the [Guide to Incrementals](/garden/guide-to-incrementals) pages to [/guide-to-incrementals](https://thepaperpilot.org/guide-to-incrementals/) so as to not break links made before the current site iteration
- Generates [/changelog](https://www.thepaperpilot.org/changelog/) and its RSS, Atom, and JSON feeds
- The outputs of the generation are NOT .gitignore'd, as I use the git log to determine which pages updated when
<span id="66757760-16ab-4777-976e-8bcbac053923"> - Commit information about when a file was last updated is added via a [data loader](https://vitepress.dev/guide/data-loading) because if it was added to the file directly, rebuilding the site would count as having updated every page, by updating each commit to the changes introduced last build</span>
[Vitepress](/garden/vitepress/index.md) builds a static site from the markdown files
[Vitepress](/garden/vitepress) builds a static site from the markdown files
- Includes a custom theme that makes the whole site paper-themed
- Includes some pages like the [homepage](https://thepaperpilot.org) and the [about me page](https://thepaperpilot.org/about) that require markup, thus don't make sense to maintain inside logseq
- The sidebar is generated from my favorited pages within Logseq
@ -47,7 +47,7 @@ Three.js is used to create the effect in the background
- Simplex noise gets used to adjust the opacity of a repeating SVG pattern
- Initially tried to use just SVG, which supports creating noise and using it as a mask, but it only does 2d noise and I need 2d slices of 3d noise
Microformats are used to markup the site for [The IndieWeb](/garden/the-small-web/index.md)
Microformats are used to markup the site for [The IndieWeb](/garden/the-small-web)
- The footer contains a minimal [h-card](https://microformats.org/wiki/h-card) with a link to my full profile
- Each garden page is an [h-entry](https://indieweb.org/h-entry), and the changelog an [h-feed](https://indieweb.org/h-feed)
- All my socials are added as [rel-me](https://indieweb.org/rel-me) links, and the profiles then link back to me (as rel-me links, if allowed by the platform)

View file

@ -0,0 +1,33 @@
---
public: "true"
slug: "my-political-beliefs"
title: "My Political Beliefs"
prev: false
next: false
---
<script setup>
import { data } from '../../git.data.ts';
import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">My Political Beliefs</h1>
<p>228 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/my-political-journey">My Political Journey</a><a href="/garden/political-quizzes">Political Quizzes</a></details>
# Government
I tend to argue in favor of a government structured as a federation of local communities, where the local communities operate through [Consensus Democracy](/garden/consensus-democracy) , and the federation has a system where it takes a very significant super majority to enact anything and operates through referendums rather than any form of representation, which I see as an unnecessary yet corruptible abstraction of the will of the people. The goal is to ensure everyone has an equal voice and are supportive or at least compatible with every policy they live under.
# Economy
I believe we should eventually arrive at communism; a post-scarcity society where everyone's needs are met via automation and without the need for currency. Along the way there I expect us to democratize the workplace and work towards nationalizing every idustry.
# Society
I believe in maximizing personal liberties, so long as one is not actively harmful to others, including most forms of discrimination.
# Security
I'm against the use of violence by anyone, including the state. I believe in [Police Abolition](/garden/abolitionism) and am against the military and espionage both foreign and domestic. I believe in the [Anarchist](/garden/anarchism) value of free association, so I believe we should have fully open borders, both for travel and immigration/emmigration. I am anti-imperialist and believe in a fairly isolationist foreign policy, but am not against humanitarian foreign aid.

View file

@ -0,0 +1,37 @@
---
public: "true"
slug: "my-political-journey"
title: "My Political Journey"
prev: false
next: false
---
<script setup>
import { data } from '../../git.data.ts';
import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">My Political Journey</h1>
<p>711 words, ~4 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/political-quizzes">Political Quizzes</a></details>
[My Political Beliefs](/garden/my-political-beliefs) have changed over time, although I believe it mostly stems from me becoming increasingly informed. Since this website contains content from as far back as 2006, when I was only 10 years old, I'd like to make it extremely clear that I have changed as a person, as we all have, and disavow a lot of my older opinions, roughly everything before 2020 or so but to be safe let's just give a dynamic (current date - 2 years). It's likely you are on this page from viewing a timeline post from that range and clicking the banner. There are bits in that period I may still agree with, but perhaps it would be best to just assume I don't have a stance on it unless there's something more recent about it. Things in [My Digital Garden](undefined) are evergreen though, meaning I'll keep them up to date as my views change. Feel completely free to judge me for my opinions in there.
## Core beliefs
I believe I've been fairly political since I was quite young, and had a relatively consistent set of core values that have just been expressed in different ways and through different lenses during different points of my life. For example I've always believed in fairness, but have used that belief to both argue for and against affirmative action. Concepts like freedom or meritocracies have similarly been redefined as I've become more aware of the influence of imperialism and colonialism on our cultural values and society.
Other beliefs of mine have always been around, but I just wasn't informed enough to properly express my views. I've voiced for a very long time that I think society will inevitably trend towards post-scarcity, a world without class, currency, or jobs, where all our needs and desires are met fully, automatically, and sustainably. I've never once pictured that society retaining a hierarchy of wealth or power in any way. But I would not have recognized that as an anarchistic society until relatively recently, mostly due to misinformation and propaganda against anarchy.
## Anti-SJW
Alright, I'm guessing you're specifically on this page because you saw me upvote something on r/KotakuInAction or like a video from PSASitch or something like that. I'm not proud of this time period, where I read and watched a bit of Anti-SJW content. I'm glad to have moved on from this point of my life, and wish it hadn't happened in the first place. I don't really have any excuses, but appreciate understanding that I have grown as a person since this time.
I migrated a LOT of posts to this website for the sake of having them all in one place and under my control, rather than other websites'. I have not been able to look through the tens of thousands of posts (mostly upvotes/likes), but have done some basic filtering of these opinions I no longer hold. I'm open about the fact I once held these beliefs, but I don't need to be de facto promoting them by sharing them on this website. If you stumble across a post you think I'd rather not have on this site anymore, please [reach out](https://www.thepaperpilot.org/about/).
## Radicalization
I believe a lot of things contributed to my radicalization, which happened sometime in the early 2020s. Ultimately I think I was just aware that I didn't really like the views I was being exposed to, the direction that media was trying to to pull me, and slowly over time just engaged less and less with that kind of content. I'd always been very economically leftist, so just needed to get over my edgy/cringe phase. I think what put the nail in the coffin was watching through the [alt right playbook](https://youtube.com/playlist?list=PLJA_jUddXvY7v0VkYRbANnTnzkA_HMFtQ), a great series I highly recommend. I also started really enjoying a lot of leftist creators, like [hasanabi](https://twitch.tv/hasanabi), [philosophy tube](https://youtube.com/@philosophytube), and others. The people around me also affect my views, and after leaving college I think I interacted with nicer people on average. Of particular note here is my wife, who had their own political journey which has similarly culminated in us sort of having a positive feedback loop further and further left. Certain events like the BLM protests following George Floyd similarly cemented our position further and further left.
I actually want to also point out I've found a lot of people in this space to be very accepting of people who previously held problematic beliefs. It's largely why I feel comfortable (enough) having a lot of my history public both on this page and the site in general, and being able to describe how my political journey got me to where I am today, a very radical leftist.

View file

@ -15,24 +15,24 @@ const pageData = useData();
<p>72 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/guide-to-incrementals/what-is-content/index.md">Guide to Incrementals/What is Content?</a></details>
<details><summary>Referenced by:</summary><a href="/garden/guide-to-incrementals/what-is-content">Guide to Incrementals/What is Content?</a></details>
<details><summary>Tagged by:</summary><a href="/garden/advent-incremental/index.md">Advent Incremental</a><a href="/garden/babble-buds/index.md">Babble Buds</a><a href="/garden/capture-the-citadel/index.md">Capture the Citadel</a><a href="/garden/dice-armor/index.md">Dice Armor</a><a href="/garden/game-dev-tree/index.md">Game Dev Tree</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/opti-speech/index.md">Opti-Speech</a><a href="/garden/planar-pioneers/index.md">Planar Pioneers</a><a href="/garden/profectus/index.md">Profectus</a><a href="/garden/v-ecs/index.md">V-ecs</a></details>
<details><summary>Tagged by:</summary><a href="/garden/advent-incremental">Advent Incremental</a><a href="/garden/babble-buds">Babble Buds</a><a href="/garden/capture-the-citadel">Capture the Citadel</a><a href="/garden/dice-armor">Dice Armor</a><a href="/garden/game-dev-tree">Game Dev Tree</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/opti-speech">Opti-Speech</a><a href="/garden/planar-pioneers">Planar Pioneers</a><a href="/garden/profectus">Profectus</a><a href="/garden/v-ecs">V-ecs</a></details>
I like making games and tools!
<span id="665e3a7a-395f-4493-8f3a-482f136ea157"><h2>Games</h2></span>
- [Planar Pioneers](/garden/planar-pioneers/index.md) ([play](https://thepaperpilot.org/planar))
- [Advent Incremental](/garden/advent-incremental/index.md) ([play](https://thepaperpilot.org/advent))
- [Game Dev Tree](/garden/game-dev-tree/index.md) ([play](https://thepaperpilot.org/gamedevtree))
- [Dice Armor](/garden/dice-armor/index.md)
- [Capture the Citadel](/garden/capture-the-citadel/index.md)
- [Planar Pioneers](/garden/planar-pioneers) ([play](https://thepaperpilot.org/planar))
- [Advent Incremental](/garden/advent-incremental) ([play](https://thepaperpilot.org/advent))
- [Game Dev Tree](/garden/game-dev-tree) ([play](https://thepaperpilot.org/gamedevtree))
- [Dice Armor](/garden/dice-armor)
- [Capture the Citadel](/garden/capture-the-citadel)
- I have more you can find on [my Itch.io page](https://thepaperpilot.itch.io/)
- ... And several more in development! Most aren't going to have their own pages on here, but a long-term project of mine called [Kronos](/garden/kronos/index.md) is the exception!
- ... And several more in development! Most aren't going to have their own pages on here, but a long-term project of mine called [Kronos](/garden/kronos) is the exception!
## Tools (and other non-games)
- [Profectus](/garden/profectus/index.md)
- [Incremental Social](/garden/incremental-social/index.md)
- [Babble Buds](/garden/babble-buds/index.md)
- [V-ecs](/garden/v-ecs/index.md)
- [Opti-Speech](/garden/opti-speech/index.md)
- [Profectus](/garden/profectus)
- [Incremental Social](/garden/incremental-social)
- [Babble Buds](/garden/babble-buds)
- [V-ecs](/garden/v-ecs)
- [Opti-Speech](/garden/opti-speech)

View file

@ -14,9 +14,9 @@ const pageData = useData();
<p>133 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/anarchism/index.md">Anarchism</a><a href="/garden/local-communities/index.md">Local Communities</a></details>
<details><summary>Referenced by:</summary><a href="/garden/anarchism">Anarchism</a><a href="/garden/local-communities">Local Communities</a></details>
Neoliberalism is a conservative political philosophy that emphasizes [Individualism](/garden/individualism/index.md) and is resistant to change/progress. It became popular with the advent of President Raegan and his sweeping changes to the US economy and government (replacing the comparatively socialist polices of the New Deal and the Great Society), and affects both the Republican and Democratic US political parties.
Neoliberalism is a conservative political philosophy that emphasizes [Individualism](/garden/individualism) and is resistant to change/progress. It became popular with the advent of President Raegan and his sweeping changes to the US economy and government (replacing the comparatively socialist polices of the New Deal and the Great Society), and affects both the Republican and Democratic US political parties.
I believe neoliberalism primarily affected the boomer generation and generation x. This lines up with [trends in protest participation](https://nealcaren.org/publication/caren-social-2011/caren-social-2011.pdf), which dipped during that time (by birth cohort) and picked back up starting with Millenials. The government is still largely controlled by those generations and still very neoliberal, despite increasingly progressive and left-leaning youth demographics.

View file

@ -15,8 +15,8 @@ const pageData = useData();
<p>8 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/fediverse/index.md">Fediverse</a></details>
<details><summary>Referenced by:</summary><a href="/garden/fediverse">Fediverse</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized/index.md">Decentralized</a></details>
<details><summary>Tags:</summary><a href="/garden/decentralized">Decentralized</a></details>
[Nostr](https://nostr.com) is a protocol for [Federated Social Media](/garden/fediverse/index.md)
[Nostr](https://nostr.com) is a protocol for [Federated Social Media](/garden/fediverse)

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>25 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/advent-incremental/index.md">Advent Incremental</a><a href="/garden/cinny/index.md">Cinny</a><a href="/garden/commune/index.md">Commune</a><a href="/garden/dice-armor/index.md">Dice Armor</a><a href="/garden/forgejo/index.md">Forgejo</a><a href="/garden/game-dev-tree/index.md">Game Dev Tree</a><a href="/garden/logseq/index.md">Logseq</a><a href="/garden/mbin/index.md">Mbin</a><a href="/garden/planar-pioneers/index.md">Planar Pioneers</a><a href="/garden/profectus/index.md">Profectus</a><a href="/garden/synapse/index.md">Synapse</a><a href="/garden/vitepress/index.md">Vitepress</a><a href="/garden/weird/index.md">Weird</a></details>
<details><summary>Referenced by:</summary><a href="/garden/advent-incremental">Advent Incremental</a><a href="/garden/cinny">Cinny</a><a href="/garden/commune">Commune</a><a href="/garden/dice-armor">Dice Armor</a><a href="/garden/forgejo">Forgejo</a><a href="/garden/game-dev-tree">Game Dev Tree</a><a href="/garden/logseq">Logseq</a><a href="/garden/mbin">Mbin</a><a href="/garden/planar-pioneers">Planar Pioneers</a><a href="/garden/profectus">Profectus</a><a href="/garden/synapse">Synapse</a><a href="/garden/vitepress">Vitepress</a><a href="/garden/weird">Weird</a></details>
Projects with the source code publicly accessible

View file

@ -15,7 +15,7 @@ const pageData = useData();
<p>312 words, ~2 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
In college I continued development on the Opti-Speech project, originally built alongside the scientific paper [Opti-speech: a real-time, 3d visual feedback system for speech training](https://www.researchgate.net/profile/Thomas-Campbell-11/publication/354182612_Opti-speech_a_real-time_3d_visual_feedback_system_for_speech_training/links/6424679ca1b72772e4360fa2/Opti-speech-a-real-time-3d-visual-feedback-system-for-speech-training.pdf)

View file

@ -15,10 +15,10 @@ const pageData = useData();
<p>25 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a><a href="/garden/profectus/index.md">Profectus</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a><a href="/garden/profectus">Profectus</a></details>
Play it [here](https://thepaperpilot.org/planar)!
An [Open Source](/garden/open-source/index.md) game designed to show off [Profectus](/garden/profectus/index.md)' dynamic layer system!
An [Open Source](/garden/open-source) game designed to show off [Profectus](/garden/profectus)' dynamic layer system!
The [TV Tropes](https://tvtropes.org/pmwiki/pmwiki.php/VideoGame/PlanarPioneers) page on this game mentions some of the cool things about this game

View file

@ -0,0 +1,91 @@
---
public: "true"
slug: "political-quizzes"
title: "Political Quizzes"
prev: false
next: false
---
<script setup>
import { data } from '../../git.data.ts';
import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">Political Quizzes</h1>
<p>841 words, ~5 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
Political quizzes are a bit of a guilty pleasure of mine. I really enjoy getting my beliefs distilled into a handful of labels, and getting forced to think about issues I may not have thought about that thoroughly. I often take issue with the wording of various questions though, and certainly have opinions on some quizzes being better than others.
Ultimately, the reason I consider these quizzes a _guilty_ pleasure is because the results shouldn't really be used for anything. I believe we should vote on the issues directly, and any form of [Representative Democracy](/garden/representative-democracy) is an unnecessary abstraction. The labels may be useful as a mnemonic, but not as useful as the individual answers and the justifications behind those answers. Plus, people are going to interpret the questions differently, especially when it comes to understanding terms like liberalism, freedom, or merit.
With all that said, here I'll discuss some tests I've taken, the results I've gotten l, and my overall thoughts on it. I'll include the dates taken so they can map to [My Political Journey](/garden/my-political-journey).
# Prism Political Quiz
Made by the six triangles creator, I really like this [test](https://prismquiz.github.io)! It actually might be my favorite. It novelly gives you multiple positions to take on a given issue, rather than a statement you can agree or disagree with. I overall really liked the choices, and nearly always felt one represented my views.
I like the [results](https://prismquiz.github.io/results.html?result=m0QWd0KZP&lang=en) I got on 2024-09-06. I was surprised at my government value being just "direct democracy", when the way I define my views on Government in [My Political Beliefs](/garden/my-political-beliefs) (at the time of taking the test), which I believe I reflected accurately in my responses here, would probably include at least some points on anarchism and confederationism. That said, I liked these results so much that they inspired me to write that page on my political beliefs, which takes clear inspiration from this test.
<div class="img-container"><img src="/garden/image_1725623164393_0.png" title="image.png"/></div>
# Six Triangles
I was intrigued by [Six Triangles](https://sixtriangles.github.io/index.html)' idea of replacing axes with triangles, although some of the additional points seem a bit redundant, or just acted as a disguised additional axis. For example, the truth corner of personal freedom is not really related to the axis of freedom vs security. It could have easily been its own axis specific to misinformation. Similarly, I think the burden corner of equality is going to be highly correlated to their equality of opportunity score. Others, however, really benefit from the third point, like economy being split up into laissez faire capitalism, "well regulated" capitalism, and socialism.
Unfortunately, I found a lot of the questions to be poorly worded or vague. For example, I disagree with the statement "The government occasionally needs to do things which aren't popular for the good of its people" because popular isn't sufficient, but in a system that requires unanimity (or near unanimity), I'd argue everything that gets passed is for the good of the people, based off the values and considerations of those specific people. That nuance doesn't carry over if I just select "disagree" though. They also have the statement "Small government is usually better than big government" without any context for what big vs small mean in this context: number of employees? Amount of nationalized services? Number of constituents? Amount of regulations? This distinction matters for getting (more) accurate results.
The [results](https://sixtriangles.github.io/results.html?xzacdiqqqqfrqqlqbnwoipzx&lang=en) from taking it on 2024-09-05 were quite satisfying to go through. I particularly enjoyed being called a "Fanatic anti-imperialist". The main score I disagreed with was the government triangle; I would've preferred a higher minarchy score.
<div class="img-container"><img src="/garden/image_1725595248824_0.png" title="image.png"/></div>
<div class="img-container"><img src="/garden/image_1725595266970_0.png" title="image.png"/></div>
# SapplyValues
I took the [SapplyValues](https://sapplyvalues.github.io/) quiz on 2024-05-07:
<div class="img-container"><img src="/garden/image_1725596689335_0.png" title="image.png"/></div>
# 4Orbs
I took the [4Orbs](https://theghostofinky.github.io/4orbs/index.html) quiz on 2023-07-09:
<div class="img-container"><img src="/garden/image_1725596872858_0.png" title="image.png"/></div>
# Spekr
I like that this quiz gives live feedback, as it helped me introspect on the differences between my self-reported positions versus positions political quizzes assign me (for example, I usually think I'm way closer to anarchist than these tests usually put me, although funnily enough this quiz probably gave me one of the strongest anarchist score of any test I've taken). I also like that the creator is an anarchist themselves, which is likely uncommon across political quiz writers. I also felt like a lot of the questions were phrased in quite interesting ways.
I took the [spekr](https://jarick.works/spekr/) quiz on 2023-05-17:
<div class="img-container"><img src="/garden/image_1725597384430_0.png" title="image.png"/></div>
# isidewith
I really didn't like this quiz. I think its overly-constrained to the range of discussion considered politically viable in America, and therefore didn't ask any questions about abolishing the state or replacing corporations with worker's co-operatives. I disagree with my results here moreso than any other political quiz I've taken.
I took the [isidewith](https://www.isidewith.com/political-quiz) quiz on 2023-05-03:
<div class="img-container"><img src="/garden/image_1725598183342_0.png" title="image.png"/></div>
<div class="img-container"><img src="/garden/image_1725598206435_0.png" title="image.png"/></div>
I got quite different [results](https://secure.isidewith.com/elections/2016-presidential/1596263908) on 2016-10-30, pre-radicalization:
<div class="img-container"><img src="/garden/image_1725617078179_0.png" title="image.png"/></div>
# Political compass
I took the [political compass](https://www.politicalcompass.org/test) quiz on 2023-02-19:
<div class="img-container"><img src="/garden/image_1725598398891_0.png" title="image.png"/></div>
I'd gotten similar results on 2022-06-15:
<div class="img-container"><img src="/garden/image_1725598481054_0.png" title="image.png"/></div>
# 9Axes
I only took the short version, but here are my results from taking the [9Axes](https://9axes.github.io/) quiz on 2022-06-15:
<div class="img-container"><img src="/garden/image_1725598637811_0.png" title="image.png"/></div>

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>98 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/video-game-monetization/index.md">Video Game Monetization</a></details>
<details><summary>Referenced by:</summary><a href="/garden/video-game-monetization">Video Game Monetization</a></details>
Pre-order bonuses are benefits given to players who buy a game before it comes out
@ -29,7 +29,7 @@ Common bonuses:
- Digital goods:
- Soundtrack
- Cosmetics
- [Premium Currency](/garden/premium-currency/index.md)
- [Premium Currency](/garden/premium-currency)
- Physical goods:
- Typically pins, keychains, etc.
- Typically only included in physical editions of the game

View file

@ -14,9 +14,9 @@ const pageData = useData();
<p>71 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/pre-order-bonuses/index.md">Pre-Order Bonuses</a></details>
<details><summary>Referenced by:</summary><a href="/garden/pre-order-bonuses">Pre-Order Bonuses</a></details>
A popular form of [MTX](/garden/mtx/index.md) where instead of receiving a useful item or effect directly, you receive a currency that is then spent on an in game store
A popular form of [MTX](/garden/mtx) where instead of receiving a useful item or effect directly, you receive a currency that is then spent on an in game store
Reasons companies use them
- Abstracts the real world price of items

View file

@ -15,13 +15,13 @@ const pageData = useData();
<p>73 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/advent-incremental/index.md">Advent Incremental</a><a href="/garden/planar-pioneers/index.md">Planar Pioneers</a></details>
<details><summary>Referenced by:</summary><a href="/garden/advent-incremental">Advent Incremental</a><a href="/garden/planar-pioneers">Planar Pioneers</a></details>
<details><summary>Tagged by:</summary><a href="/garden/advent-incremental/index.md">Advent Incremental</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/planar-pioneers/index.md">Planar Pioneers</a></details>
<details><summary>Tagged by:</summary><a href="/garden/advent-incremental">Advent Incremental</a><a href="/garden/kronos">Kronos</a><a href="/garden/planar-pioneers">Planar Pioneers</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
[Profectus](https://moddingtree.com) is an [Open Source](/garden/open-source/index.md) game engine I made, loosely based on The Modding Tree by Acamaeda
[Profectus](https://moddingtree.com) is an [Open Source](/garden/open-source) game engine I made, loosely based on The Modding Tree by Acamaeda
Technically it's more of a template for making web games

View file

@ -11,9 +11,11 @@ import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">Representative Democracy</h1>
<p>51 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<p>87 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/anarchism/index.md">Anarchism</a></details>
<details><summary>Referenced by:</summary><a href="/garden/anarchism">Anarchism</a><a href="/garden/political-quizzes">Political Quizzes</a></details>
A form of democracy where people vote for representatives who then vote on the actual issues. The US has a representative democracy. By virtue of representatives not perfectly reflecting the views of their constituents, and by forming a hierarchy of power, this is a form of Democracy that is not [Anarchistic](/garden/anarchism/index.md).
A form of democracy where people vote for representatives who then vote on the actual issues. The US has a representative democracy. By virtue of representatives not perfectly reflecting the views of their constituents, and by forming a hierarchy of power, this is a form of Democracy that is not [Anarchistic](/garden/anarchism).
Representative forms of government were once useful for their logistical simplifications, but now primarily serve as a way to limit the range of acceptable political opinions/options and to perpetuate the reign of those in power.

View file

@ -15,15 +15,15 @@ const pageData = useData();
<p>98 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/commune/index.md">Commune</a><a href="/garden/fediverse/index.md">Fediverse</a></details>
<details><summary>Referenced by:</summary><a href="/garden/commune">Commune</a><a href="/garden/fediverse">Fediverse</a></details>
Traditional social media
- Not [Decentralized](/garden/decentralized/index.md)
- Not [Decentralized](/garden/decentralized)
- Can't choose your own rules, sorting methods, data queries, etc.
- Overrun by scams and ads and influencers
[Federated Social Media](/garden/fediverse/index.md)
- Partially [Decentralized](/garden/decentralized/index.md)
[Federated Social Media](/garden/fediverse)
- Partially [Decentralized](/garden/decentralized)
- Self hosting is too hard for everyone to do
- Still subject to instance's moderation, limitations, etc.
- Users need to pick an instance, associating their identity with one specific group
@ -31,4 +31,4 @@ Traditional social media
- The person is permanently associated with that one group
- You have to pick before getting a "trial period" to ensure you actually like that group/instance
My take on an ideal social media [Fedi v2](/garden/fedi-v2/index.md)
My take on an ideal social media [Fedi v2](/garden/fedi-v2)

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>2 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social/index.md">Incremental Social</a></details>
<details><summary>Referenced by:</summary><a href="/garden/incremental-social">Incremental Social</a></details>
[Synapse](https://github.com/element-hq/synapse) is an [Open Source](/garden/open-source/index.md) server software for the [Matrix](/garden/matrix/index.md) protocol
[Synapse](https://github.com/element-hq/synapse) is an [Open Source](/garden/open-source) server software for the [Matrix](/garden/matrix) protocol

View file

@ -15,11 +15,11 @@ const pageData = useData();
<p>144 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/davey-wreden/index.md">Davey Wreden</a></details>
<details><summary>Referenced by:</summary><a href="/garden/davey-wreden">Davey Wreden</a></details>
<details><summary>Tags:</summary><a href="/garden/davey-wreden/index.md">Davey Wreden</a></details>
<details><summary>Tags:</summary><a href="/garden/davey-wreden">Davey Wreden</a></details>
My favorite video game of all time, bar none. Created by [Davey Wreden](/garden/davey-wreden/index.md)
My favorite video game of all time, bar none. Created by [Davey Wreden](/garden/davey-wreden)
The game broadly comments on the relationship between creators and consumers, and it can apply to all forms of art
- Perhaps also an important commentary on parasocial relationships

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>45 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a></details>
The Cozy Web is an extension of the dark forest theory of the Internet
@ -22,4 +22,4 @@ It refers to the part of the web that is not web indexable
This part of the web is known for not typically having ads or marketers
Popularized by [this article](https://maggieappleton.com/cozy-web) written by Maggie Appleton, who has also written a lot about [Digital Gardens](/garden/digital-gardens/index.md)
Popularized by [this article](https://maggieappleton.com/cozy-web) written by Maggie Appleton, who has also written a lot about [Digital Gardens](/garden/digital-gardens)

View file

@ -14,10 +14,10 @@ const pageData = useData();
<p>57 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/my-political-journey">My Political Journey</a><a href="/now/index">/now</a><a href="/garden/social-media">Social Media</a></details>
Refers to reblogging (and re-hosting, sometimes) of someone else's content on your own site
[The Internet is a series of webs](https://aramzs.xyz/essays/the-internet-is-a-series-of-webs/) discusses some ideas and best practices for amplification
To ensure the rehosted content actually came from the claimed author and was not tampered with, all content should be signed using [The IndieWeb/Signature Blocks](/garden/the-indieweb/signature-blocks/index.md)
To ensure the rehosted content actually came from the claimed author and was not tampered with, all content should be signed using [The IndieWeb/Signature Blocks](/garden/the-indieweb/signature-blocks)

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>14 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a><a href="/garden/incremental-social/index.md">Incremental Social</a><a href="/garden/kronos/index.md">Kronos</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/social-media/index.md">Social Media</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a><a href="/garden/incremental-social">Incremental Social</a><a href="/garden/kronos">Kronos</a><a href="/garden/my-political-journey">My Political Journey</a><a href="/now/index">/now</a><a href="/garden/social-media">Social Media</a></details>
A proposal I want to write for posting signed content on your [IndieWeb](/garden/the-small-web/index.md) website
A proposal I want to write for posting signed content on your [IndieWeb](/garden/the-small-web) website

View file

@ -15,9 +15,9 @@ const pageData = useData();
<p>778 words, ~4 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/commune/index.md">Commune</a><a href="/garden/federated-identity/index.md">Federated Identity</a><a href="/garden/fedi-v2/index.md">Fedi v2</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/now/index">/now</a><a href="/garden/the-indieweb/signature-blocks/index.md">The IndieWeb/Signature Blocks</a><a href="/garden/this-knowledge-hub/index.md">This Knowledge Hub</a><a href="/garden/webrings/index.md">Webrings</a><a href="/garden/weird/index.md">Weird</a></details>
<details><summary>Referenced by:</summary><a href="/garden/commune">Commune</a><a href="/garden/federated-identity">Federated Identity</a><a href="/garden/fedi-v2">Fedi v2</a><a href="/garden/my-personal-website">My Personal Website</a><a href="/now/index">/now</a><a href="/garden/the-indieweb/signature-blocks">The IndieWeb/Signature Blocks</a><a href="/garden/this-knowledge-hub">This Knowledge Hub</a><a href="/garden/webrings">Webrings</a><a href="/garden/weird">Weird</a></details>
The small web (also known as the indie web, personal web, the web revival movement, and other terms) refers to small, personal, independent websites. It is seen as a direct alternative to the centralized and homogenized websites like X, Meta, and TikTok. [My Personal Website](/garden/my-personal-website/index.md) is part of the small web!
The small web (also known as the indie web, personal web, the web revival movement, and other terms) refers to small, personal, independent websites. It is seen as a direct alternative to the centralized and homogenized websites like X, Meta, and TikTok. [My Personal Website](/garden/my-personal-website) is part of the small web!
## Motivation behind the small web
@ -53,7 +53,7 @@ These are videos and articles that continue expanding on the values and motivati
## Browsing the small web
Follow [Webrings](/garden/webrings/index.md) or other links from known small websites.
Follow [Webrings](/garden/webrings) or other links from known small websites.
[Marginalia](https://search.marginalia.nu) is a search engine for non-commercial content with a "random" button and filters for the small web explicitly (amongst other useful filters!)
@ -70,23 +70,23 @@ The [Tildeverse](https://tildeverse.org/) contains a large set of personal websi
- [mmm.page — Your Corner of the Internet](https://mmm.page/)
- [Codeberg pages](https://codeberg.page) (and any other [pages-server](https://codeberg.org/Codeberg/pages-server) instance - like on [Incremental Social](https://incremental.social/pages)!)
- [Github pages](https://pages.github.com)
- [Weird](/garden/weird/index.md) (in development)
- [Weird](/garden/weird) (in development)
Other resources:
- [32-bit cafe](https://32bit.cafe/)
### [Streams](https://indieweb.org/stream)
[Microsub](https://indieweb.org/Microsub) is a proposed protocol to support hosting streams of content on personal websites in a way they can be consistently ingested by microsub clients. This way, people could subscribe to multiple streams on independent websites and get them in one feed. Through this, the indie web becomes a [Federated Social Media](/garden/fediverse/index.md).
[Microsub](https://indieweb.org/Microsub) is a proposed protocol to support hosting streams of content on personal websites in a way they can be consistently ingested by microsub clients. This way, people could subscribe to multiple streams on independent websites and get them in one feed. Through this, the indie web becomes a [Federated Social Media](/garden/fediverse).
Streams also allow your personal website to be the one source of truth for your posted content, in a concept called [POSSE](https://indieweb.org/POSSE) - Publish (on your) Own Site, Syndicate Elsewhere (other social media sites). This would effectively solve the problems described in [Hey Creators, Please Make Firehoses!](https://jonbell.medium.com/hey-creators-please-make-firehoses-8d0c48c075e4)
Multiple streams can be hosted by one site/person so people can subscribe to the kind of content they're interested in.
### [Digital Gardens](/garden/digital-gardens/index.md)
### [Digital Gardens](/garden/digital-gardens)
These sites may be useful to occasionally check up on rather than get notifications from on every post/change
- Although [Garden-RSS](/garden/garden-rss/index.md) could allow those who want to receive notifications to do so
- Although [Garden-RSS](/garden/garden-rss) could allow those who want to receive notifications to do so
### The future

View file

@ -14,21 +14,21 @@ const pageData = useData();
<p>135 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens/index.md">Digital Gardens</a></details>
<details><summary>Referenced by:</summary><a href="/garden/digital-gardens">Digital Gardens</a></details>
This is my knowledge hub!
- It's a [Digital Garden](/garden/digital-gardens/index.md) collecting my thoughts in varying levels of completeness on basically anything I have interest in
- It's a [Digital Garden](/garden/digital-gardens) collecting my thoughts in varying levels of completeness on basically anything I have interest in
This is not Wikipedia. My thoughts are biased and argumentative, but to the best of my ability based on fact and expertise
<span id="6637b86a-3603-45ef-a21e-b33c7d96c529">I'm writing on _something_ essentially every day</span>
- Most of my pages are private, especially the journal pages
- I'll only push updates to this site every so often (not an automatic process)
- Until something like [Garden-RSS](/garden/garden-rss/index.md) exists, we'll have to make do with [/changelog](https://thepaperpilot.org/changelog) which gives a git diff summary for every pushed change, in the form of a [The IndieWeb](/garden/the-small-web/index.md) stream as well as an RSS feed
- Until something like [Garden-RSS](/garden/garden-rss) exists, we'll have to make do with [/changelog](https://thepaperpilot.org/changelog) which gives a git diff summary for every pushed change, in the form of a [The IndieWeb](/garden/the-small-web) stream as well as an RSS feed
Written in [Logseq](/garden/logseq/index.md) and rendered with [Vitepress](/garden/vitepress/index.md)
Written in [Logseq](/garden/logseq) and rendered with [Vitepress](/garden/vitepress)
I want to utilize the strategies described in [Andy's working notes](https://notes.andymatuschak.org/About_these_notes?stackedNotes=zPKTSiU725W9WQCqoVPBcxm) to help improve my digital garden
Suggested pages:
- [The Small Web](/garden/the-small-web/index.md)
- [The Small Web](/garden/the-small-web)

View file

@ -0,0 +1,25 @@
---
public: "true"
slug: "trans-athletes-in-sports"
title: "Trans athletes in sports"
prev: false
next: false
---
<script setup>
import { data } from '../../git.data.ts';
import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">Trans athletes in sports</h1>
<p>353 words, ~2 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
I believe trans athletes should be allowed in sports. They do not need a separate category, and should be allowed to pick if they compete with those of their gender assigned at birth or how they identify now.
The main argument against the above is that it invalidates the perceived fairness of the sport. However, fairness as a concept is inherently subjective and influenced by many rules and assumptions we as a society made up and are now using for the express purpose of discriminating against trans individuals.
Gender is a social construct, but so is sex: we've created an arbitrary definition to separate male and female that, due to being overly rigid, fails to handle edge cases like women with naturally higher testosterone levels, or individuals with atypical amounts of x and/or y chromosomes, or atypical sets of body parts. These distinctions do not need to exist.
The typical arguments for separating the sexes in sports is because testosterone is a performance enhancing substance, so those who naturally produce more of it should have their own category, for reasons similar to those of weight classes in boxing. However, other biological advantages do not warrant separate classes, like Phelp's webbed feet or atypical lactic acid production. In fact, many athletes are even allowed to take performance enhancing drugs like Adderall to combat biological disadvantages like ADHD. But not all drugs are banned or permitted equally - caffeine is a stimulant like Adderall but is allowed to be taken by anyone. Even the distinction of what a drug is is arbitrary, as protein powder is a substance that improves performance but is not considered a drug.
There are many advantages one might have in sports, but contrary to a true meritocracy, our arbitrary distinctions on allowed vs disallowed advantages exposes the biases of our society, which are often racist, sexist, and transphobic. Therefore, appeals to "fairness" are explicit endorsements of those biases, namely bigotry. Even if you do agree with some of the arbitrary distinctions I've brought up, that doesn't detract from the point that they _are_ arbitrary and subjective.

View file

@ -15,7 +15,7 @@ const pageData = useData();
<p>209 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/my-projects/index.md">My Projects</a></details>
<details><summary>Tags:</summary><a href="/garden/my-projects">My Projects</a></details>
<div class="img-container"><img src="/garden/screenshot_1717383987886_0.png" title="screenshot.png"/></div>
@ -31,4 +31,4 @@ Instead, I made a couple of worlds by myself - an infinite procedurally generate
<div class="img-container"><img src="/garden/sandsoftime_1717383994964_0.png" title="sandsoftime.png"/></div>
The gameplay of Sands of Time was replicated in [Kronos](/garden/kronos/index.md) Chapter 2!
The gameplay of Sands of Time was replicated in [Kronos](/garden/kronos) Chapter 2!

View file

@ -14,7 +14,7 @@ const pageData = useData();
<p>250 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/life-is-strange/index.md">Life is Strange</a></details>
<details><summary>Referenced by:</summary><a href="/garden/life-is-strange">Life is Strange</a></details>
## AAA games
@ -22,12 +22,12 @@ They Clcost a lot of money to make, mostly due to the graphics arms race. The pr
Graphics would not justify significantly higher prices, and AAA studios know this. So instead they use the techniques to make more money without raising the base price:
- Premium Editions
- [MTX](/garden/mtx/index.md)
- [Pre-Order Bonuses](/garden/pre-order-bonuses/index.md)
- [MTX](/garden/mtx)
- [Pre-Order Bonuses](/garden/pre-order-bonuses)
## Free-to-play games
Typically utilize [MTX](/garden/mtx/index.md) and ads in order to profit. Often extreme cases of designing games to compell players to spend money.
Typically utilize [MTX](/garden/mtx) and ads in order to profit. Often extreme cases of designing games to compell players to spend money.
## Indie developers
@ -38,7 +38,7 @@ Trying to make a sustainable living as an indie developer is hard. The industry
Requirements:
- Free demo
- Paid base game
- No [MTX](/garden/mtx/index.md)
- No [MTX](/garden/mtx)
- Paid content expansions
The goal of the above is to allow players to determine if they enjoy the game without putting money down, and to ensure the game design cannot be tainted by the monetization.

View file

@ -14,6 +14,6 @@ const pageData = useData();
<p>4 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/this-knowledge-hub/index.md">This Knowledge Hub</a></details>
<details><summary>Referenced by:</summary><a href="/garden/my-personal-website">My Personal Website</a><a href="/garden/this-knowledge-hub">This Knowledge Hub</a></details>
[Vitepress](https://vitepress.dev) is an [Open Source](/garden/open-source/index.md) static site generator
[Vitepress](https://vitepress.dev) is an [Open Source](/garden/open-source) static site generator

View file

@ -15,6 +15,6 @@ const pageData = useData();
<p>8 words, ~0 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Tags:</summary><a href="/garden/davey-wreden/index.md">Davey Wreden</a></details>
<details><summary>Tags:</summary><a href="/garden/davey-wreden">Davey Wreden</a></details>
[Wanderstop](https://www.wanderstopgame.com/) is the first game by [Ivy Road](/garden/ivy-road/index.md). It's a narrative focused cozy game
[Wanderstop](https://www.wanderstopgame.com/) is the first game by [Ivy Road](/garden/ivy-road). It's a narrative focused cozy game

View file

@ -14,20 +14,20 @@ const pageData = useData();
<p>139 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/the-small-web/index.md">The Small Web</a></details>
<details><summary>Referenced by:</summary><a href="/garden/the-small-web">The Small Web</a></details>
A collection of [Personal Websites](/garden/the-small-web/index.md) that link to each other
A collection of [Personal Websites](/garden/the-small-web) that link to each other
- These websites are all endorsing each other
- They form a network of related sites readers might be interested in
- Built on human trust rather than algorithms
[Commune](/garden/commune/index.md) has a vision for modern webrings
[Commune](/garden/commune) has a vision for modern webrings
- Have communities set up matrix spaces for chatting
- Multiple spaces can contain the same room
- Related communities can share a room about a relevant topic
- e.g. a bunch of game development libraries shared a "Game Design" room
- This allows smaller communities to grow from cross-pollinating with other related communities
- Could [Incremental Social](/garden/incremental-social/index.md) host a shared "Incremental Games" room?
- Could [Incremental Social](/garden/incremental-social) host a shared "Incremental Games" room?
- How to bridge one channel to multiple discord servers, since that's where most incremental games communities are
- Would this be appealing to already large communities?
- Would this be overwhelming to smaller communities?

View file

@ -14,15 +14,15 @@ const pageData = useData();
<p>114 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
<details><summary>Referenced by:</summary><a href="/garden/commune/index.md">Commune</a><a href="/garden/fedi-v2/index.md">Fedi v2</a><a href="/garden/my-personal-website/index.md">My Personal Website</a><a href="/garden/the-small-web/index.md">The Small Web</a></details>
<details><summary>Referenced by:</summary><a href="/garden/commune">Commune</a><a href="/garden/fedi-v2">Fedi v2</a><a href="/garden/the-small-web">The Small Web</a></details>
[Weird](https://weird.one) is an [Open Source](/garden/open-source/index.md) project by the [Commune](/garden/commune/index.md) team currently in development
[Weird](https://weird.one) is an [Open Source](/garden/open-source) project by the [Commune](/garden/commune) team currently in development
Aims to make creating [Personal Websites](/garden/the-small-web/index.md) with [Federated Identity](/garden/federated-identity/index.md) available to everyone
- Also plans on having paid tiers for giving people access to single user instances of various [Fediverse](/garden/fediverse/index.md) tools
Aims to make creating [Personal Websites](/garden/the-small-web) with [Federated Identity](/garden/federated-identity) available to everyone
- Also plans on having paid tiers for giving people access to single user instances of various [Fediverse](/garden/fediverse) tools
Long term, Weird wants to build a new better fediverse
- It's being built on [Iroh](https://iroh.computer), which allows for decentralized identities that are not reliant on any specific servers continuing to exist
- Their current plans are laid out in [Next Gen Federation on Iroh: Graph Data & Linked Documents Layers](https://github.com/commune-os/weird/discussions/32)
- Erlend discusses some of the implications for identity specifically in [Weird Netizens](https://blog.erlend.sh/weird-netizens)
- I have my own high level take on how this new fediverse would look like in [Fedi v2](/garden/fedi-v2/index.md)
- I have my own high level take on how this new fediverse would look like in [Fedi v2](/garden/fedi-v2)

View file

@ -36,9 +36,14 @@ I'm Anthony, or The Paper Pilot, and welcome to my [digital garden](/garden/digi
This is a public website collecting all my (public) thoughts and projects all in one place. There are a lot of pages here, that link to each other wiki-style. I suggest starting your browsing with one of the recommended pages that most closely align with your interests :).
## Most Recent Activity
<div style="margin-top: 60px" class="post-feed" v-html="data.join('')" />
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { TresCanvas } from '@tresjs/core'
import { data } from "./recent-posts.data.ts";
import Hole from "./.vitepress/theme/Hole.vue";
const xOffset = ref(0);

View file

@ -11,31 +11,27 @@ import { useData } from 'vitepress';
const pageData = useData();
</script>
<h1 class="p-name">/now</h1>
<p>181 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<p>229 words, ~1 minute read. <span v-html="data[`site/${pageData.page.value.relativePath}`]" /></p>
<hr/>
This "now page" offers a big picture glimpse into what Im focused on at this point in my life. [What is a now page](https://nownownow.com/about)?
## IndieWeb
## Digital Gardens
I've been learning a lot about [The Small Web](/garden/the-small-web/index.md) (or the various other names it goes by). I've been working on this website and implementing the various IndieWeb building blocks
I've been learning about a ton of concepts that I think can be interestingly combined; namely [The Small Web](/garden/the-small-web), [Digital Garden](/garden/digital-gardens)s, [Commune](/garden/commune), and [Local-First Software](undefined). I have an idea for a digital garden that is structured like a network of topics and includes conversations, pages, and citations about each topic. I want to build it on the [Agentic fediverse](undefined) so clients can build a large network by subscribing to community made networks across the fediverse.
I'm also working on a proposal for adding [The IndieWeb/Signature Blocks](/garden/the-indieweb/signature-blocks/index.md) to your notes
## Commune
While I'm not contributing to the project directly, I'm following along and participating with the discussions and designs of [Commune](/garden/commune/index.md).
I'm working on a mockup of what an app could look like that treats incoming messages, emails, etc. differently based on user defined rules, with a focus on moving them into personal or communal digital gardens.
Ultimately, I think this project could have some implications on how _this_ digital garden operates, so I've decided to stop further indieweb integrations like webmentions for now. I'd like to see a server be able to bridge indieweb and agentic fediverse posts, and start using the agentic fediverse posts as my new source of truth.
## Incremental Social
I'm running and improving the social media site [Incremental Social](/garden/incremental-social/index.md), along with CardboardEmpress.
I'm running and improving the social media site [Incremental Social](/garden/incremental-social), along with CardboardEmpress.
I'd like to look into it eventually hosting a bridge between the mbin AP actors and [Agentic fediverse](undefined) identities.
## Chromatic Lattice
I'm working on a multiplayer incremental game. That's all that's known publicly for now 😜.
I'm working on a multiplayer incremental game called [Chromatic Lattice](/garden/chromatic-lattice) .
## Kronos
I'm working on a long single player narratively driven incremental game. This is a very long-term project.
I'm working on a long single player narratively driven incremental game called [Kronos](/garden/kronos) . This is a very long-term project.

BIN
site/public/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

260
utils.js Normal file
View file

@ -0,0 +1,260 @@
const fs = require("fs");
const path = require("path");
const sanitizeHtml = require('sanitize-html');
const { getLinkPreview } = require("link-preview-js");
function walk(dir, cb) {
const list = fs.readdirSync(dir);
return Promise.all(list.map(file => {
const resolvedFile = path.resolve(dir, file);
const stat = fs.statSync(resolvedFile);
if (stat.isDirectory()) {
return walk(resolvedFile, cb);
} else {
return new Promise((resolve) => cb(dir, resolvedFile, resolve));
}
}));
}
function getActionDescription({ action, verb, timestamp }) {
const ts = new Date(timestamp);
const tsString = ts.toLocaleString();
const timeDisplay = timestamp ?
` on <time class="dt-published" datetime="${tsString}" title="${tsString}">
${ts.toLocaleDateString()}
</time>` : '';
return `<div class="action-description">
${action ? `<span class="action">${action}</span>` : ""}
<a class="p-name u-url h-card" href="/about">The Paper Pilot</a>
<a class="u-url" href="/posts/${timestamp}">${verb}</a>
<span>this post${timeDisplay}:</span>
</div>`;
}
async function getAvatar({ photo, name, url, syndications, timestamp, tags, action, linkClasses }) {
url ??= "https://www.thepaperpilot.org/about/";
name ??= "The Paper Pilot";
photo ??= "https://www.thepaperpilot.org/me.jpg";
return `<div class="avatar p-author h-card">
<a class="u-url ${linkClasses ?? ''}" href="${url}">
<div class="photo">
<img class="u-photo" src="${await getMediaUrl(photo)}" />
${action ? `<div class="action">${action}</div>` : ""}
</div>
<div class="p-name">${name}</div>
</a>
${timestamp ? `<time class="dt-published" datetime="${new Date(timestamp).toLocaleString()}" title="${new Date(timestamp).toLocaleString()}">
${new Date(timestamp).toLocaleDateString()}
</time>\n` : ''}
${syndications?.length > 0 ? `<div class="syndications">
${syndications.map(synd => `<a href="${synd.url}">${synd.type}</a>`).join("")}
</div>\n` : ''}
${tags?.length > 0 ? `<ul class="tags">
${tags.map(tag => `<li><a href="/tag/${tag}/1">${tag}</a></li>`).join("")}
</ul>\n` : ''}
</div>`;
}
async function processLinkPreview({ mediaType, url, contentType, images, favicons, title, description }, header) {
let mediaPreview = '';
// Don't show media previews if we failed to retrieve the image
if (mediaType === "image") {
const localUrl = await getMediaUrl(url);
if (localUrl !== url) {
mediaPreview = `<img src="${localUrl}" />`;
}
} else if (mediaType === "audio") {
mediaPreview = `<audio controls>
<source src="${url}" type="${contentType}">
</audio>`;
} else if (mediaType === "video") {
mediaPreview = `<video controls>
<source src="${url}" type="${contentType}">
</video>`;
} else if (images?.length > 0) {
const localUrl = await getMediaUrl(images[0]);
if (localUrl !== url) {
mediaPreview = `<img src="${localUrl}" />`;
}
} // Otherwise, no media preview
let infoSection = '';
let favicon = favicons.find(f => !f.startsWith(""));
const localFavicon = favicon ? await getMediaUrl(favicon) : "";
favicon = favicon && favicon !== localFavicon ? `<img src="${localFavicon}" />` : '';
if (title) {
title = title.replaceAll(/</g, '&lt;').replaceAll(/>/g, '&gt;');
infoSection += `\n<a class="u-url" href="${url}"><h2>${favicon}${title}</h2></a>`;
favicon = '';
}
if (description) {
description = description.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')
.replaceAll(/\n/g, '<br />');
infoSection += `\n<div>${favicon}${linkify(description)}</div>`;
}
if (!title) {
infoSection += `\n<a class="u-url" href="${url}">${url}</a>`;
}
return `<div class="img-container">
${header ? '<h2>' + header.replaceAll(/</g, '&lt;').replaceAll(/>/g, '&gt;') + '</h2>' : ''}
${mediaPreview}
<div class="description">
${infoSection}
</div>
</div>`;
}
let archiveTries = 0;
async function getArchivePreview(url, timestamp, fallback) {
const trimmedUrl = encodeURIComponent(url.replace(/^https?:\/\/(www\.)?/, ''));
const archiveInfoUrl = `http://archive.org/wayback/available?url=${trimmedUrl}&timestamp=${timestamp}`;
const archiveResponse = await fetch(archiveInfoUrl).then(r => r.text());
let archiveJson;
try {
archiveJson = JSON.parse(archiveResponse);
} catch (err) {
console.error("Unexpected response from wayback machine:", archiveInfoUrl, archiveResponse, err);
process.exit(0);
}
const archivedTimestamp = archiveJson.archived_snapshots?.closest?.timestamp;
if (archivedTimestamp) {
const preview = await getLinkPreview(`https://web.archive.org/web/${archivedTimestamp}id_/${archiveJson.url}`, { followRedirects: true })
.catch(err => {
console.log("Error while trying to retrieve page from wayback machine:", err);
archiveTries++;
if (archiveTries >= 3) {
console.log("Too many retries. Giving up.");
process.exit(1);
} else {
console.log(`Trying again. Try number ${archiveTries + 1}/3`);
return getArchivePreview(url, timestamp, fallback);
}
});
archiveTries = 0;
if (preview?.mediaType === "website" && (preview.images ?? []).length === 0 && fallback) {
preview.images = [fallback];
}
return preview;
} else {
console.log("Not available on wayback machine. No preview available.");
return;
}
}
async function getMediaUrl(url) {
if (url.startsWith("https://www.thepaperpilot.org") || !url.startsWith("http") || url.includes(".gfycat.com/")) {
return url;
}
const fileExtensions = url.match(/\.[a-zA-Z]+/g);
let ext = fileExtensions[fileExtensions.length - 1].toLowerCase();
if (![".png", ".jpg", ".jpeg", ".ico", ".gif", ".webp", ".svg"].includes(ext)) {
if (url.startsWith("https://yt3.ggpht.com/")) {
ext = ".png";
} else {
return url;
}
}
const urlHash = hash(url);
let publicUrl = `/media/${urlHash}${ext}`;
const filepath = `site/public/media/${urlHash}${ext}`;
if (fs.existsSync(filepath)) {
if (fs.statSync(filepath).size === 0) {
fs.rmSync(filepath);
} else {
return publicUrl;
}
}
console.log(`Downloading ${url} to ${publicUrl}`);
await new Promise((resolve, reject) => {
setTimeout(reject, 2000);
fetch(url).then(async response => {
const body = await response.text();
if (body.startsWith("<html") || body === "") {
// TODO also blacklist this url so future runs are faster
reject();
} else {
const fd = fs.openSync(filepath, "w+");
fs.writeSync(fd, body);
fs.closeSync(fd);
resolve();
}
}).catch(reject);
}).catch(() => publicUrl = url);
if (fs.existsSync(filepath) && fs.statSync(filepath).size !== 0) {
console.log("Download successful!");
return publicUrl;
}
return url;
}
// https://byby.dev/js-slugify-string
function slugify(str) {
return String(str)
.normalize('NFKD') // split accented characters into their base characters and diacritical marks
.replace(/[\u0300-\u036f]/g, '') // remove all the accents, which happen to be all in the \u03xx UNICODE block.
.trim() // trim leading or trailing whitespace
.toLowerCase() // convert to lowercase
.replace(/[^a-z0-9 -]/g, '') // remove non-alphanumeric characters
.replace(/\s+/g, '-') // replace spaces with hyphens
.replace(/-+/g, '-'); // remove consecutive hyphens
}
// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
function hash(str) {
let hash = 0, i, chr;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
function linkify(str) {
return str.replaceAll(
/((http|https):\/\/)?([a-z0-9-]+\.)?[a-z0-9-]+(\.[a-z]{2,6}){1,3}(\/[a-z0-9,._\/~#&=;%+?-]*)?/isg,
'<a href="$&">$&</a>');
}
function monthString(month) {
return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"][month];
}
function preparePost(post) {
return post.replaceAll(/\r/g, '').replaceAll(/\n(\s*\n)*/gm, '\n');
}
function encodeString(str, indent) {
const spaces = new Array(indent).fill(' ').join('');
return `"${str
.replaceAll('\\', '\\\\')
.replaceAll('"', '\\"')
.replaceAll(/\n/g, '\n' + spaces)
.replaceAll(/{/g, '\\\\{')
.replaceAll(/}/g, '\\\\}')}"`;
}
function sanitize(content) {
return sanitizeHtml(content)
.replaceAll(/{/g, '&#123;')
.replaceAll(/}/g, '&#125;');
}
module.exports = {
walk,
getActionDescription,
getAvatar,
slugify,
monthString,
preparePost,
encodeString,
processLinkPreview,
getArchivePreview,
getMediaUrl,
linkify,
sanitize
};

1035
yarn.lock

File diff suppressed because it is too large Load diff