docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable
This commit is contained in:
commit
96961f23f5
268 changed files with 24161 additions and 0 deletions
13
scripts/bonfire/README.md
Normal file
13
scripts/bonfire/README.md
Normal 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.
|
||||
56
scripts/bonfire/apply-branding.exs
Normal file
56
scripts/bonfire/apply-branding.exs
Normal 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
|
||||
55
scripts/bonfire/apply-branding.sh
Executable file
55
scripts/bonfire/apply-branding.sh
Executable 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
240
scripts/bonfire/gitbot-mirror.py
Executable 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()
|
||||
29
scripts/bonfire/install-systemd.sh
Executable file
29
scripts/bonfire/install-systemd.sh
Executable 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
|
||||
28
scripts/bonfire/publish-outbox-to-public.sql
Normal file
28
scripts/bonfire/publish-outbox-to-public.sql
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue