# SPDX-License-Identifier: AGPL-3.0-or-later """Configuration helper for Miniflux feed reader.""" import json import os import pathlib from typing import Tuple import pexpect from plinth import action_utils from plinth.actions import privileged, secret_str from plinth.db import postgres, dbconfig from plinth.utils import is_non_empty_file STATIC_SETTINGS = { 'BASE_URL': 'http://localhost/miniflux/', 'RUN_MIGRATIONS': 1, 'PORT': 8788 } ENV_VARS_FILE = '/etc/miniflux/freedombox.conf' DBCONFIG_PATH = '/etc/dbconfig-common/miniflux.conf' DB_BACKUP_FILE = '/var/lib/plinth/backups-data/miniflux-database.sql' def _dict_to_env_file(dictionary: dict[str, str]) -> str: """Write a dictionary into a systemd environment file format.""" return "\n".join((f"{k}={v}" for k, v in dictionary.items())) def _env_file_to_dict(env_vars: str) -> dict[str, str]: """Return systemd environtment variables as a dictionary.""" return { line.split('=')[0]: line.split('=')[1].strip() for line in env_vars.splitlines() if line.strip() and not line.strip().startswith('#') } @privileged def pre_setup(): """Perform pre-install actions for Miniflux.""" vars_file = pathlib.Path(ENV_VARS_FILE) vars_file.parent.mkdir(parents=True, exist_ok=True) existing_settings = {} if is_non_empty_file(ENV_VARS_FILE): # Any comments in the file will be dropped. existing_settings = _env_file_to_dict(vars_file.read_text()) new_settings = existing_settings | STATIC_SETTINGS vars_file.write_text(_dict_to_env_file(new_settings)) def _run_miniflux_interactively(command: str, username: str, password: str) -> Tuple[str, dict]: """Fill interactive terminal prompt for username and password.""" args = ['-c', '/etc/miniflux/miniflux.conf', command] os.environ['LOG_FORMAT'] = 'json' child = pexpect.spawn('miniflux', args, env=os.environ) # The CLI is in English only. child.expect('Enter Username: ') child.sendline(username) child.expect('Enter Password: ') child.sendline(password) child.expect(pexpect.EOF) raw_message = child.before.decode() # type: ignore try: json_message = json.loads(raw_message) except (KeyError, json.JSONDecodeError): json_message = {} child.close() if child.exitstatus or child.signalstatus: message = json_message.get('msg') if json_message else raw_message raise Exception(message) return raw_message, json_message @privileged def create_admin_user(username: str, password: secret_str): """Create a new admin user for Miniflux CLI. Raise exception if a user with the name already exists or otherwise fails. """ _, json_message = _run_miniflux_interactively('--create-admin', username, password) # user_id is allocated only when a new user is created successfully. if json_message and not json_message.get('user_id'): raise Exception(json_message.get('msg')) @privileged def reset_user_password(username: str, password: secret_str): """Reset a user password using Miniflux CLI. Raise exception if the user does not exist or otherwise fails. """ _run_miniflux_interactively('--reset-password', username, password) @privileged def uninstall(): """Ensure that the database is removed.""" action_utils.debconf_set_selections([ 'miniflux miniflux/purge boolean true', 'miniflux miniflux/dbconfig-install boolean true', 'miniflux miniflux/dbconfig-reinstall boolean true' 'miniflux miniflux/dbconfig-upgrade boolean true', 'miniflux miniflux/dbconfig-remove boolean true', ]) @privileged def dump_database(): """Dump database to file.""" config = dbconfig.get_credentials(DBCONFIG_PATH) postgres.dump_database(DB_BACKUP_FILE, config['database']) @privileged def restore_database(): """Restore database from file.""" config = dbconfig.get_credentials(DBCONFIG_PATH) postgres.restore_database(DB_BACKUP_FILE, config['database'], config['user'], config['password'])