Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ Add-on skills (activated when corresponding add-on is installed):
- `trilium.md` — TriliumNext knowledge base: note search, creation, web clipping, organization
- `knowledge-base.md` — Multilingual knowledge base: create, edit, publish, search, verify resources, share articles, LAN discovery
- `maker-lab.md` — STEM education companion for kids: scaffolded AI tutor, hint-ladder pedagogy, age-banded personas (kid/tween/adult), solo/family/classroom modes, guest sidecar
- `gotosocial.md` — GoToSocial ActivityPub microblog: post, follow, search, moderate (block_user/mute inline; defederate/block_domain/import_blocklist queued for operator confirmation), media prune, federation health
- `calibre-server.md` — Calibre content server: search, browse, download ebooks via OPDS
- `calibre-web.md` — Calibre-Web reader: search, shelves, reading status, download
- `miniflux.md` — Miniflux RSS reader: subscribe feeds, read articles, star, mark read
Expand Down
32 changes: 32 additions & 0 deletions bundles/gotosocial/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# GoToSocial — required config

# Public domain dedicated to GoToSocial (must be a subdomain; subpath mounts
# break ActivityPub federation because actors are URL-keyed).
GTS_HOST=gts.example.com

# Account handle domain. Usually the same as GTS_HOST. If different, you
# must add a .well-known/webfinger delegation on the apex (caddy_set_wellknown).
GTS_ACCOUNT_DOMAIN=gts.example.com

# Internal URL the Crow MCP server uses to reach GoToSocial. Leave as-is
# when running over the shared crow-federation docker network.
GTS_URL=http://gotosocial:8080

# API access token for the admin account. Generate via the web UI after
# first login, or via the CLI:
# docker exec crow-gotosocial ./gotosocial --config-path /gotosocial/config.yaml \
# admin account create-token --username <admin>
# Leaving this unset limits the Crow tools to public read-only operations.
GTS_ACCESS_TOKEN=

# Remote media retention. Pi-class hosts should lower to 7; x86 hosts with
# large disks can raise to 30+.
GTS_MEDIA_RETENTION_DAYS=14

# Optional: IFTAS / The Bad Space / custom domain blocklist URL. Imported
# once at post-install if set (subsequent imports go through the standard
# moderation confirmation flow).
# GTS_IMPORT_BLOCKLIST=https://iftas.org/example-blocklist.txt

# Host data directory override (default ~/.crow/gotosocial).
# GTS_DATA_DIR=/mnt/nvme/gotosocial
50 changes: 50 additions & 0 deletions bundles/gotosocial/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# GoToSocial — lightweight ActivityPub server.
#
# Deployment model: no host port publish. Caddy (crow-federation network)
# reaches the container by service name (gotosocial:8080) and terminates TLS
# at 443. Add the site with caddy_add_federation_site after install.
#
# Data lives at ~/.crow/gotosocial/ (single bind mount). SQLite is the
# default backend; switching to Postgres requires manual compose edit.

networks:
crow-federation:
external: true
default:

