docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable

This commit is contained in:
Hernâni Marques 2026-07-13 10:12:00 +02:00
commit 96961f23f5
268 changed files with 24161 additions and 0 deletions

13
scripts/bonfire/README.md Normal file
View file

@ -0,0 +1,13 @@
# Bonfire scripts
Live: `/home/hernani/koopa-bonfire/`.
Docs: `configs/bonfire/README.md`, [Bonfire docs](https://docs.bonfirenetworks.org/).
| Script | Role |
|--------|------|
| `gitbot-mirror.py` | Poll git.ngi-0.eu, post via GraphQL, republish feeds |
| `install-systemd.sh` | Boot + gitbot units |
| `apply-branding.sh` | Logo + hacktivism theme (`bonfire remote`) |
| `publish-outbox-to-public.sql` | One-off outbox → public feeds |
No official gitbot or feed-republish guides — custom ops scripts.

View file

@ -0,0 +1,56 @@
# Apply hacktivism branding to Bonfire instance settings.
# Run on koopa: printf '%s\n' "$(cat apply-branding.exs)" | podman exec -i koopa-bonfire /opt/app/bin/bonfire remote
#
# Instance settings API: https://docs.bonfirenetworks.org/settings_system.html
palette = %{
"color-base-100" => "#1a1410",
"color-base-200" => "#221c16",
"color-base-300" => "#2a2018",
"color-base-content" => "#fff6e8",
"color-primary" => "#e8a838",
"color-primary-content" => "#1a1410",
"color-secondary" => "#3d3128",
"color-secondary-content" => "#fff6e8",
"color-accent" => "#3ecfbf",
"color-accent-content" => "#0e1c1e",
"color-neutral" => "#14110e",
"color-neutral-content" => "#ebe0d0",
"color-info" => "#2563eb",
"color-info-content" => "#ffffff",
"color-success" => "#16a34a",
"color-success-content" => "#ffffff",
"color-warning" => "#f0d090",
"color-warning-content" => "#1a1410",
"color-error" => "#b91c1c",
"color-error-content" => "#ffffff",
"radius-box" => "0.875rem",
"radius-field" => "0.5rem",
"radius-selector" => "0.75rem"
}
opts = [skip_boundary_check: true, scope: :instance]
settings = %{
ui: %{
theme: %{
instance_name: "hacktivism bonfire",
instance_tagline: "hacktivism magician - federated FOSS timeline",
instance_description:
"Bonfire on hacktivism.ch — local FOSS posts, gitbot commits, and federation.",
instance_icon: "/images/hacktivism-logo.svg",
instance_theme: "dark",
instance_theme_light: "light",
preferred: :custom,
custom_instance: palette
}
}
}
case Bonfire.Common.Settings.set(settings, opts) do
{:ok, _} ->
IO.puts("ok branding settings applied")
other ->
IO.inspect(other, label: "branding settings failed")
end

View file

@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Deploy hacktivism Bonfire branding (hacktivism magician logo + exchange-dark palette).
# Live root: ~/koopa-bonfire/
set -euo pipefail
ROOT="${KOOPA_BONFIRE_ROOT:-$HOME/koopa-bonfire}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd || true)"
BRANDING_DST="$ROOT/data/branding"
ENV_FILE="$ROOT/.env"
install -d -m 755 "$BRANDING_DST"
if [[ -d "$REPO_ROOT/configs/bonfire/assets/img" ]]; then
BRANDING_SRC="$REPO_ROOT/configs/bonfire/assets/img"
install -m 644 "$BRANDING_SRC/logo.svg" "$BRANDING_DST/logo.svg"
install -m 644 "$BRANDING_SRC/favicon.svg" "$BRANDING_DST/favicon.svg"
install -m 644 "$BRANDING_SRC/logo.png" "$BRANDING_DST/logo.png"
elif [[ ! -f "$BRANDING_DST/logo.svg" ]]; then
echo "missing $BRANDING_DST/logo.svg (copy assets or run from koopa-admin-log checkout)" >&2
exit 1
fi
# APP_NAME drives <title> suffix and og:site_name when instance_name is unset in HTML meta.
if [[ -f "$ENV_FILE" ]]; then
if grep -q '^APP_NAME=' "$ENV_FILE"; then
sed -i 's/^APP_NAME=.*/APP_NAME=hacktivism bonfire/' "$ENV_FILE"
else
echo 'APP_NAME=hacktivism bonfire' >> "$ENV_FILE"
fi
if grep -q '^INSTANCE_DESCRIPTION=' "$ENV_FILE"; then
sed -i 's/^INSTANCE_DESCRIPTION=.*/INSTANCE_DESCRIPTION=Bonfire on hacktivism.ch - FOSS posts and gitbot commits./' "$ENV_FILE"
else
echo 'INSTANCE_DESCRIPTION=Bonfire on hacktivism.ch - FOSS posts and gitbot commits.' >> "$ENV_FILE"
fi
fi
if [[ "$SCRIPT_DIR/apply-branding.exs" != "$ROOT/bin/apply-branding.exs" ]]; then
install -m 755 "$SCRIPT_DIR/apply-branding.exs" "$ROOT/bin/apply-branding.exs"
fi
echo "Applying instance theme settings via bonfire remote..."
sleep 2
printf '%s\n' "$(cat "$ROOT/bin/apply-branding.exs")" | podman exec -i koopa-bonfire /opt/app/bin/bonfire remote >/tmp/bonfire-branding.log 2>&1 || {
echo "remote apply failed; see /tmp/bonfire-branding.log" >&2
tail -30 /tmp/bonfire-branding.log >&2
exit 1
}
grep -q 'ok branding settings applied' /tmp/bonfire-branding.log
echo "Recreating web container (compose branding volume)..."
cd "$ROOT"
podman-compose up -d --no-deps --force-recreate web
echo "Done. Check https://bonfire.hacktivism.ch/ (hard-reload if cached)."

240
scripts/bonfire/gitbot-mirror.py Executable file
View file

@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""Poll git.ngi-0.eu and post new commits to Bonfire as gitbot."""
from __future__ import annotations
import json, os, pathlib, re, subprocess, sys, time, urllib.error, urllib.request
GQL = os.environ.get("BONFIRE_GQL", "http://127.0.0.1:9021/api/graphql")
GIT = os.environ.get("GIT_BASE", "https://git.ngi-0.eu")
STATE = pathlib.Path(os.environ.get("GITBOT_STATE", "/home/hernani/koopa-bonfire/gitbot-state.json"))
USERS = pathlib.Path(os.environ.get("BONFIRE_USERS_ENV", "/home/hernani/koopa-bonfire/users.env"))
INTERVAL = int(os.environ.get("GITBOT_INTERVAL", "300"))
MAX_FAILS = int(os.environ.get("GITBOT_MAX_FAILS", "3"))
MAX_TIME = 45
OUTBOX_FEED = os.environ.get(
"GITBOT_OUTBOX_FEED", "019f487a-df20-4ed0-a334-40ec3eac23e7"
)
FEED_INTERNET = os.environ.get(
"GITBOT_FEED_INTERNET", "0aab414c-eb0a-ac1d-8c81-ef0d74ec55da"
)
FEED_LOCAL = os.environ.get(
"GITBOT_FEED_LOCAL", "797632fc-029e-06f0-1031-410d73a5558e"
)
PUBLISH_CMD = os.environ.get(
"GITBOT_PUBLISH_CMD",
"podman exec -i koopa-bonfire-db psql -U postgres -d bonfire_db",
)
_BARE_HOST = re.compile(
r"(?<!https://)(?<!http://)"
r"(?<![\w./@-])"
r"((?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+(?:[a-zA-Z]{2,}))"
r"(?![\w./:])"
)
MUT = """
mutation CreatePost($pc: PostContentInput!) {
createPost(postContent: $pc) { id }
}
"""
def sanitize_text(text: str) -> str:
"""Add https:// to bare hostnames (Bonfire URI.parse breaks without it)."""
return _BARE_HOST.sub(r"https://\1", text)
def load_users():
d = {}
for line in USERS.read_text().splitlines():
if "=" in line and not line.strip().startswith("#"):
k, v = line.split("=", 1)
d[k.strip()] = v.strip()
return d
def http_json(url, data=None, headers=None, timeout=MAX_TIME):
req = urllib.request.Request(
url,
data=None if data is None else json.dumps(data).encode(),
headers=headers or {},
method="GET" if data is None else "POST",
)
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.load(r)
def gql(token, query, variables=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
body = {"query": query}
if variables is not None:
body["variables"] = variables
return http_json(GQL, body, headers)
def bonfire_ready() -> bool:
base = GQL.rsplit("/api/", 1)[0] + "/"
try:
with urllib.request.urlopen(base, timeout=10) as r:
return 200 <= r.status < 500
except (urllib.error.URLError, TimeoutError, OSError):
return False
def login(email_or_user, password):
res = gql(
None,
"mutation($e:String!,$p:String!){ login(emailOrUsername:$e, password:$p){ token currentUsername } }",
{"e": email_or_user, "p": password},
)
tok = (res.get("data") or {}).get("login") or {}
if not tok.get("token"):
raise RuntimeError(f"login failed: {res}")
return tok["token"]
def select_user(token, username):
res = gql(
token,
"mutation($u:String!){ selectUser(username:$u){ token currentUsername } }",
{"u": username},
)
t = (res.get("data") or {}).get("selectUser") or {}
return t.get("token") or token
def create_post(token, body, summary, name):
res = gql(
token,
MUT,
{"pc": {"htmlBody": body, "summary": summary[:140], "name": name}},
)
post = (res.get("data") or {}).get("createPost")
if not post:
raise RuntimeError(f"createPost failed: {res}")
return post["id"]
def post_commit(token, full, c):
sha = c["sha"]
short = sha[:8]
raw_msg = c["commit"]["message"].split("\n")[0][:140]
msg = sanitize_text(raw_msg)
html = c.get("html_url") or f"{GIT}/{full}/commit/{sha}"
author = (c.get("commit") or {}).get("author", {}).get("name") or "?"
full_body = (
f"[gitbot] commit {short} on {full}\n"
f"{msg}\nby {author}\n{html}\n#git #ngi0 #foss"
)
summary = sanitize_text(f"{full}@{short}: {msg}")[:140]
try:
return create_post(token, full_body, summary, f"commit {short}")
except RuntimeError:
minimal_body = f"[gitbot] commit {short} on {full}\n{html}\n#git #ngi0 #foss"
return create_post(token, minimal_body, f"{full}@{short}", f"commit {short}")
def load_state():
state = {"seen": [], "fails": {}}
if STATE.exists():
state = json.loads(STATE.read_text())
state.setdefault("seen", [])
state.setdefault("fails", {})
return state
def save_state(state, seen):
state["seen"] = list(seen)[-800:]
STATE.write_text(json.dumps(state, indent=2))
def publish_outbox_to_public() -> bool:
"""Copy foss outbox into local/internet feeds (ops workaround; no official republish API)."""
sql = f"""
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
SELECT fp.id, '{FEED_INTERNET}'::uuid
FROM bonfire_data_social_feed_publish fp
WHERE fp.feed_id = '{OUTBOX_FEED}'::uuid
ON CONFLICT DO NOTHING;
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
SELECT fp.id, '{FEED_LOCAL}'::uuid
FROM bonfire_data_social_feed_publish fp
WHERE fp.feed_id = '{OUTBOX_FEED}'::uuid
ON CONFLICT DO NOTHING;
"""
try:
proc = subprocess.run(
PUBLISH_CMD.split(),
input=sql,
text=True,
capture_output=True,
timeout=30,
check=False,
)
if proc.returncode != 0:
print("publish feeds failed:", proc.stderr.strip(), file=sys.stderr)
return False
print("published outbox -> local/internet feeds", flush=True)
return True
except (OSError, subprocess.TimeoutExpired) as e:
print("publish feeds error:", e, file=sys.stderr)
return False
def once():
if not bonfire_ready():
print("bonfire not ready, skipping cycle", flush=True)
return 0
u = load_users()
tok = login(u.get("FOSS_USER", "foss"), u["FOSS_PW"])
try:
tok = select_user(tok, u.get("GITBOT_USER", "gitbot"))
except Exception as e:
print("select gitbot skipped:", e, file=sys.stderr)
state = load_state()
seen = set(state["seen"])
fails = state["fails"]
skipped = set(state.get("skipped") or [])
repos = http_json(f"{GIT}/api/v1/repos/search?limit=30&sort=updated")["data"]
new_n = 0
for repo in repos:
full = repo["full_name"]
owner, name = full.split("/", 1)
try:
commits = http_json(f"{GIT}/api/v1/repos/{owner}/{name}/commits?limit=10")
except Exception as e:
print("repo fail", full, e, file=sys.stderr)
continue
for c in reversed(commits):
sha = c["sha"]
if sha in seen or sha in skipped:
continue
try:
pid = post_commit(tok, full, c)
print(f"posted {full}@{sha[:8]} -> {pid}", flush=True)
seen.add(sha)
fails.pop(sha, None)
new_n += 1
save_state(state, seen)
time.sleep(0.4)
except Exception as e:
n = fails.get(sha, 0) + 1
fails[sha] = n
state["fails"] = fails
save_state(state, seen)
print("post fail", full, sha[:8], f"try={n}", e, file=sys.stderr)
if n >= MAX_FAILS:
skipped.add(sha)
state["skipped"] = list(skipped)[-200:]
save_state(state, seen)
print("skip", full, sha[:8], "after repeated failures", file=sys.stderr)
publish_outbox_to_public()
print(f"done new={new_n} tracked={len(seen)} skipped={len(skipped)}", flush=True)
return new_n
def main():
loop = "--loop" in sys.argv
if loop:
print(f"gitbot loop interval={INTERVAL}s gql={GQL}", flush=True)
while True:
try:
once()
except Exception as e:
print("cycle error:", e, file=sys.stderr, flush=True)
time.sleep(INTERVAL)
else:
once()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Install Bonfire + gitbot user systemd units on koopa.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
UNIT_DIR="${HOME}/.config/systemd/user"
BIN_DIR="${HOME}/koopa-bonfire/bin"
mkdir -p "${UNIT_DIR}" "${BIN_DIR}"
install -m 755 "${ROOT}/scripts/bonfire/gitbot-mirror.py" "${BIN_DIR}/gitbot-mirror.py"
cp "${ROOT}/configs/bonfire/container-koopa-bonfire-db.service" "${UNIT_DIR}/"
cp "${ROOT}/configs/bonfire/container-koopa-bonfire.service" "${UNIT_DIR}/"
cp "${ROOT}/configs/bonfire/gitbot-mirror.service" "${UNIT_DIR}/"
systemctl --user daemon-reload
systemctl --user enable container-koopa-bonfire-db.service container-koopa-bonfire.service gitbot-mirror.service
if [[ -f "${HOME}/koopa-bonfire/gitbot.pid" ]]; then
old_pid="$(cat "${HOME}/koopa-bonfire/gitbot.pid" 2>/dev/null || true)"
[[ -n "${old_pid}" ]] && kill "${old_pid}" 2>/dev/null || true
rm -f "${HOME}/koopa-bonfire/gitbot.pid"
fi
if ! podman ps --format '{{.Names}}' | grep -qx koopa-bonfire; then
(cd "${HOME}/koopa-bonfire" && podman-compose up -d)
fi
systemctl --user start container-koopa-bonfire-db.service container-koopa-bonfire.service
systemctl --user restart gitbot-mirror.service
systemctl --user --no-pager status container-koopa-bonfire-db.service container-koopa-bonfire.service gitbot-mirror.service

View file

@ -0,0 +1,28 @@
-- Republish outbox activities into guest-visible feeds (ops workaround).
-- FeedPublish model: https://docs.bonfirenetworks.org/feed_structure.html
-- No official republish procedure. Default outbox = foss.
-- Run:
-- podman exec -i koopa-bonfire-db psql -U postgres -d bonfire_db < publish-outbox-to-public.sql
\set OUTBOX_FEED '019f487a-df20-4ed0-a334-40ec3eac23e7'
\set FEED_INTERNET '0aab414c-eb0a-ac1d-8c81-ef0d74ec55da'
\set FEED_LOCAL_USERS '797632fc-029e-06f0-1031-410d73a5558e'
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
SELECT fp.id, :'FEED_INTERNET'::uuid
FROM bonfire_data_social_feed_publish fp
WHERE fp.feed_id = :'OUTBOX_FEED'::uuid
ON CONFLICT DO NOTHING;
INSERT INTO bonfire_data_social_feed_publish (id, feed_id)
SELECT fp.id, :'FEED_LOCAL_USERS'::uuid
FROM bonfire_data_social_feed_publish fp
WHERE fp.feed_id = :'OUTBOX_FEED'::uuid
ON CONFLICT DO NOTHING;
SELECT n.name, count(*)
FROM bonfire_data_social_feed_publish fp
LEFT JOIN bonfire_data_social_named n ON n.id = fp.feed_id
GROUP BY 1
ORDER BY 2 DESC
LIMIT 15;