From f20bb1df6b8f80fec60ca7830ba11ba03a51dab1 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 13 Feb 2026 13:52:58 +0100 Subject: [PATCH] feat(gotosocial): Add GoToSocial Fediverse server packages Add secubox-app-gotosocial and luci-app-gotosocial for running a lightweight ActivityPub social network server in LXC container. Features: - gotosocialctl CLI with install, start, stop, user management - LXC container deployment (ARM64) - HAProxy integration via emancipate command - UCI configuration for instance, container, proxy, federation settings - LuCI web interface with overview, users, and settings tabs - Mesh integration support for auto-federation between SecuBox nodes - Backup/restore functionality Co-Authored-By: Claude Opus 4.5 --- package/secubox/luci-app-gotosocial/Makefile | 29 + .../resources/view/gotosocial/overview.js | 399 +++++++++ .../resources/view/gotosocial/settings.js | 143 ++++ .../resources/view/gotosocial/users.js | 258 ++++++ .../root/usr/libexec/rpcd/luci.gotosocial | 264 ++++++ .../luci/menu.d/luci-app-gotosocial.json | 37 + .../share/rpcd/acl.d/luci-app-gotosocial.json | 17 + .../secubox/secubox-app-gotosocial/Makefile | 39 + .../files/etc/config/gotosocial | 39 + .../files/etc/init.d/gotosocial | 33 + .../files/usr/sbin/gotosocialctl | 778 ++++++++++++++++++ .../usr/share/gotosocial/config.yaml.template | 34 + 12 files changed, 2070 insertions(+) create mode 100644 package/secubox/luci-app-gotosocial/Makefile create mode 100644 package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/overview.js create mode 100644 package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/settings.js create mode 100644 package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/users.js create mode 100644 package/secubox/luci-app-gotosocial/root/usr/libexec/rpcd/luci.gotosocial create mode 100644 package/secubox/luci-app-gotosocial/root/usr/share/luci/menu.d/luci-app-gotosocial.json create mode 100644 package/secubox/luci-app-gotosocial/root/usr/share/rpcd/acl.d/luci-app-gotosocial.json create mode 100644 package/secubox/secubox-app-gotosocial/Makefile create mode 100644 package/secubox/secubox-app-gotosocial/files/etc/config/gotosocial create mode 100644 package/secubox/secubox-app-gotosocial/files/etc/init.d/gotosocial create mode 100644 package/secubox/secubox-app-gotosocial/files/usr/sbin/gotosocialctl create mode 100644 package/secubox/secubox-app-gotosocial/files/usr/share/gotosocial/config.yaml.template diff --git a/package/secubox/luci-app-gotosocial/Makefile b/package/secubox/luci-app-gotosocial/Makefile new file mode 100644 index 00000000..8d855763 --- /dev/null +++ b/package/secubox/luci-app-gotosocial/Makefile @@ -0,0 +1,29 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI app for GoToSocial Fediverse Server +LUCI_DEPENDS:=+secubox-app-gotosocial +luci-base + +PKG_NAME:=luci-app-gotosocial +PKG_VERSION:=0.1.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=MIT + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-gotosocial/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.gotosocial $(1)/usr/libexec/rpcd/luci.gotosocial + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-gotosocial.json $(1)/usr/share/rpcd/acl.d/luci-app-gotosocial.json + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-gotosocial.json $(1)/usr/share/luci/menu.d/luci-app-gotosocial.json + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/gotosocial + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/gotosocial/*.js $(1)/www/luci-static/resources/view/gotosocial/ +endef + +$(eval $(call BuildPackage,luci-app-gotosocial)) diff --git a/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/overview.js b/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/overview.js new file mode 100644 index 00000000..b3d26f22 --- /dev/null +++ b/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/overview.js @@ -0,0 +1,399 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; + +var callStatus = rpc.declare({ + object: 'luci.gotosocial', + method: 'status', + expect: {} +}); + +var callInstall = rpc.declare({ + object: 'luci.gotosocial', + method: 'install', + expect: {} +}); + +var callStart = rpc.declare({ + object: 'luci.gotosocial', + method: 'start', + expect: {} +}); + +var callStop = rpc.declare({ + object: 'luci.gotosocial', + method: 'stop', + expect: {} +}); + +var callRestart = rpc.declare({ + object: 'luci.gotosocial', + method: 'restart', + expect: {} +}); + +var callEmancipate = rpc.declare({ + object: 'luci.gotosocial', + method: 'emancipate', + params: ['domain', 'tor', 'dns', 'mesh'], + expect: {} +}); + +var callRevoke = rpc.declare({ + object: 'luci.gotosocial', + method: 'revoke', + expect: {} +}); + +var callBackup = rpc.declare({ + object: 'luci.gotosocial', + method: 'backup', + expect: {} +}); + +var callLogs = rpc.declare({ + object: 'luci.gotosocial', + method: 'logs', + params: ['lines'], + expect: {} +}); + +return view.extend({ + status: null, + + load: function() { + return callStatus(); + }, + + pollStatus: function() { + return callStatus().then(L.bind(function(status) { + this.status = status; + this.updateStatusDisplay(status); + }, this)); + }, + + updateStatusDisplay: function(status) { + var containerEl = document.getElementById('container-status'); + var serviceEl = document.getElementById('service-status'); + var versionEl = document.getElementById('gts-version'); + var hostEl = document.getElementById('gts-host'); + var exposureEl = document.getElementById('exposure-status'); + + if (containerEl) { + if (status.container_running) { + containerEl.textContent = 'Running'; + containerEl.className = 'badge success'; + } else if (status.installed) { + containerEl.textContent = 'Stopped'; + containerEl.className = 'badge warning'; + } else { + containerEl.textContent = 'Not Installed'; + containerEl.className = 'badge danger'; + } + } + + if (serviceEl) { + if (status.service_running) { + serviceEl.textContent = 'Running'; + serviceEl.className = 'badge success'; + } else { + serviceEl.textContent = 'Stopped'; + serviceEl.className = 'badge warning'; + } + } + + if (versionEl) { + versionEl.textContent = status.version || '-'; + } + + if (hostEl) { + hostEl.textContent = status.host || '-'; + } + + if (exposureEl) { + var channels = []; + if (status.tor_enabled) channels.push('Tor'); + if (status.dns_enabled) channels.push('DNS/SSL'); + if (status.mesh_enabled) channels.push('Mesh'); + exposureEl.textContent = channels.length > 0 ? channels.join(', ') : 'None'; + } + + // Update button states + var installBtn = document.getElementById('btn-install'); + var startBtn = document.getElementById('btn-start'); + var stopBtn = document.getElementById('btn-stop'); + var restartBtn = document.getElementById('btn-restart'); + + if (installBtn) installBtn.disabled = status.installed; + if (startBtn) startBtn.disabled = !status.installed || status.container_running; + if (stopBtn) stopBtn.disabled = !status.container_running; + if (restartBtn) restartBtn.disabled = !status.container_running; + }, + + handleInstall: function() { + return ui.showModal(_('Install GoToSocial'), [ + E('p', _('This will download and install GoToSocial in an LXC container. This may take several minutes.')), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action important', + 'click': ui.createHandlerFn(this, function() { + ui.hideModal(); + ui.showModal(_('Installing...'), [ + E('p', { 'class': 'spinning' }, _('Installing GoToSocial, please wait...')) + ]); + return callInstall().then(function(res) { + ui.hideModal(); + if (res.success) { + ui.addNotification(null, E('p', _('GoToSocial installed successfully')), 'success'); + } else { + ui.addNotification(null, E('p', res.error || _('Installation failed')), 'error'); + } + return callStatus(); + }).then(L.bind(function(status) { + this.updateStatusDisplay(status); + }, this)); + }) + }, _('Install')) + ]) + ]); + }, + + handleStart: function() { + ui.showModal(_('Starting...'), [ + E('p', { 'class': 'spinning' }, _('Starting GoToSocial...')) + ]); + return callStart().then(L.bind(function(res) { + ui.hideModal(); + ui.addNotification(null, E('p', res.message || _('GoToSocial started')), 'success'); + return this.pollStatus(); + }, this)); + }, + + handleStop: function() { + return callStop().then(L.bind(function(res) { + ui.addNotification(null, E('p', res.message || _('GoToSocial stopped')), 'info'); + return this.pollStatus(); + }, this)); + }, + + handleRestart: function() { + ui.showModal(_('Restarting...'), [ + E('p', { 'class': 'spinning' }, _('Restarting GoToSocial...')) + ]); + return callRestart().then(L.bind(function(res) { + ui.hideModal(); + ui.addNotification(null, E('p', res.message || _('GoToSocial restarted')), 'success'); + return this.pollStatus(); + }, this)); + }, + + handleEmancipate: function() { + var domain = this.status && this.status.host ? this.status.host : ''; + + return ui.showModal(_('Expose Service'), [ + E('p', _('Configure exposure channels for your Fediverse instance.')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Domain')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'text', 'id': 'emancipate-domain', 'class': 'cbi-input-text', 'value': domain }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Channels')), + E('div', { 'class': 'cbi-value-field' }, [ + E('label', { 'style': 'display:block' }, [ + E('input', { 'type': 'checkbox', 'id': 'emancipate-tor' }), ' ', _('Tor (.onion)') + ]), + E('label', { 'style': 'display:block' }, [ + E('input', { 'type': 'checkbox', 'id': 'emancipate-dns', 'checked': true }), ' ', _('DNS/SSL (HTTPS)') + ]), + E('label', { 'style': 'display:block' }, [ + E('input', { 'type': 'checkbox', 'id': 'emancipate-mesh' }), ' ', _('Mesh Network') + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, function() { + var domain = document.getElementById('emancipate-domain').value; + var tor = document.getElementById('emancipate-tor').checked; + var dns = document.getElementById('emancipate-dns').checked; + var mesh = document.getElementById('emancipate-mesh').checked; + + ui.hideModal(); + ui.showModal(_('Exposing...'), [ + E('p', { 'class': 'spinning' }, _('Setting up exposure channels...')) + ]); + + return callEmancipate(domain, tor, dns, mesh).then(function(res) { + ui.hideModal(); + if (res.success) { + ui.addNotification(null, E('p', res.message || _('Service exposed successfully')), 'success'); + } else { + ui.addNotification(null, E('p', res.error || _('Exposure failed')), 'error'); + } + }); + }) + }, _('Expose')) + ]) + ]); + }, + + handleRevoke: function() { + return ui.showModal(_('Revoke Exposure'), [ + E('p', _('This will remove all exposure channels for GoToSocial. The service will no longer be accessible externally.')), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-negative', + 'click': ui.createHandlerFn(this, function() { + ui.hideModal(); + return callRevoke().then(function(res) { + ui.addNotification(null, E('p', res.message || _('Exposure revoked')), 'info'); + }); + }) + }, _('Revoke')) + ]) + ]); + }, + + handleBackup: function() { + ui.showModal(_('Creating Backup...'), [ + E('p', { 'class': 'spinning' }, _('Creating backup...')) + ]); + return callBackup().then(function(res) { + ui.hideModal(); + if (res.success) { + ui.addNotification(null, E('p', res.message || _('Backup created')), 'success'); + } else { + ui.addNotification(null, E('p', res.error || _('Backup failed')), 'error'); + } + }); + }, + + handleViewLogs: function() { + return callLogs(100).then(function(res) { + var logs = res.logs || []; + ui.showModal(_('GoToSocial Logs'), [ + E('div', { 'style': 'max-height:400px; overflow-y:auto; font-family:monospace; font-size:12px; background:#111; color:#0f0; padding:10px; white-space:pre-wrap;' }, + logs.join('\n') || _('No logs available') + ), + E('div', { 'class': 'right', 'style': 'margin-top:10px' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) + ]) + ]); + }); + }, + + render: function(status) { + this.status = status; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', _('GoToSocial Fediverse Server')), + E('div', { 'class': 'cbi-map-descr' }, _('Lightweight ActivityPub social network server for the Fediverse.')), + + E('div', { 'class': 'cbi-section' }, [ + E('h3', _('Status')), + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'width': '200px' }, _('Container')), + E('td', { 'class': 'td' }, E('span', { 'id': 'container-status', 'class': 'badge' }, '-')) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Service')), + E('td', { 'class': 'td' }, E('span', { 'id': 'service-status', 'class': 'badge' }, '-')) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Version')), + E('td', { 'class': 'td' }, E('span', { 'id': 'gts-version' }, '-')) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Domain')), + E('td', { 'class': 'td' }, E('span', { 'id': 'gts-host' }, '-')) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, _('Exposure')), + E('td', { 'class': 'td' }, E('span', { 'id': 'exposure-status' }, '-')) + ]) + ]) + ]), + + E('div', { 'class': 'cbi-section' }, [ + E('h3', _('Actions')), + E('div', { 'class': 'cbi-value' }, [ + E('button', { + 'id': 'btn-install', + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, this.handleInstall) + }, _('Install')), + ' ', + E('button', { + 'id': 'btn-start', + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, this.handleStart) + }, _('Start')), + ' ', + E('button', { + 'id': 'btn-stop', + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, this.handleStop) + }, _('Stop')), + ' ', + E('button', { + 'id': 'btn-restart', + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, this.handleRestart) + }, _('Restart')) + ]), + E('div', { 'class': 'cbi-value', 'style': 'margin-top:10px' }, [ + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, this.handleEmancipate) + }, _('Expose Service')), + ' ', + E('button', { + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, this.handleRevoke) + }, _('Revoke Exposure')), + ' ', + E('button', { + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, this.handleBackup) + }, _('Backup')), + ' ', + E('button', { + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, this.handleViewLogs) + }, _('View Logs')) + ]) + ]), + + E('style', {}, ` + .badge { padding: 2px 8px; border-radius: 3px; font-weight: bold; } + .badge.success { background: #4CAF50; color: white; } + .badge.warning { background: #FF9800; color: white; } + .badge.danger { background: #f44336; color: white; } + `) + ]); + + this.updateStatusDisplay(status); + poll.add(L.bind(this.pollStatus, this), 5); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/settings.js b/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/settings.js new file mode 100644 index 00000000..4e0b7ebf --- /dev/null +++ b/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/settings.js @@ -0,0 +1,143 @@ +'use strict'; +'require view'; +'require form'; +'require rpc'; +'require uci'; +'require ui'; + +return view.extend({ + load: function() { + return uci.load('gotosocial'); + }, + + render: function() { + var m, s, o; + + m = new form.Map('gotosocial', _('GoToSocial Settings'), + _('Configure your Fediverse instance settings.')); + + // Main settings + s = m.section(form.NamedSection, 'main', 'gotosocial', _('Instance Settings')); + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enabled'), + _('Enable GoToSocial service')); + o.rmempty = false; + + o = s.option(form.Value, 'host', _('Domain'), + _('The domain name for your instance (e.g., social.example.com)')); + o.rmempty = false; + o.placeholder = 'social.example.com'; + + o = s.option(form.Value, 'port', _('Port'), + _('Internal port for GoToSocial')); + o.datatype = 'port'; + o.default = '8484'; + + o = s.option(form.ListValue, 'protocol', _('Protocol'), + _('Protocol for external access')); + o.value('https', 'HTTPS'); + o.value('http', 'HTTP'); + o.default = 'https'; + + o = s.option(form.Value, 'bind_address', _('Bind Address'), + _('IP address to listen on')); + o.default = '0.0.0.0'; + + o = s.option(form.Value, 'instance_name', _('Instance Name'), + _('Display name for your instance')); + o.placeholder = 'SecuBox Social'; + + o = s.option(form.TextValue, 'instance_description', _('Instance Description'), + _('Description shown on the instance landing page')); + o.rows = 3; + + // Registration settings + s = m.section(form.NamedSection, 'main', 'gotosocial', _('Registration')); + + o = s.option(form.Flag, 'accounts_registration_open', _('Open Registration'), + _('Allow new users to sign up')); + o.default = '0'; + + o = s.option(form.Flag, 'accounts_approval_required', _('Require Approval'), + _('New registrations require admin approval')); + o.default = '1'; + + // LXC Container settings + s = m.section(form.NamedSection, 'container', 'lxc', _('Container Settings')); + s.addremove = false; + + o = s.option(form.Value, 'rootfs_path', _('Container Root'), + _('Path to LXC container rootfs')); + o.default = '/srv/lxc/gotosocial/rootfs'; + o.readonly = true; + + o = s.option(form.Value, 'data_path', _('Data Path'), + _('Path to persistent data storage')); + o.default = '/srv/gotosocial'; + o.readonly = true; + + o = s.option(form.Value, 'memory_limit', _('Memory Limit'), + _('Maximum memory for container')); + o.default = '512M'; + + o = s.option(form.Value, 'version', _('GoToSocial Version'), + _('Version to install')); + o.default = '0.17.3'; + + // HAProxy integration + s = m.section(form.NamedSection, 'proxy', 'haproxy', _('HAProxy Integration')); + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable HAProxy'), + _('Route traffic through HAProxy')); + o.default = '0'; + + o = s.option(form.Value, 'vhost_domain', _('Virtual Host Domain'), + _('Domain for HAProxy vhost (usually same as main domain)')); + + o = s.option(form.Flag, 'ssl_enabled', _('Enable SSL'), + _('Enable HTTPS via HAProxy')); + o.default = '1'; + + o = s.option(form.Flag, 'acme_enabled', _('Enable ACME'), + _('Automatically provision SSL certificates')); + o.default = '1'; + + // Federation settings + s = m.section(form.NamedSection, 'federation', 'federation', _('Federation')); + s.addremove = false; + + o = s.option(form.Flag, 'enabled', _('Enable Federation'), + _('Allow communication with other Fediverse instances')); + o.default = '1'; + + o = s.option(form.Flag, 'auto_approve_followers', _('Auto-Approve Followers'), + _('Automatically approve follow requests')); + o.default = '0'; + + o = s.option(form.DynamicList, 'blocked_domains', _('Blocked Domains'), + _('Instances to block from federation')); + + o = s.option(form.DynamicList, 'allowed_domains', _('Allowed Domains'), + _('If set, only federate with these instances (allowlist mode)')); + + // Mesh settings + s = m.section(form.NamedSection, 'mesh', 'mesh', _('SecuBox Mesh')); + s.addremove = false; + + o = s.option(form.Flag, 'auto_federate', _('Auto-Federate with Mesh'), + _('Automatically federate with other SecuBox nodes')); + o.default = '1'; + + o = s.option(form.Flag, 'announce_to_peers', _('Announce to Peers'), + _('Publish this instance to mesh network')); + o.default = '1'; + + o = s.option(form.Flag, 'share_blocklist', _('Share Blocklist'), + _('Share and sync blocked domains with mesh peers')); + o.default = '1'; + + return m.render(); + } +}); diff --git a/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/users.js b/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/users.js new file mode 100644 index 00000000..103b189d --- /dev/null +++ b/package/secubox/luci-app-gotosocial/htdocs/luci-static/resources/view/gotosocial/users.js @@ -0,0 +1,258 @@ +'use strict'; +'require view'; +'require rpc'; +'require ui'; +'require poll'; + +var callUsers = rpc.declare({ + object: 'luci.gotosocial', + method: 'users', + expect: {} +}); + +var callCreateUser = rpc.declare({ + object: 'luci.gotosocial', + method: 'create_user', + params: ['username', 'email', 'password', 'admin'], + expect: {} +}); + +var callDeleteUser = rpc.declare({ + object: 'luci.gotosocial', + method: 'delete_user', + params: ['username'], + expect: {} +}); + +var callPromoteUser = rpc.declare({ + object: 'luci.gotosocial', + method: 'promote_user', + params: ['username'], + expect: {} +}); + +var callDemoteUser = rpc.declare({ + object: 'luci.gotosocial', + method: 'demote_user', + params: ['username'], + expect: {} +}); + +return view.extend({ + users: [], + + load: function() { + return callUsers(); + }, + + pollUsers: function() { + return callUsers().then(L.bind(function(res) { + this.users = res.users || []; + this.updateUserTable(); + }, this)); + }, + + updateUserTable: function() { + var tbody = document.getElementById('users-tbody'); + if (!tbody) return; + + while (tbody.firstChild) { + tbody.removeChild(tbody.firstChild); + } + + if (this.users.length === 0) { + tbody.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'colspan': '5', 'style': 'text-align:center' }, _('No users found')) + ])); + return; + } + + this.users.forEach(L.bind(function(user) { + var row = E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, user.username || '-'), + E('td', { 'class': 'td' }, user.email || '-'), + E('td', { 'class': 'td' }, user.admin ? + E('span', { 'class': 'badge success' }, _('Admin')) : + E('span', { 'class': 'badge' }, _('User')) + ), + E('td', { 'class': 'td' }, user.confirmed ? + E('span', { 'class': 'badge success' }, _('Confirmed')) : + E('span', { 'class': 'badge warning' }, _('Pending')) + ), + E('td', { 'class': 'td' }, [ + user.admin ? + E('button', { + 'class': 'btn cbi-button-neutral', + 'click': ui.createHandlerFn(this, this.handleDemote, user.username) + }, _('Demote')) : + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, this.handlePromote, user.username) + }, _('Promote')), + ' ', + E('button', { + 'class': 'btn cbi-button-negative', + 'click': ui.createHandlerFn(this, this.handleDelete, user.username) + }, _('Delete')) + ]) + ]); + tbody.appendChild(row); + }, this)); + }, + + handleCreate: function() { + return ui.showModal(_('Create User'), [ + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Username')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'text', 'id': 'new-username', 'class': 'cbi-input-text', 'placeholder': 'johndoe' }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Email')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'email', 'id': 'new-email', 'class': 'cbi-input-text', 'placeholder': 'john@example.com' }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Password')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'password', 'id': 'new-password', 'class': 'cbi-input-text' }) + ]) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Admin')), + E('div', { 'class': 'cbi-value-field' }, [ + E('input', { 'type': 'checkbox', 'id': 'new-admin' }), + ' ', _('Grant admin privileges') + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, function() { + var username = document.getElementById('new-username').value; + var email = document.getElementById('new-email').value; + var password = document.getElementById('new-password').value; + var admin = document.getElementById('new-admin').checked; + + if (!username || !email || !password) { + ui.addNotification(null, E('p', _('All fields are required')), 'error'); + return; + } + + ui.hideModal(); + ui.showModal(_('Creating User...'), [ + E('p', { 'class': 'spinning' }, _('Creating user...')) + ]); + + return callCreateUser(username, email, password, admin).then(L.bind(function(res) { + ui.hideModal(); + if (res.success) { + ui.addNotification(null, E('p', res.message || _('User created successfully')), 'success'); + return this.pollUsers(); + } else { + ui.addNotification(null, E('p', res.error || _('Failed to create user')), 'error'); + } + }, this)); + }) + }, _('Create')) + ]) + ]); + }, + + handleDelete: function(username) { + return ui.showModal(_('Delete User'), [ + E('p', _('Are you sure you want to delete user "%s"?').format(username)), + E('p', { 'class': 'alert-message warning' }, _('This action cannot be undone. All posts and data will be lost.')), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-negative', + 'click': ui.createHandlerFn(this, function() { + ui.hideModal(); + return callDeleteUser(username).then(L.bind(function(res) { + if (res.success) { + ui.addNotification(null, E('p', res.message || _('User deleted')), 'success'); + return this.pollUsers(); + } else { + ui.addNotification(null, E('p', res.error || _('Failed to delete user')), 'error'); + } + }, this)); + }) + }, _('Delete')) + ]) + ]); + }, + + handlePromote: function(username) { + return callPromoteUser(username).then(L.bind(function(res) { + if (res.success) { + ui.addNotification(null, E('p', res.message || _('User promoted')), 'success'); + return this.pollUsers(); + } else { + ui.addNotification(null, E('p', res.error || _('Failed to promote user')), 'error'); + } + }, this)); + }, + + handleDemote: function(username) { + return callDemoteUser(username).then(L.bind(function(res) { + if (res.success) { + ui.addNotification(null, E('p', res.message || _('User demoted')), 'success'); + return this.pollUsers(); + } else { + ui.addNotification(null, E('p', res.error || _('Failed to demote user')), 'error'); + } + }, this)); + }, + + render: function(data) { + this.users = data.users || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', _('GoToSocial Users')), + E('div', { 'class': 'cbi-map-descr' }, _('Manage user accounts for your Fediverse instance.')), + + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'style': 'margin-bottom:10px' }, [ + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, this.handleCreate) + }, _('Create User')) + ]), + + E('table', { 'class': 'table' }, [ + E('thead', {}, [ + E('tr', { 'class': 'tr' }, [ + E('th', { 'class': 'th' }, _('Username')), + E('th', { 'class': 'th' }, _('Email')), + E('th', { 'class': 'th' }, _('Role')), + E('th', { 'class': 'th' }, _('Status')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ]), + E('tbody', { 'id': 'users-tbody' }) + ]) + ]), + + E('style', {}, ` + .badge { padding: 2px 8px; border-radius: 3px; background: #666; color: white; } + .badge.success { background: #4CAF50; } + .badge.warning { background: #FF9800; } + `) + ]); + + this.updateUserTable(); + poll.add(L.bind(this.pollUsers, this), 10); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-gotosocial/root/usr/libexec/rpcd/luci.gotosocial b/package/secubox/luci-app-gotosocial/root/usr/libexec/rpcd/luci.gotosocial new file mode 100644 index 00000000..8142aa6c --- /dev/null +++ b/package/secubox/luci-app-gotosocial/root/usr/libexec/rpcd/luci.gotosocial @@ -0,0 +1,264 @@ +#!/bin/sh + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +GOTOSOCIALCTL="/usr/sbin/gotosocialctl" + +case "$1" in + list) + echo '{"status":{},"install":{},"start":{},"stop":{},"restart":{},"users":{},"create_user":{"username":"str","email":"str","password":"str","admin":"bool"},"delete_user":{"username":"str"},"promote_user":{"username":"str"},"demote_user":{"username":"str"},"get_config":{},"save_config":{"host":"str","port":"int","protocol":"str","instance_name":"str","instance_description":"str","accounts_registration_open":"bool","accounts_approval_required":"bool"},"emancipate":{"domain":"str","tor":"bool","dns":"bool","mesh":"bool"},"revoke":{},"backup":{},"logs":{"lines":"int"}}' + ;; + call) + case "$2" in + status) + $GOTOSOCIALCTL status + ;; + install) + result=$($GOTOSOCIALCTL install 2>&1) + json_init + if echo "$result" | grep -q "successfully\|already installed"; then + json_add_boolean "success" 1 + json_add_string "message" "$result" + else + json_add_boolean "success" 0 + json_add_string "error" "$result" + fi + json_dump + ;; + start) + result=$($GOTOSOCIALCTL start 2>&1) + json_init + json_add_boolean "success" 1 + json_add_string "message" "$result" + json_dump + ;; + stop) + result=$($GOTOSOCIALCTL stop 2>&1) + json_init + json_add_boolean "success" 1 + json_add_string "message" "$result" + json_dump + ;; + restart) + $GOTOSOCIALCTL stop 2>/dev/null + sleep 2 + result=$($GOTOSOCIALCTL start 2>&1) + json_init + json_add_boolean "success" 1 + json_add_string "message" "$result" + json_dump + ;; + users) + $GOTOSOCIALCTL users + ;; + create_user) + read input + json_load "$input" + json_get_var username username + json_get_var email email + json_get_var password password + json_get_var admin admin + + if [ -z "$username" ] || [ -z "$email" ] || [ -z "$password" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Missing required fields: username, email, password" + json_dump + exit 0 + fi + + admin_flag="" + [ "$admin" = "1" ] || [ "$admin" = "true" ] && admin_flag="--admin" + + result=$($GOTOSOCIALCTL user create "$username" "$email" "$password" $admin_flag 2>&1) + json_init + if echo "$result" | grep -qi "success\|created"; then + json_add_boolean "success" 1 + json_add_string "message" "User $username created successfully" + else + json_add_boolean "success" 0 + json_add_string "error" "$result" + fi + json_dump + ;; + delete_user) + read input + json_load "$input" + json_get_var username username + + if [ -z "$username" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Username required" + json_dump + exit 0 + fi + + result=$($GOTOSOCIALCTL user delete "$username" 2>&1) + json_init + json_add_boolean "success" 1 + json_add_string "message" "User $username deleted" + json_dump + ;; + promote_user) + read input + json_load "$input" + json_get_var username username + + result=$($GOTOSOCIALCTL user promote "$username" 2>&1) + json_init + json_add_boolean "success" 1 + json_add_string "message" "User $username promoted to admin" + json_dump + ;; + demote_user) + read input + json_load "$input" + json_get_var username username + + result=$($GOTOSOCIALCTL user demote "$username" 2>&1) + json_init + json_add_boolean "success" 1 + json_add_string "message" "User $username demoted from admin" + json_dump + ;; + get_config) + json_init + + config_load gotosocial + + # Main config + config_get enabled main enabled '0' + config_get host main host 'social.example.com' + config_get port main port '8484' + config_get protocol main protocol 'https' + config_get bind_address main bind_address '0.0.0.0' + config_get instance_name main instance_name 'SecuBox Social' + config_get instance_description main instance_description '' + config_get accounts_registration_open main accounts_registration_open '0' + config_get accounts_approval_required main accounts_approval_required '1' + + json_add_string "enabled" "$enabled" + json_add_string "host" "$host" + json_add_string "port" "$port" + json_add_string "protocol" "$protocol" + json_add_string "bind_address" "$bind_address" + json_add_string "instance_name" "$instance_name" + json_add_string "instance_description" "$instance_description" + json_add_string "accounts_registration_open" "$accounts_registration_open" + json_add_string "accounts_approval_required" "$accounts_approval_required" + + # LXC config + config_get rootfs_path container rootfs_path '/srv/lxc/gotosocial/rootfs' + config_get data_path container data_path '/srv/gotosocial' + config_get memory_limit container memory_limit '512M' + config_get version container version '0.17.3' + + json_add_string "rootfs_path" "$rootfs_path" + json_add_string "data_path" "$data_path" + json_add_string "memory_limit" "$memory_limit" + json_add_string "version" "$version" + + # HAProxy config + config_get proxy_enabled proxy enabled '0' + config_get vhost_domain proxy vhost_domain '' + config_get ssl_enabled proxy ssl_enabled '1' + config_get acme_enabled proxy acme_enabled '1' + + json_add_string "proxy_enabled" "$proxy_enabled" + json_add_string "vhost_domain" "$vhost_domain" + json_add_string "ssl_enabled" "$ssl_enabled" + json_add_string "acme_enabled" "$acme_enabled" + + # Federation config + config_get federation_enabled federation enabled '1' + config_get auto_approve federation auto_approve_followers '0' + + json_add_string "federation_enabled" "$federation_enabled" + json_add_string "auto_approve_followers" "$auto_approve" + + json_dump + ;; + save_config) + read input + json_load "$input" + + json_get_var host host + json_get_var port port + json_get_var protocol protocol + json_get_var instance_name instance_name + json_get_var instance_description instance_description + json_get_var accounts_registration_open accounts_registration_open + json_get_var accounts_approval_required accounts_approval_required + + [ -n "$host" ] && uci set gotosocial.main.host="$host" + [ -n "$port" ] && uci set gotosocial.main.port="$port" + [ -n "$protocol" ] && uci set gotosocial.main.protocol="$protocol" + [ -n "$instance_name" ] && uci set gotosocial.main.instance_name="$instance_name" + [ -n "$instance_description" ] && uci set gotosocial.main.instance_description="$instance_description" + [ -n "$accounts_registration_open" ] && uci set gotosocial.main.accounts_registration_open="$accounts_registration_open" + [ -n "$accounts_approval_required" ] && uci set gotosocial.main.accounts_approval_required="$accounts_approval_required" + + uci commit gotosocial + + json_init + json_add_boolean "success" 1 + json_add_string "message" "Configuration saved" + json_dump + ;; + emancipate) + read input + json_load "$input" + json_get_var domain domain + json_get_var tor tor + json_get_var dns dns + json_get_var mesh mesh + + args="" + [ "$tor" = "1" ] || [ "$tor" = "true" ] && args="$args --tor" + [ "$dns" = "1" ] || [ "$dns" = "true" ] && args="$args --dns" + [ "$mesh" = "1" ] || [ "$mesh" = "true" ] && args="$args --mesh" + [ -z "$args" ] && args="--all" + + result=$($GOTOSOCIALCTL emancipate "$domain" $args 2>&1) + json_init + if echo "$result" | grep -qi "success\|complete\|enabled"; then + json_add_boolean "success" 1 + json_add_string "message" "$result" + else + json_add_boolean "success" 0 + json_add_string "error" "$result" + fi + json_dump + ;; + revoke) + result=$($GOTOSOCIALCTL revoke 2>&1) + json_init + json_add_boolean "success" 1 + json_add_string "message" "Exposure revoked" + json_dump + ;; + backup) + result=$($GOTOSOCIALCTL backup 2>&1) + json_init + if echo "$result" | grep -qi "backup\|success"; then + json_add_boolean "success" 1 + json_add_string "message" "$result" + else + json_add_boolean "success" 0 + json_add_string "error" "$result" + fi + json_dump + ;; + logs) + read input + json_load "$input" + json_get_var lines lines + [ -z "$lines" ] && lines=50 + + $GOTOSOCIALCTL logs "$lines" + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-gotosocial/root/usr/share/luci/menu.d/luci-app-gotosocial.json b/package/secubox/luci-app-gotosocial/root/usr/share/luci/menu.d/luci-app-gotosocial.json new file mode 100644 index 00000000..fd853701 --- /dev/null +++ b/package/secubox/luci-app-gotosocial/root/usr/share/luci/menu.d/luci-app-gotosocial.json @@ -0,0 +1,37 @@ +{ + "admin/services/gotosocial": { + "title": "GoToSocial", + "order": 60, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": ["luci-app-gotosocial"], + "uci": {"gotosocial": true} + } + }, + "admin/services/gotosocial/overview": { + "title": "Overview", + "order": 1, + "action": { + "type": "view", + "path": "gotosocial/overview" + } + }, + "admin/services/gotosocial/users": { + "title": "Users", + "order": 2, + "action": { + "type": "view", + "path": "gotosocial/users" + } + }, + "admin/services/gotosocial/settings": { + "title": "Settings", + "order": 3, + "action": { + "type": "view", + "path": "gotosocial/settings" + } + } +} diff --git a/package/secubox/luci-app-gotosocial/root/usr/share/rpcd/acl.d/luci-app-gotosocial.json b/package/secubox/luci-app-gotosocial/root/usr/share/rpcd/acl.d/luci-app-gotosocial.json new file mode 100644 index 00000000..3df4e9b8 --- /dev/null +++ b/package/secubox/luci-app-gotosocial/root/usr/share/rpcd/acl.d/luci-app-gotosocial.json @@ -0,0 +1,17 @@ +{ + "luci-app-gotosocial": { + "description": "Grant access to GoToSocial Fediverse Server", + "read": { + "ubus": { + "luci.gotosocial": ["status", "users", "get_config", "logs"] + }, + "uci": ["gotosocial"] + }, + "write": { + "ubus": { + "luci.gotosocial": ["install", "start", "stop", "restart", "create_user", "delete_user", "promote_user", "demote_user", "save_config", "emancipate", "revoke", "backup"] + }, + "uci": ["gotosocial"] + } + } +} diff --git a/package/secubox/secubox-app-gotosocial/Makefile b/package/secubox/secubox-app-gotosocial/Makefile new file mode 100644 index 00000000..b76f60be --- /dev/null +++ b/package/secubox/secubox-app-gotosocial/Makefile @@ -0,0 +1,39 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-gotosocial +PKG_VERSION:=0.1.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=MIT + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-gotosocial + SECTION:=secubox + CATEGORY:=SecuBox + TITLE:=GoToSocial Fediverse Server + DEPENDS:=+lxc +lxc-attach +wget +jq +openssl-util + PKGARCH:=all +endef + +define Package/secubox-app-gotosocial/description + Lightweight ActivityPub social network server for SecuBox. + Provides a self-hosted Fediverse instance with LuCI management. +endef + +define Package/secubox-app-gotosocial/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/gotosocial $(1)/etc/config/gotosocial + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/gotosocial $(1)/etc/init.d/gotosocial + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/gotosocialctl $(1)/usr/sbin/gotosocialctl + + $(INSTALL_DIR) $(1)/usr/share/gotosocial + $(INSTALL_DATA) ./files/usr/share/gotosocial/config.yaml.template $(1)/usr/share/gotosocial/ +endef + +$(eval $(call BuildPackage,secubox-app-gotosocial)) diff --git a/package/secubox/secubox-app-gotosocial/files/etc/config/gotosocial b/package/secubox/secubox-app-gotosocial/files/etc/config/gotosocial new file mode 100644 index 00000000..08f73178 --- /dev/null +++ b/package/secubox/secubox-app-gotosocial/files/etc/config/gotosocial @@ -0,0 +1,39 @@ +config gotosocial 'main' + option enabled '0' + option host 'social.example.com' + option port '8484' + option protocol 'https' + option bind_address '0.0.0.0' + option db_type 'sqlite' + option db_path '/data/gotosocial.db' + option storage_path '/data/storage' + option letsencrypt_enabled '0' + option letsencrypt_email '' + option instance_name 'SecuBox Social' + option instance_description 'A SecuBox Fediverse instance' + option accounts_registration_open '0' + option accounts_approval_required '1' + +config lxc 'container' + option rootfs_path '/srv/lxc/gotosocial/rootfs' + option data_path '/srv/gotosocial' + option memory_limit '512M' + option version '0.17.3' + +config haproxy 'proxy' + option enabled '0' + option backend_name 'gotosocial' + option vhost_domain '' + option ssl_enabled '1' + option acme_enabled '1' + +config federation 'federation' + option enabled '1' + option auto_approve_followers '0' + option blocked_domains '' + option allowed_domains '' + +config mesh 'mesh' + option auto_federate '1' + option announce_to_peers '1' + option share_blocklist '1' diff --git a/package/secubox/secubox-app-gotosocial/files/etc/init.d/gotosocial b/package/secubox/secubox-app-gotosocial/files/etc/init.d/gotosocial new file mode 100644 index 00000000..f713e204 --- /dev/null +++ b/package/secubox/secubox-app-gotosocial/files/etc/init.d/gotosocial @@ -0,0 +1,33 @@ +#!/bin/sh /etc/rc.common + +START=95 +STOP=10 +USE_PROCD=1 + +PROG=/usr/sbin/gotosocialctl + +start_service() { + local enabled + config_load gotosocial + config_get enabled main enabled '0' + + [ "$enabled" = "1" ] || return 0 + + $PROG start +} + +stop_service() { + $PROG stop +} + +reload_service() { + $PROG reload +} + +service_triggers() { + procd_add_reload_trigger "gotosocial" +} + +status() { + $PROG status +} diff --git a/package/secubox/secubox-app-gotosocial/files/usr/sbin/gotosocialctl b/package/secubox/secubox-app-gotosocial/files/usr/sbin/gotosocialctl new file mode 100644 index 00000000..97989fa3 --- /dev/null +++ b/package/secubox/secubox-app-gotosocial/files/usr/sbin/gotosocialctl @@ -0,0 +1,778 @@ +#!/bin/sh +# GoToSocial Controller for SecuBox +# Manages GoToSocial LXC container and configuration + +set -e + +VERSION="0.1.0" +GTS_VERSION="0.17.3" +LXC_NAME="gotosocial" +LXC_PATH="/srv/lxc/gotosocial" +DATA_PATH="/srv/gotosocial" +CONFIG_FILE="/etc/config/gotosocial" +GTS_BINARY_URL="https://github.com/superseriousbusiness/gotosocial/releases/download/v${GTS_VERSION}/gotosocial_${GTS_VERSION}_linux_arm64.tar.gz" + +# Logging +log_info() { logger -t gotosocial -p daemon.info "$1"; echo "[INFO] $1"; } +log_error() { logger -t gotosocial -p daemon.err "$1"; echo "[ERROR] $1" >&2; } +log_warn() { logger -t gotosocial -p daemon.warn "$1"; echo "[WARN] $1"; } + +# UCI helpers +get_config() { + local section="$1" + local option="$2" + local default="$3" + uci -q get "gotosocial.${section}.${option}" || echo "$default" +} + +set_config() { + uci set "gotosocial.$1.$2=$3" + uci commit gotosocial +} + +# Check if container exists +container_exists() { + [ -d "$LXC_PATH/rootfs" ] +} + +# Check if container is running +container_running() { + lxc-info -n "$LXC_NAME" 2>/dev/null | grep -q "RUNNING" +} + +# Download GoToSocial binary +download_binary() { + local version="${1:-$GTS_VERSION}" + local url="https://github.com/superseriousbusiness/gotosocial/releases/download/v${version}/gotosocial_${version}_linux_arm64.tar.gz" + local tmp_dir="/tmp/gotosocial_install" + + log_info "Downloading GoToSocial v${version}..." + + mkdir -p "$tmp_dir" + cd "$tmp_dir" + + wget -q -O gotosocial.tar.gz "$url" || { + log_error "Failed to download GoToSocial" + return 1 + } + + tar -xzf gotosocial.tar.gz + + mkdir -p "$LXC_PATH/rootfs/opt/gotosocial" + cp gotosocial "$LXC_PATH/rootfs/opt/gotosocial/" + chmod +x "$LXC_PATH/rootfs/opt/gotosocial/gotosocial" + + # Copy web assets + [ -d "web" ] && cp -r web "$LXC_PATH/rootfs/opt/gotosocial/" + + rm -rf "$tmp_dir" + log_info "GoToSocial binary installed" +} + +# Create minimal rootfs +create_rootfs() { + local rootfs="$LXC_PATH/rootfs" + + log_info "Creating minimal rootfs..." + + mkdir -p "$rootfs"/{opt/gotosocial,data,etc,proc,sys,dev,tmp,run} + + # Create basic filesystem structure + mkdir -p "$rootfs/etc/ssl/certs" + + # Copy SSL certificates from host + cp /etc/ssl/certs/ca-certificates.crt "$rootfs/etc/ssl/certs/" 2>/dev/null || \ + cat /etc/ssl/certs/*.pem > "$rootfs/etc/ssl/certs/ca-certificates.crt" 2>/dev/null || true + + # Create passwd/group for GoToSocial + echo "root:x:0:0:root:/root:/bin/sh" > "$rootfs/etc/passwd" + echo "gotosocial:x:1000:1000:GoToSocial:/data:/bin/false" >> "$rootfs/etc/passwd" + echo "root:x:0:" > "$rootfs/etc/group" + echo "gotosocial:x:1000:" >> "$rootfs/etc/group" + + # Create resolv.conf + cp /etc/resolv.conf "$rootfs/etc/" + + # Create hosts file + cat > "$rootfs/etc/hosts" < "$LXC_PATH/config" < "$data_path/config.yaml" </dev/null || { + log_error "LXC not installed. Install lxc package first." + return 1 + } + + # Create directories + mkdir -p "$LXC_PATH" "$DATA_PATH" + + # Create rootfs + create_rootfs + + # Download binary + download_binary "$version" + + # Create LXC config + create_lxc_config + + # Generate GoToSocial config + generate_config + + log_info "GoToSocial installed successfully" + log_info "Run 'gotosocialctl start' to start the service" + log_info "Then create a user with 'gotosocialctl user create '" +} + +# Uninstall +cmd_uninstall() { + local keep_data="$1" + + log_info "Uninstalling GoToSocial..." + + # Stop container if running + container_running && cmd_stop + + # Remove container + rm -rf "$LXC_PATH" + + # Remove data unless --keep-data + if [ "$keep_data" != "--keep-data" ]; then + rm -rf "$DATA_PATH" + log_info "Data removed" + else + log_info "Data preserved at $DATA_PATH" + fi + + log_info "GoToSocial uninstalled" +} + +# Start container +cmd_start() { + if ! container_exists; then + log_error "GoToSocial not installed. Run 'gotosocialctl install' first." + return 1 + fi + + if container_running; then + log_info "GoToSocial is already running" + return 0 + fi + + # Regenerate config in case settings changed + create_lxc_config + generate_config + + log_info "Starting GoToSocial container..." + + lxc-start -n "$LXC_NAME" -d -P "$(dirname $LXC_PATH)" || { + log_error "Failed to start container" + return 1 + } + + sleep 2 + + if container_running; then + log_info "GoToSocial started" + local port=$(get_config main port "8484") + log_info "Web interface available at http://localhost:$port" + else + log_error "Container failed to start" + return 1 + fi +} + +# Stop container +cmd_stop() { + if ! container_running; then + log_info "GoToSocial is not running" + return 0 + fi + + log_info "Stopping GoToSocial..." + lxc-stop -n "$LXC_NAME" -P "$(dirname $LXC_PATH)" || true + log_info "GoToSocial stopped" +} + +# Restart +cmd_restart() { + cmd_stop + sleep 1 + cmd_start +} + +# Reload config +cmd_reload() { + log_info "Reloading configuration..." + generate_config + cmd_restart +} + +# Status (JSON output for RPCD) +cmd_status() { + local installed="false" + local container_state="false" + local service_state="false" + local host=$(get_config main host "social.example.com") + local port=$(get_config main port "8484") + local version=$(get_config container version "$GTS_VERSION") + local tor_enabled=$(get_config federation tor_enabled "0") + local dns_enabled=$(get_config proxy enabled "0") + local mesh_enabled=$(get_config mesh announce_to_peers "0") + + container_exists && installed="true" + container_running && container_state="true" + + # Check if API responds + if [ "$container_state" = "true" ]; then + curl -s --connect-timeout 2 "http://127.0.0.1:$port/api/v1/instance" >/dev/null 2>&1 && service_state="true" + fi + + cat </dev/null | grep -E "State|PID|CPU|Memory" + + local port=$(get_config main port "8484") + local host=$(get_config main host "localhost") + echo "Host: $host" + echo "Port: $port" + + # Check if web interface responds + if curl -s --connect-timeout 2 "http://127.0.0.1:$port/api/v1/instance" >/dev/null 2>&1; then + echo "API: responding" + else + echo "API: not responding (may still be starting)" + fi + else + echo "GoToSocial: stopped" + return 1 + fi +} + +# Create user +cmd_user_create() { + local username="$1" + local email="$2" + local admin="${3:-false}" + + [ -z "$username" ] || [ -z "$email" ] && { + echo "Usage: gotosocialctl user create [--admin]" + return 1 + } + + [ "$3" = "--admin" ] && admin="true" + + if ! container_running; then + log_error "GoToSocial is not running" + return 1 + fi + + log_info "Creating user $username..." + + # Generate random password + local password=$(openssl rand -base64 12) + + lxc-attach -n "$LXC_NAME" -P "$(dirname $LXC_PATH)" -- \ + /opt/gotosocial/gotosocial admin account create \ + --username "$username" \ + --email "$email" \ + --password "$password" \ + --config /data/config.yaml + + if [ "$admin" = "true" ]; then + lxc-attach -n "$LXC_NAME" -P "$(dirname $LXC_PATH)" -- \ + /opt/gotosocial/gotosocial admin account promote \ + --username "$username" \ + --config /data/config.yaml + fi + + echo "" + echo "User created successfully!" + echo "Username: $username" + echo "Email: $email" + echo "Password: $password" + echo "" + echo "Please change this password after first login." +} + +# List users (JSON output for RPCD) +cmd_users() { + local db_path="$DATA_PATH/gotosocial.db" + local users="[]" + + if [ -f "$db_path" ] && command -v sqlite3 >/dev/null; then + users=$(sqlite3 -json "$db_path" "SELECT username, created_at as created, + CASE WHEN suspended_at IS NULL THEN 0 ELSE 1 END as suspended, + CASE WHEN confirmed_at IS NULL THEN 0 ELSE 1 END as confirmed + FROM accounts WHERE domain IS NULL OR domain = '';" 2>/dev/null || echo "[]") + fi + + echo "{\"users\":$users}" +} + +# List users (human readable) +cmd_user_list() { + if ! container_running; then + log_error "GoToSocial is not running" + return 1 + fi + + local port=$(get_config main port "8484") + + # Use API to list accounts (requires admin token) + # For now, check the database directly + local db_path="$DATA_PATH/gotosocial.db" + + if [ -f "$db_path" ] && command -v sqlite3 >/dev/null; then + sqlite3 "$db_path" "SELECT username, created_at, suspended_at FROM accounts WHERE domain IS NULL OR domain = '';" 2>/dev/null || { + echo "Unable to query database directly. Use the web interface." + } + else + echo "Use the web interface to manage users." + echo "URL: https://$(get_config main host)/admin" + fi +} + +# Confirm user email +cmd_user_confirm() { + local username="$1" + + [ -z "$username" ] && { + echo "Usage: gotosocialctl user confirm " + return 1 + } + + if ! container_running; then + log_error "GoToSocial is not running" + return 1 + fi + + lxc-attach -n "$LXC_NAME" -P "$(dirname $LXC_PATH)" -- \ + /opt/gotosocial/gotosocial admin account confirm \ + --username "$username" \ + --config /data/config.yaml + + log_info "User $username confirmed" +} + +# Emancipate - expose via HAProxy +cmd_emancipate() { + local domain="$1" + + [ -z "$domain" ] && domain=$(get_config main host) + [ -z "$domain" ] || [ "$domain" = "social.example.com" ] && { + echo "Usage: gotosocialctl emancipate " + echo "Example: gotosocialctl emancipate social.mysite.com" + return 1 + } + + local port=$(get_config main port "8484") + local lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") + + log_info "Exposing GoToSocial at $domain..." + + # Update config + set_config main host "$domain" + set_config proxy enabled "1" + set_config proxy vhost_domain "$domain" + + # Create HAProxy backend + uci set haproxy.gotosocial=backend + uci set haproxy.gotosocial.name='gotosocial' + uci set haproxy.gotosocial.mode='http' + uci set haproxy.gotosocial.balance='roundrobin' + uci set haproxy.gotosocial.enabled='1' + + uci set haproxy.gotosocial_srv=server + uci set haproxy.gotosocial_srv.backend='gotosocial' + uci set haproxy.gotosocial_srv.name='gotosocial' + uci set haproxy.gotosocial_srv.address="$lan_ip" + uci set haproxy.gotosocial_srv.port="$port" + uci set haproxy.gotosocial_srv.weight='100' + uci set haproxy.gotosocial_srv.check='1' + uci set haproxy.gotosocial_srv.enabled='1' + + # Create vhost + local vhost_name=$(echo "$domain" | tr '.-' '_') + uci set haproxy.${vhost_name}=vhost + uci set haproxy.${vhost_name}.domain="$domain" + uci set haproxy.${vhost_name}.backend='gotosocial' + uci set haproxy.${vhost_name}.ssl='1' + uci set haproxy.${vhost_name}.ssl_redirect='1' + uci set haproxy.${vhost_name}.acme='1' + uci set haproxy.${vhost_name}.enabled='1' + + uci commit haproxy + uci commit gotosocial + + # Regenerate HAProxy config + if command -v haproxyctl >/dev/null; then + haproxyctl generate + /etc/init.d/haproxy reload + fi + + # Regenerate GoToSocial config with new domain + generate_config + + # Restart to apply new config + container_running && cmd_restart + + log_info "GoToSocial exposed at https://$domain" + log_info "SSL certificate will be provisioned automatically" +} + +# Backup +cmd_backup() { + local backup_path="${1:-/tmp/gotosocial-backup-$(date +%Y%m%d-%H%M%S).tar.gz}" + + log_info "Creating backup..." + + # Stop container for consistent backup + local was_running=false + if container_running; then + was_running=true + cmd_stop + fi + + tar -czf "$backup_path" -C "$DATA_PATH" . 2>/dev/null || { + log_error "Backup failed" + [ "$was_running" = "true" ] && cmd_start + return 1 + } + + [ "$was_running" = "true" ] && cmd_start + + log_info "Backup created: $backup_path" + ls -lh "$backup_path" +} + +# Restore +cmd_restore() { + local backup_path="$1" + + [ -z "$backup_path" ] || [ ! -f "$backup_path" ] && { + echo "Usage: gotosocialctl restore " + return 1 + } + + log_info "Restoring from $backup_path..." + + # Stop container + container_running && cmd_stop + + # Clear existing data + rm -rf "$DATA_PATH"/* + + # Extract backup + tar -xzf "$backup_path" -C "$DATA_PATH" || { + log_error "Restore failed" + return 1 + } + + log_info "Restore complete" + cmd_start +} + +# Federation commands +cmd_federation_list() { + local port=$(get_config main port "8484") + + curl -s "http://127.0.0.1:$port/api/v1/instance/peers" 2>/dev/null | jq -r '.[]' 2>/dev/null || { + echo "Unable to fetch federation list. Is GoToSocial running?" + } +} + +# Show logs (JSON output) +cmd_logs() { + local lines="${1:-50}" + local logs + + logs=$(logread -e gotosocial 2>/dev/null | tail -n "$lines" | jq -R -s 'split("\n") | map(select(length > 0))' 2>/dev/null || echo "[]") + + echo "{\"logs\":$logs}" +} + +# Show help +cmd_help() { + cat < [options] + +Installation: + install [version] Install GoToSocial (default: v$GTS_VERSION) + uninstall [--keep-data] Remove GoToSocial + update [version] Update to new version + +Service: + start Start GoToSocial + stop Stop GoToSocial + restart Restart GoToSocial + reload Reload configuration + status Show status + +User Management: + user create [--admin] Create user + user list List users + user confirm Confirm user email + +Exposure: + emancipate Expose via HAProxy + SSL + +Backup: + backup [path] Backup data + restore Restore from backup + +Federation: + federation list List federated instances + +Other: + help Show this help + version Show version + +Examples: + gotosocialctl install + gotosocialctl start + gotosocialctl user create alice alice@example.com --admin + gotosocialctl emancipate social.mysite.com + +EOF +} + +# Main +case "$1" in + install) + cmd_install "$2" + ;; + uninstall) + cmd_uninstall "$2" + ;; + update) + cmd_stop + download_binary "${2:-$GTS_VERSION}" + cmd_start + ;; + start) + cmd_start + ;; + stop) + cmd_stop + ;; + restart) + cmd_restart + ;; + reload) + cmd_reload + ;; + status) + cmd_status + ;; + status-human) + cmd_status_human + ;; + users) + cmd_users + ;; + logs) + cmd_logs "$2" + ;; + user) + case "$2" in + create) + cmd_user_create "$3" "$4" "$5" + ;; + list) + cmd_user_list + ;; + confirm) + cmd_user_confirm "$3" + ;; + *) + echo "Usage: gotosocialctl user {create|list|confirm}" + ;; + esac + ;; + emancipate) + cmd_emancipate "$2" + ;; + backup) + cmd_backup "$2" + ;; + restore) + cmd_restore "$2" + ;; + federation) + case "$2" in + list) + cmd_federation_list + ;; + *) + echo "Usage: gotosocialctl federation {list}" + ;; + esac + ;; + version) + echo "gotosocialctl v$VERSION (GoToSocial v$GTS_VERSION)" + ;; + help|--help|-h|"") + cmd_help + ;; + *) + echo "Unknown command: $1" + cmd_help + exit 1 + ;; +esac diff --git a/package/secubox/secubox-app-gotosocial/files/usr/share/gotosocial/config.yaml.template b/package/secubox/secubox-app-gotosocial/files/usr/share/gotosocial/config.yaml.template new file mode 100644 index 00000000..9877bce9 --- /dev/null +++ b/package/secubox/secubox-app-gotosocial/files/usr/share/gotosocial/config.yaml.template @@ -0,0 +1,34 @@ +# GoToSocial Configuration Template +# This file is used as a reference. Actual config is generated by gotosocialctl. + +host: "${GTS_HOST}" +account-domain: "${GTS_HOST}" +protocol: "${GTS_PROTOCOL}" +bind-address: "${GTS_BIND_ADDRESS}" +port: ${GTS_PORT} + +db-type: "sqlite" +db-address: "/data/gotosocial.db" + +storage-backend: "local" +storage-local-base-path: "/data/storage" + +instance-expose-public-timeline: true +instance-expose-suspended: false + +accounts-registration-open: false +accounts-approval-required: true +accounts-reason-required: true + +media-image-max-size: 10485760 +media-video-max-size: 41943040 + +statuses-max-chars: 5000 +statuses-cw-max-chars: 100 +statuses-poll-max-options: 6 +statuses-poll-option-max-chars: 50 +statuses-media-max-files: 6 + +letsencrypt-enabled: false + +log-level: "info"