services:
gotosocial:
image: superseriousbusiness/gotosocial:0.18.0
container_name: crow-gotosocial
networks:
- default
- crow-federation
environment:
GTS_HOST: ${GTS_HOST}
GTS_ACCOUNT_DOMAIN: ${GTS_ACCOUNT_DOMAIN:-${GTS_HOST}}
GTS_PROTOCOL: https
GTS_PORT: "8080"
GTS_BIND_ADDRESS: 0.0.0.0
# SQLite by default — simplest for Pi-class hosts. Switch to postgres
# by setting GTS_DB_TYPE=postgres plus GTS_DB_ADDRESS/USER/PASSWORD.
GTS_DB_TYPE: sqlite
GTS_DB_ADDRESS: /gotosocial/storage/sqlite.db
GTS_STORAGE_BACKEND: local
GTS_STORAGE_LOCAL_BASE_PATH: /gotosocial/storage
GTS_LETSENCRYPT_ENABLED: "false" # Caddy handles TLS
GTS_TRUSTED_PROXIES: 172.16.0.0/12,10.0.0.0/8
GTS_MEDIA_REMOTE_CACHE_DAYS: ${GTS_MEDIA_RETENTION_DAYS:-14}
# Advertise-proto: behind Caddy the client sees HTTPS; tell GTS the
# external scheme so generated URLs are correct.
GTS_ADVERTISE_HTTPS: "true"
volumes:
- ${GTS_DATA_DIR:-~/.crow/gotosocial}:/gotosocial/storage
init: true
mem_limit: 1g
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/readyz || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 20s
71 changes: 71 additions & 0 deletions bundles/gotosocial/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"id": "gotosocial",
"name": "GoToSocial",
"version": "1.0.0",
"description": "Lightweight ActivityPub microblog server (fediverse). Pi-friendly single-binary alternative to Mastodon — joins the public fediverse with a sub-500 MB footprint.",
"type": "bundle",
"author": "Crow",
"category": "federated-social",
"tags": ["activitypub", "fediverse", "microblog", "federated", "mastodon-compatible"],
"icon": "globe",
"docker": { "composefile": "docker-compose.yml" },
"server": {
"command": "node",
"args": ["server/index.js"],
"envKeys": ["GTS_URL", "GTS_ACCESS_TOKEN"]
},
"panel": "panel/gotosocial.js",
"panelRoutes": "panel/routes.js",
"skills": ["skills/gotosocial.md"],
"consent_required": true,
"install_consent_messages": {
"en": "GoToSocial joins the fediverse: your instance becomes publicly addressable under the domain you configure, and any content you publish is replicated to other ActivityPub servers. Replicated content cannot be fully recalled — deletions may not reach every server that cached your post. The instance fetches and caches content and profile images from other federated servers, which may include objectionable material. You are responsible for moderating your instance and for legal compliance in your jurisdiction. If your instance is reported for abuse, major hubs (mastodon.social, matrix.org) may defederate your domain — and a poisoned domain cannot easily be rehabilitated; you may need to move to a fresh domain. Automatic media pruning (14-day retention by default, 7 days on Pi-class hosts) is enabled from day 1; remote media caches still grow substantially under active federation.",
"es": "GoToSocial se une al fediverso: tu instancia será públicamente direccionable bajo el dominio que configures, y todo el contenido que publiques se replica a otros servidores ActivityPub. El contenido replicado no puede recuperarse completamente — las eliminaciones pueden no llegar a todos los servidores que guardaron tu publicación. La instancia obtiene y guarda contenido e imágenes de perfil de otros servidores federados, que pueden incluir material objetable. Eres responsable de moderar tu instancia y cumplir con la ley de tu jurisdicción. Si tu instancia es reportada por abuso, los nodos principales (mastodon.social, matrix.org) pueden dejar de federarse con tu dominio — y un dominio envenenado no puede rehabilitarse fácilmente; puede que tengas que mudarte a un dominio nuevo. La limpieza automática de medios (14 días por defecto, 7 días en hosts tipo Pi) está activa desde el primer día; los cachés de medios remotos igualmente crecen bajo federación activa."
},
"requires": {
"env": ["GTS_HOST", "GTS_ACCOUNT_DOMAIN"],
"bundles": ["caddy"],
"min_ram_mb": 512,
"recommended_ram_mb": 1024,
"min_disk_mb": 2000,
"recommended_disk_mb": 20000
},
"env_vars": [
{
"name": "GTS_HOST",
"description": "Public domain for this GoToSocial instance (e.g., gts.example.com). Must be a subdomain dedicated to GoToSocial — ActivityPub actors are URL-keyed and subpath mounts break federation.",
"required": true
},
{
"name": "GTS_ACCOUNT_DOMAIN",
"description": "Account domain used in @user@domain handles. Usually the same as GTS_HOST. If your apex is example.com and GTS_HOST=gts.example.com, set this to example.com and add a .well-known/webfinger delegation on the apex.",
"required": true
},
{
"name": "GTS_URL",
"description": "Internal URL the Crow MCP server uses to reach GoToSocial (over the crow-federation docker network).",
"default": "http://gotosocial:8080",
"required": false
},
{
"name": "GTS_ACCESS_TOKEN",
"description": "API access token for the admin account (generated via the web UI after first login, or via the CLI: docker exec gotosocial ./gotosocial admin account create-token).",
"required": false,
"secret": true
},
{
"name": "GTS_MEDIA_RETENTION_DAYS",
"description": "Days to keep remote media in the local cache before pruning. Default 14; Pi-class hosts should lower to 7.",
"default": "14",
"required": false
},
{
"name": "GTS_IMPORT_BLOCKLIST",
"description": "Optional: URL to an IFTAS / Bad Space / custom domain blocklist (one domain per line). Imported on first post-install run if set.",
"required": false
}
],
"ports": [],
"webUI": null,
"notes": "Subdomain-only deployment (federation requires URL-keyed actors). Caddy reaches GoToSocial by docker service name (gotosocial:8080) over the crow-federation network; no host port is published. After install, run caddy_add_federation_site { domain: GTS_HOST, upstream: 'gotosocial:8080', profile: 'activitypub', wellknown: { nodeinfo: { href: 'https://<GTS_HOST>/nodeinfo/2.0' } } } to expose the instance with a real LE cert."
}
11 changes: 11 additions & 0 deletions bundles/gotosocial/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "crow-gotosocial",
"version": "1.0.0",
"description": "GoToSocial MCP server — post, follow, search, moderate across the fediverse",
"type": "module",
"main": "server/index.js",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.24.0"
}
}
188 changes: 188 additions & 0 deletions bundles/gotosocial/panel/gotosocial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* Crow's Nest Panel — GoToSocial: status + recent timeline preview
*
* Read-only view. Moderation queue confirmation UI lands with F.11/F.12;
* until then the operator confirms queued actions via direct DB edits or
* a follow-up panel enhancement.
*
* XSS-safe: textContent / createElement only.
*/

