diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6e94c2c2e786173cc275e215e2a7d7d7fffa266c..b623caf2098f61aac7a7ed0efc4a8f3a1f077b9b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,19 @@
# See https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
+stages:
+ - .pre
+ - lemmy
+ - build
+ - test
+ - deploy
+ - .post
+
include:
+ - local: .gitlab/build-year.yml
+ inputs:
+ year: 2026
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- local: .gitlab/build-year.yml
inputs:
year: 2025
@@ -12,6 +25,18 @@ include:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+lemmy fetcher:
+ image: python:3-alpine
+ stage: lemmy
+ rules:
+ - if: $CI_PIPELINE_SOURCE == "schedule"
+ script:
+ - python ./tools/lemmy_fetcher.py
+ cache:
+ key: lemmy-fetcher
+ paths:
+ - .fetcher
+
build:
image: python:3-alpine
stage: build
@@ -20,12 +45,14 @@ build:
script:
- python ./tools/merge_data.py ./entries/2024 ./atlas2024.json
- python ./tools/merge_data.py ./entries/2025 ./atlas2025.json
+ - python ./tools/merge_data.py ./entries/2026 ./atlas2026.json
artifacts:
expire_in: "1 hour"
expose_as: "atlas_json"
paths:
- "atlas2024.json"
- "atlas2025.json"
+ - "atlas2026.json"
comment:
image: alpine
@@ -41,6 +68,7 @@ comment:
- build
before_script:
- apk add curl jq git
+ - git fetch origin main
script:
- |
# 📝 Build MR Comment Body
diff --git a/entries/2026/.keep b/entries/2026/.keep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tools/lemmy_fetcher.py b/tools/lemmy_fetcher.py
index 7080f183a25400c8afc235d164ebdf477a207add..68352de35f245b93da5c77cf01b5ce1ccf73ddad 100644
--- a/tools/lemmy_fetcher.py
+++ b/tools/lemmy_fetcher.py
@@ -8,6 +8,16 @@
from lib.gitlab_forge import GitlabForge
import hashlib
+# cached in gitlab-ci
+STATE_FOLDER = ".fetcher/"
+
+LEMMY_HOST = "toast.ooo"
+
+GIT_CONFIG = [
+ "-c", "user.name=Atlas Bot [CI]",
+ "-c", "user.email=canvas-atlas@sc07.com"
+]
+
EDITIONS = {
"2024": {
"community": "2024lemmycanvasatlas",
@@ -17,12 +27,42 @@
"community": "2025fedicanvasatlas",
"entry_folder": "entries/2025/"
},
+ "2026": {
+ "community": "2026canvasatlas",
+ "entry_folder": "entries/2026/"
+ },
"test": {
"community": "testatlas",
"entry_folder": "entries/2025/"
}
}
+ENDPOINTS = {
+ "login": "https://" + LEMMY_HOST + "/api/v3/user/login",
+ "logout": "https://" + LEMMY_HOST + "/api/v3/user/logout",
+ "list_posts": "https://" + LEMMY_HOST + "/api/v3/post/list",
+ "comment": "https://" + LEMMY_HOST + "/api/v3/comment"
+}
+
+JWT = ""
+
+def login():
+ global JWT
+ login_req = requests.post(ENDPOINTS["login"], json={
+ "username_or_email": os.environ.get("LEMMY_USERNAME"),
+ "password": os.environ.get("LEMMY_PASSWORD"),
+ "totp_2fa_token": "",
+ })
+
+ assert login_req.status_code == 200
+ JWT = login_req.json()["jwt"]
+
+def logout():
+ global JWT
+ requests.post(ENDPOINTS["logout"], headers={
+ "Authorization": "Bearer " + JWT
+ })
+
class LemmyFetcher:
def __init__(self, edition):
assert edition in EDITIONS
@@ -31,8 +71,9 @@ def __init__(self, edition):
self.forge = GitlabForge()
self.forge.from_env()
self.read_ids = set()
- os.makedirs("processed_posts", exist_ok = True)
- os.makedirs("failed_posts", exist_ok = True)
+
+ self.prepare_dirs()
+
for filename in os.listdir("processed_posts"):
self.read_ids.add(filename)
@@ -40,15 +81,10 @@ def __init__(self, edition):
for filename in os.listdir("failed_posts"):
self.failed_ids_hashes.add(filename)
- login_req = requests.post("https://toast.ooo/api/v3/user/login", json={
- "username_or_email": os.environ.get("LEMMY_USERNAME"),
- "password": os.environ.get("LEMMY_PASSWORD"),
- "totp_2fa_token": "",
- })
- print(login_req.content)
- assert login_req.status_code == 200
- self.lemmy_jwt = login_req.json()["jwt"]
-
+ def prepare_dirs():
+ os.makedirs(STATE_FOLDER, exist_ok=True)
+ os.makedirs(STATE_FOLDER + "processed_posts", exist_ok=True)
+ os.makedirs(STATE_FOLDER + "failed_posts", exist_ok=True)
def is_post_failed(self, post_id, body):
entry_key = str(post_id) + "-" + self.hash_body(body)
@@ -138,7 +174,7 @@ def process_post(self, post_body, make_pr):
if make_pr:
if success:
subprocess.check_call(["git", "add", EDITIONS[self.edition]["entry_folder"]])
- subprocess.check_call(["git", "-c", "user.name=lemmy_canvas_bot", "-c", "user.email=lemmy_canvas_bot@mariusdavid.fr", "commit", "-m", "Add submission from Lemmy post " + str(post_id)])
+ subprocess.check_call(["git", *GIT_CONFIG, "commit", "-m", "Add submission from Lemmy post " + str(post_id)])
subprocess.check_call(["git", "push", "--force", "origin", new_branch_name])
pr_url = self.forge.does_pr_already_exist(new_branch_name)
if pr_url == False:
@@ -155,18 +191,18 @@ def process_post(self, post_body, make_pr):
f.close()
def send_comment(self, post_id, comment):
- req = requests.post("https://toast.ooo/api/v3/comment", json={
+ req = requests.post(ENDPOINTS["comment"], json={
"content": comment,
"post_id": post_id,
}, headers = {
- "Authorization": "Bearer " + self.lemmy_jwt
+ "Authorization": "Bearer " + JWT
})
print(req.content)
assert req.status_code == 200
print("Commented on post " + str(post_id))
def crawl_latest(self):
- req = requests.get("https://toast.ooo/api/v3/post/list", params={
+ req = requests.get(ENDPOINTS["list_posts"], params={
"community_name": EDITIONS[self.edition]["community"],
"type_": "All",
"sort": "New",
@@ -177,7 +213,7 @@ def crawl_latest(self):
data = req.json()
- print("Fetched " + str(len(data["posts"])) + " post fetched");
+ print("Fetched " + str(len(data["posts"])) + " posts");
for post in data["posts"]:
try:
self.process_post(post["post"], True)
@@ -186,9 +222,15 @@ def crawl_latest(self):
print(traceback.format_exc())
subprocess.check_call(["git", "checkout", "main"])
+login()
+
+lm2026 = LemmyFetcher("2026")
+lm2026.crawl_latest()
lm2025 = LemmyFetcher("2025")
lm2025.crawl_latest()
lm2024 = LemmyFetcher("2024")
lm2024.crawl_latest()
+
+logout()
\ No newline at end of file
diff --git a/tools/pre_process.py b/tools/pre_process.py
index 84268d9de9c7b545ddc6f0b2b8352feff8e51d83..65646e6c1ba443db2a3533c956318a96dd73d998 100644
--- a/tools/pre_process.py
+++ b/tools/pre_process.py
@@ -13,7 +13,7 @@ def process_edition(year, community):
.replace("{YEAR}", year) \
.replace("{COMMUNITY}", community)
- for known_year in ["2025", "2024"]:
+ for known_year in ["2026", "2025", "2024"]:
if known_year == year:
index = index.replace("{" + known_year + "ONLYCOMMENTSTART}", "")
@@ -32,6 +32,8 @@ def process_edition(year, community):
process_edition("2024", "2024lemmycanvasatlas")
elif sys.argv[1] == "2025":
process_edition("2025", "2025fedicanvasatlas")
+elif sys.argv[1] == "2026":
+ process_edition("2026", "2026canvasatlas")
else:
print("Unrecognizeable edition: " + sys.argv[1])
exit(-1)
diff --git a/web/_img/fedicanvas2026/final.png b/web/_img/fedicanvas2026/final.png
new file mode 100644
index 0000000000000000000000000000000000000000..fe3c62eb59a980118e2437810a05c44d49901b63
Binary files /dev/null and b/web/_img/fedicanvas2026/final.png differ
diff --git a/web/_js/config-2026.js b/web/_js/config-2026.js
new file mode 100644
index 0000000000000000000000000000000000000000..242acfc2009d1ecc803e177816601870e7c7ff26
--- /dev/null
+++ b/web/_js/config-2026.js
@@ -0,0 +1,262 @@
+// This script only applies to this instance of the Atlas.
+// Please also check code indicated with "@instanceonly" outside this file.
+// TODO: Avoid having instance-only code inside the main scripts to make updating easier.
+
+const prodDomain = "2026.canvas-atlas.fediverse.events";
+window.prodDomain = prodDomain;
+
+const instanceId = "canvas2026";
+window.instanceId = instanceId;
+
+const instanceSubreddit = null;
+window.instanceSubreddit = instanceSubreddit;
+
+const instanceRepo = null;
+window.instanceRepo = instanceRepo;
+
+const pageTitle = "2026 Fediverse Canvas Atlas";
+window.pageTitle = pageTitle;
+
+const canvasSize = {
+ x: 500,
+ y: 500,
+};
+window.canvasSize = canvasSize;
+
+const canvasOffset = {
+ x: 0,
+ y: 0,
+};
+window.canvasOffset = canvasOffset;
+
+const canvasCenter = {
+ x: canvasSize.x / 2 + canvasOffset.x,
+ y: canvasSize.y / 2 + canvasOffset.y,
+};
+window.canvasCenter = canvasCenter;
+
+const variationsConfig = {
+ default: {
+ name: "canvas",
+ code: "",
+ default: 48,
+ drawablePeriods: [1, 48],
+ drawableRegions: [
+ [
+ [0, 0],
+ [0, 0, 499, 499],
+ ],
+ ],
+ versions: [
+ {
+ timestamp: "blank",
+ url: "./_img/fedicanvas2025/canvas-1752285642.png",
+ },
+ {
+ timestamp: 1752289199,
+ url: "./_img/fedicanvas2025/canvas-1752289199.png",
+ },
+ {
+ timestamp: 1752292797,
+ url: "./_img/fedicanvas2025/canvas-1752292797.png",
+ },
+ {
+ timestamp: 1752296399,
+ url: "./_img/fedicanvas2025/canvas-1752296399.png",
+ },
+ {
+ timestamp: 1752299999,
+ url: "./_img/fedicanvas2025/canvas-1752299999.png",
+ },
+ {
+ timestamp: 1752303597,
+ url: "./_img/fedicanvas2025/canvas-1752303597.png",
+ },
+ {
+ timestamp: 1752307199,
+ url: "./_img/fedicanvas2025/canvas-1752307199.png",
+ },
+ {
+ timestamp: 1752310797,
+ url: "./_img/fedicanvas2025/canvas-1752310797.png",
+ },
+ {
+ timestamp: 1752314399,
+ url: "./_img/fedicanvas2025/canvas-1752314399.png",
+ },
+ {
+ timestamp: 1752317999,
+ url: "./_img/fedicanvas2025/canvas-1752317999.png",
+ },
+ {
+ timestamp: 1752321599,
+ url: "./_img/fedicanvas2025/canvas-1752321599.png",
+ },
+ {
+ timestamp: 1752325199,
+ url: "./_img/fedicanvas2025/canvas-1752325199.png",
+ },
+ {
+ timestamp: 1752328799,
+ url: "./_img/fedicanvas2025/canvas-1752328799.png",
+ },
+ {
+ timestamp: 1752332399,
+ url: "./_img/fedicanvas2025/canvas-1752332399.png",
+ },
+ {
+ timestamp: 1752335999,
+ url: "./_img/fedicanvas2025/canvas-1752335999.png",
+ },
+ {
+ timestamp: 1752339599,
+ url: "./_img/fedicanvas2025/canvas-1752339599.png",
+ },
+ {
+ timestamp: 1752343199,
+ url: "./_img/fedicanvas2025/canvas-1752343199.png",
+ },
+ {
+ timestamp: 1752346799,
+ url: "./_img/fedicanvas2025/canvas-1752346799.png",
+ },
+ {
+ timestamp: 1752350399,
+ url: "./_img/fedicanvas2025/canvas-1752350399.png",
+ },
+ {
+ timestamp: 1752353999,
+ url: "./_img/fedicanvas2025/canvas-1752353999.png",
+ },
+ {
+ timestamp: 1752357598,
+ url: "./_img/fedicanvas2025/canvas-1752357598.png",
+ },
+ {
+ timestamp: 1752361199,
+ url: "./_img/fedicanvas2025/canvas-1752361199.png",
+ },
+ {
+ timestamp: 1752364799,
+ url: "./_img/fedicanvas2025/canvas-1752364799.png",
+ },
+ {
+ timestamp: 1752368399,
+ url: "./_img/fedicanvas2025/canvas-1752368399.png",
+ },
+ {
+ timestamp: 1752371999,
+ url: "./_img/fedicanvas2025/canvas-1752371999.png",
+ },
+ {
+ timestamp: 1752375597,
+ url: "./_img/fedicanvas2025/canvas-1752375597.png",
+ },
+ {
+ timestamp: 1752379199,
+ url: "./_img/fedicanvas2025/canvas-1752379199.png",
+ },
+ {
+ timestamp: 1752382799,
+ url: "./_img/fedicanvas2025/canvas-1752382799.png",
+ },
+ {
+ timestamp: 1752386399,
+ url: "./_img/fedicanvas2025/canvas-1752386399.png",
+ },
+ {
+ timestamp: 1752389999,
+ url: "./_img/fedicanvas2025/canvas-1752389999.png",
+ },
+ {
+ timestamp: 1752393599,
+ url: "./_img/fedicanvas2025/canvas-1752393599.png",
+ },
+ {
+ timestamp: 1752397199,
+ url: "./_img/fedicanvas2025/canvas-1752397199.png",
+ },
+ {
+ timestamp: 1752400799,
+ url: "./_img/fedicanvas2025/canvas-1752400799.png",
+ },
+ {
+ timestamp: 1752404399,
+ url: "./_img/fedicanvas2025/canvas-1752404399.png",
+ },
+ {
+ timestamp: 1752407998,
+ url: "./_img/fedicanvas2025/canvas-1752407998.png",
+ },
+ {
+ timestamp: 1752411599,
+ url: "./_img/fedicanvas2025/canvas-1752411599.png",
+ },
+ {
+ timestamp: 1752415199,
+ url: "./_img/fedicanvas2025/canvas-1752415199.png",
+ },
+ {
+ timestamp: 1752418799,
+ url: "./_img/fedicanvas2025/canvas-1752418799.png",
+ },
+ {
+ timestamp: 1752422399,
+ url: "./_img/fedicanvas2025/canvas-1752422399.png",
+ },
+ {
+ timestamp: 1752425999,
+ url: "./_img/fedicanvas2025/canvas-1752425999.png",
+ },
+ {
+ timestamp: 1752429594,
+ url: "./_img/fedicanvas2025/canvas-1752429594.png",
+ },
+ {
+ timestamp: 1752433199,
+ url: "./_img/fedicanvas2025/canvas-1752433199.png",
+ },
+ {
+ timestamp: 1752436799,
+ url: "./_img/fedicanvas2025/canvas-1752436799.png",
+ },
+ {
+ timestamp: 1752440399,
+ url: "./_img/fedicanvas2025/canvas-1752440399.png",
+ },
+ {
+ timestamp: 1752443999,
+ url: "./_img/fedicanvas2025/canvas-1752443999.png",
+ },
+ {
+ timestamp: 1752447596,
+ url: "./_img/fedicanvas2025/canvas-1752447596.png",
+ },
+ {
+ timestamp: 1752451198,
+ url: "./_img/fedicanvas2025/canvas-1752451198.png",
+ },
+ {
+ timestamp: 1752454799,
+ url: "./_img/fedicanvas2025/canvas-1752454799.png",
+ },
+ {
+ timestamp: "final",
+ url: "./_img/fedicanvas2026/final.png",
+ },
+ ],
+ icon: '',
+ },
+};
+window.variationsConfig = variationsConfig;
+
+let defaultVariation = "default";
+window.defaultVariation = defaultVariation;
+
+let defaultPeriod = variationsConfig[defaultVariation].default;
+window.defaultPeriod = defaultPeriod;
+
+const useNumericalId = true;
+window.useNumericalId = useNumericalId;
+
+window.defaultAtlasURL = "./atlas/2026.json";
diff --git a/web/_js/config-common.js b/web/_js/config-common.js
index f67aed7157ce88ced53f81af4872bfcdf132eb42..1b4a68b655f97ca3d00d5555d940616cf9317346 100644
--- a/web/_js/config-common.js
+++ b/web/_js/config-common.js
@@ -116,6 +116,7 @@ console.info(
%cCopyright (c) 2017 Roland Rytz
Copyright (c) 2023 Place Atlas Initiative and contributors
Copyright (c) 2025 Fediverse Canvas Atlas contributors
+Copyright (c) 2026 Fediverse Canvas Atlas contributors
Licensed under AGPL-3.0 (https://2023.place-atlas.stefanocoding.me/license.txt)
https://sc07.dev/fediverse.events/canvas-atlas
@@ -123,5 +124,5 @@ https://sc07.dev/fediverse.events/canvas-atlas
To get the image of the canvas, use downloadCanvas().
`,
"font-size: 150%; line-height: 150%",
- ""
+ "",
);
diff --git a/web/index_template.html b/web/index_template.html
index ef7952dcb117fdd473c6c65e6c91e87c89bf2f14..9e469e596f394fd2bcd057c08d7fa7787a59c956 100644
--- a/web/index_template.html
+++ b/web/index_template.html
@@ -49,20 +49,20 @@
- {2025ONLYCOMMENTSTART}{NOT2026ONLYCOMMENTEND}