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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-13 13:52:58 +01:00
parent 92b422e900
commit f20bb1df6b
12 changed files with 2070 additions and 0 deletions

View File

@ -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))

View File

@ -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
});

View File

@ -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();
}
});

View File

@ -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
});

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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"]
}
}
}

View File

@ -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))

View File

@ -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'

View File

@ -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
}

View File

@ -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" <<EOF
127.0.0.1 localhost
::1 localhost
EOF
log_info "Rootfs created"
}
# Generate LXC config
create_lxc_config() {
local host=$(get_config main host "social.local")
local port=$(get_config main port "8484")
local data_path=$(get_config container data_path "$DATA_PATH")
log_info "Creating LXC configuration..."
mkdir -p "$LXC_PATH"
cat > "$LXC_PATH/config" <<EOF
# GoToSocial LXC Configuration
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_PATH/rootfs
lxc.arch = aarch64
# Network: use host network
lxc.net.0.type = none
# Mount points
lxc.mount.auto = proc:mixed sys:ro
lxc.mount.entry = $data_path data none bind,create=dir 0 0
# Environment
lxc.environment = GTS_HOST=$host
lxc.environment = GTS_PORT=$port
lxc.environment = GTS_DB_TYPE=sqlite
lxc.environment = GTS_DB_ADDRESS=/data/gotosocial.db
lxc.environment = GTS_STORAGE_LOCAL_BASE_PATH=/data/storage
lxc.environment = GTS_LETSENCRYPT_ENABLED=false
lxc.environment = HOME=/data
# Security
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio
# Init command
lxc.init.cmd = /opt/gotosocial/gotosocial server
EOF
log_info "LXC config created"
}
# Generate GoToSocial config
generate_config() {
local host=$(get_config main host "social.local")
local port=$(get_config main port "8484")
local protocol=$(get_config main protocol "https")
local bind=$(get_config main bind_address "0.0.0.0")
local instance_name=$(get_config main instance_name "SecuBox Social")
local instance_desc=$(get_config main instance_description "A SecuBox Fediverse instance")
local reg_open=$(get_config main accounts_registration_open "false")
local approval=$(get_config main accounts_approval_required "true")
local data_path=$(get_config container data_path "$DATA_PATH")
mkdir -p "$data_path"
cat > "$data_path/config.yaml" <<EOF
# GoToSocial Configuration
# Generated by SecuBox gotosocialctl
host: "$host"
account-domain: "$host"
protocol: "$protocol"
bind-address: "$bind"
port: $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
instance-expose-suspended-web: false
accounts-registration-open: $reg_open
accounts-approval-required: $approval
accounts-reason-required: true
media-image-max-size: 10485760
media-video-max-size: 41943040
media-description-min-chars: 0
media-description-max-chars: 500
media-remote-cache-days: 30
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
oidc-enabled: false
smtp-host: ""
smtp-port: 0
syslog-enabled: false
syslog-protocol: "udp"
syslog-address: "localhost:514"
log-level: "info"
log-db-queries: false
advanced-cookies-samesite: "lax"
advanced-rate-limit-requests: 300
advanced-throttling-multiplier: 8
cache:
gts:
account-max-size: 2000
account-ttl: "30m"
account-sweep-freq: "1m"
status-max-size: 2000
status-ttl: "30m"
status-sweep-freq: "1m"
EOF
# Create storage directories
mkdir -p "$data_path/storage"
log_info "Configuration generated at $data_path/config.yaml"
}
# Install GoToSocial
cmd_install() {
local version="${1:-$GTS_VERSION}"
log_info "Installing GoToSocial v${version}..."
# Check dependencies
command -v lxc-start >/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 <username> <email>'"
}
# 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 <<EOF
{
"installed": $installed,
"container_running": $container_state,
"service_running": $service_state,
"host": "$host",
"port": "$port",
"version": "$version",
"tor_enabled": $([ "$tor_enabled" = "1" ] && echo "true" || echo "false"),
"dns_enabled": $([ "$dns_enabled" = "1" ] && echo "true" || echo "false"),
"mesh_enabled": $([ "$mesh_enabled" = "1" ] && echo "true" || echo "false")
}
EOF
}
# Status (human readable)
cmd_status_human() {
if container_running; then
echo "GoToSocial: running"
lxc-info -n "$LXC_NAME" -P "$(dirname $LXC_PATH)" 2>/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 <username> <email> [--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 <username>"
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 <domain>"
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 <backup-file>"
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 <<EOF
GoToSocial Controller for SecuBox v$VERSION
Usage: gotosocialctl <command> [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 <user> <email> [--admin] Create user
user list List users
user confirm <user> Confirm user email
Exposure:
emancipate <domain> Expose via HAProxy + SSL
Backup:
backup [path] Backup data
restore <path> 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

View File

@ -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"