export default {
id: "gotosocial",
name: "GoToSocial",
icon: "globe",
route: "/dashboard/gotosocial",
navOrder: 70,
category: "federated-social",

async handler(req, res, { layout }) {
const content = `
<style>${styles()}</style>
<div class="gts-panel">
<h1>GoToSocial <span class="gts-subtitle">fediverse microblog</span></h1>

<div class="gts-section">
<h3>Status</h3>
<div id="gts-status" class="gts-status"><div class="np-loading">Loading…</div></div>
</div>

<div class="gts-section">
<h3>Recent Timeline</h3>
<div class="gts-toggle">
<button id="gts-tl-public" type="button" class="gts-tab-active">Public</button>
<button id="gts-tl-home" type="button">Home</button>
</div>
<div id="gts-timeline" class="gts-timeline"><div class="np-loading">Loading…</div></div>
</div>

<div class="gts-section gts-notes">
<h3>Notes</h3>
<ul>
<li>Moderation actions (defederate, block_domain, import_blocklist) queue pending rows in <code>moderation_actions</code>. Operator confirmation UI lands in a later release.</li>
<li>Remote media cache prunes daily via <code>scripts/media-prune.sh</code>. Override retention via <code>GTS_MEDIA_RETENTION_DAYS</code>.</li>
<li>Exposed via Caddy. Verify TLS with <code>caddy_cert_health</code>.</li>
</ul>
</div>
</div>
<script>${script()}</script>
`;
res.send(layout({ title: "GoToSocial", content }));
},
};

function script() {
return `
function clearNode(el) { while (el.firstChild) el.removeChild(el.firstChild); }
function row(label, value) {
const r = document.createElement('div');
r.className = 'gts-row';
const b = document.createElement('b');
b.textContent = label;
r.appendChild(b);
const s = document.createElement('span');
s.textContent = value == null ? '—' : String(value);
r.appendChild(s);
return r;
}
function errorNode(msg) {
const d = document.createElement('div');
d.className = 'np-error';
d.textContent = msg;
return d;
}

async function loadStatus() {
const el = document.getElementById('gts-status');
clearNode(el);
try {
const res = await fetch('/api/gotosocial/status');
const d = await res.json();
if (d.error) { el.appendChild(errorNode(d.error)); return; }
const card = document.createElement('div');
card.className = 'gts-card';
card.appendChild(row('Instance', d.uri));
card.appendChild(row('Title', d.title));
card.appendChild(row('Version', d.version));
card.appendChild(row('Users', d.stats?.user_count));
card.appendChild(row('Statuses', d.stats?.status_count));
card.appendChild(row('Federated peers', d.federated_peers));
card.appendChild(row('Authenticated as', d.account ? '@' + d.account.acct : '(none — set GTS_ACCESS_TOKEN)'));
el.appendChild(card);
} catch (e) {
el.appendChild(errorNode('Cannot reach GoToSocial API.'));
}
}

async function loadTimeline(source) {
const el = document.getElementById('gts-timeline');
clearNode(el);
try {
const res = await fetch('/api/gotosocial/timeline?source=' + encodeURIComponent(source) + '&limit=10');
const d = await res.json();
if (d.error) { el.appendChild(errorNode(d.error)); return; }
if (!d.items || d.items.length === 0) {
const e = document.createElement('div');
e.className = 'np-idle';
e.textContent = 'Timeline is empty.';
el.appendChild(e);
return;
}
for (const it of d.items) {
const card = document.createElement('div');
card.className = 'gts-toot';
const head = document.createElement('div');
head.className = 'gts-toot-head';
const author = document.createElement('b');
author.textContent = '@' + (it.acct || 'unknown');
head.appendChild(author);
const when = document.createElement('span');
when.className = 'gts-toot-when';
when.textContent = new Date(it.created_at).toLocaleString();
head.appendChild(when);
card.appendChild(head);
const body = document.createElement('div');
body.className = 'gts-toot-body';
body.textContent = it.content_excerpt || '';
card.appendChild(body);
const meta = document.createElement('div');
meta.className = 'gts-toot-meta';
meta.textContent = 'reblogs ' + (it.reblogs || 0) + ' \u2022 favs ' + (it.favs || 0);
card.appendChild(meta);
el.appendChild(card);
}
} catch (e) {
el.appendChild(errorNode('Cannot load timeline: ' + e.message));
}
}

document.getElementById('gts-tl-public').addEventListener('click', function () {
document.getElementById('gts-tl-public').className = 'gts-tab-active';
document.getElementById('gts-tl-home').className = '';
loadTimeline('public');
});
document.getElementById('gts-tl-home').addEventListener('click', function () {
document.getElementById('gts-tl-home').className = 'gts-tab-active';
document.getElementById('gts-tl-public').className = '';
loadTimeline('home');
});
loadStatus();
loadTimeline('public');
`;
}

function styles() {
return `
.gts-panel h1 { margin: 0 0 1rem; font-size: 1.5rem; }
.gts-subtitle { font-size: 0.85rem; color: var(--crow-text-muted); font-weight: 400; margin-left: .5rem; }
.gts-section { margin-bottom: 1.8rem; }
.gts-section h3 { font-size: 0.8rem; color: var(--crow-text-muted); text-transform: uppercase;
letter-spacing: 0.05em; margin: 0 0 0.7rem; }
.gts-card { background: var(--crow-bg-elevated); border: 1px solid var(--crow-border);
border-radius: 10px; padding: 1rem; }
.gts-row { display: flex; justify-content: space-between; gap: 1rem; padding: .25rem 0;
font-size: .9rem; color: var(--crow-text-primary); }
.gts-row b { color: var(--crow-text-muted); font-weight: 500; min-width: 140px; }
.gts-toggle { display: flex; gap: .4rem; margin-bottom: .7rem; }
.gts-toggle button { background: var(--crow-bg-elevated); color: var(--crow-text-muted);
border: 1px solid var(--crow-border); border-radius: 6px;
padding: .3rem .7rem; font-size: .85rem; cursor: pointer; }
.gts-tab-active { background: var(--crow-accent) !important; color: #0b0d10 !important;
border-color: var(--crow-accent) !important; }
.gts-toot { background: var(--crow-bg-elevated); border: 1px solid var(--crow-border);
border-radius: 10px; padding: .8rem 1rem; margin-bottom: .5rem; }
.gts-toot-head { display: flex; justify-content: space-between; margin-bottom: .3rem; }
.gts-toot-head b { font-size: .9rem; color: var(--crow-text-primary); }
.gts-toot-when { font-size: .75rem; color: var(--crow-text-muted); font-family: ui-monospace, monospace; }
.gts-toot-body { font-size: .9rem; color: var(--crow-text-primary); line-height: 1.4; }
.gts-toot-meta { font-size: .75rem; color: var(--crow-text-muted); margin-top: .4rem; }
.gts-notes ul { margin: 0; padding-left: 1.2rem; color: var(--crow-text-secondary); font-size: .88rem; }
.gts-notes li { margin-bottom: .3rem; }
.gts-notes code { font-family: ui-monospace, monospace; background: var(--crow-bg);
padding: 1px 4px; border-radius: 3px; font-size: .8em; }
.np-idle, .np-loading { color: var(--crow-text-muted); font-size: 0.9rem; padding: 1rem;
background: var(--crow-bg-elevated); border-radius: 10px; text-align: center; }
.np-error { color: var(--crow-error, #ef4444); font-size: 0.9rem; padding: 1rem;
background: var(--crow-bg-elevated); border-radius: 10px; text-align: center; }
`;
}
Loading
Loading