import 'dart:async'; import 'dart:io'; import 'package:halkyon_base_server/base.dart'; import 'package:halkyon_learn_server/src/auth/learn_base.dart'; import 'package:halkyon_learn_server/src/dictionary/device_dictionaries.dart'; import 'package:halkyon_learn_server/src/dictionary/dictionary_loader.dart'; import 'package:halkyon_learn_server/src/generated/serverpod.dart'; import 'package:halkyon_learn_server/src/grammar/grammar_checking.dart'; import 'package:halkyon_learn_server/src/instance/jellyfin_suggestion.dart'; import 'package:serverpod_auth_idp_server/core.dart'; /// The starting point of the Serverpod server. Future run(List args) async { // Initialize Serverpod. The generated Serverpod class is already connected // with your project's generated code. final pod = Serverpod(args); // What every Halkyon server has (ADR 0014): sign-in, the account and its // deletion, the pruning, the instance's basics, set up for Learn. // Learn holds the instance's register of accounts (ADR 0016), telling the // instance's other services when one goes. HalkyonBase.configure(pod, learnBase(accounts: learnAccounts())); pod // Token managers validate and issue authentication keys; identity // providers are the ways people can sign in (halkyon_base's // auth_config.dart). ..initializeAuthServices( tokenManagerBuilders: tokenManagerBuilders, identityProviderBuilders: identityProviderBuilders(pod), ) // Files live in Postgres until the self-hosted S3-compatible storage arrives later. ..addCloudStorage(DatabaseCloudStorage('private')) ..addCloudStorage(DatabaseCloudStorage('public')); // The calls of the instance's other services, on the web server // (ADR 0016); Caddy never forwards them. addInstanceRoutes(pod); // Where SimpleLogin sends a sign-in back, which forwards it to the app // (ADR 0020): the register's page, as its SimpleLogin app is. addSimpleLoginCallbackRoute(pod); // The instance's privacy page, `/privacy`, made from its settings (ADR // 0021, D39), before the profile pages' route, whose one segment would // take it. addPrivacyPageRoute(pod); // Profile pages, `/@handle`, which profile links link back to (ADR 0011), // halkyon_base's since profiles are the instance's register's (ADR 0016). addProfilePageRoute(pod); // What devices download to look Japanese up by themselves (ADR 0012). addDeviceDictionaryRoute(pod); // Start the server. await pod.start(); // Whether a Jellyfin server answers where the instance would suggest one // (decided 2026-10-13), so instance.info() can name it from the first ask. unawaited(JellyfinSuggestion.of(pod).look()); // Which languages the instance's LanguageTool checks (ADR 0024), likewise. unawaited(GrammarChecking.of(pod).look()); // The accounts nobody signs into any more, daily (decided 2026-09-30; // every anonymous account left since ADR 0020, D25). Scheduling always // adds an entry, so the previous one goes first, and the one scheduled // under the earlier name, which would call nothing. await pod.futureCalls.cancel(_anonymousPruning); await pod.futureCalls.cancel(_pruning); await pod.futureCalls .callRecurring(identifier: _pruning) .cron('17 3 * * *') .pruning .pruneAccounts(); // The IP addresses of sign-up attempts, hourly, once the rate limit is // done with them (decided 2026-10-10). await pod.futureCalls.cancel(_attemptPruning); await pod.futureCalls .callRecurring(identifier: _attemptPruning) .cron('27 * * * *') .pruning .forgetSignUpAttempts(); // The deletions the instance's members haven't confirmed yet, hourly // (ADR 0016). await pod.futureCalls.cancel(_deletionNotices); await pod.futureCalls .callRecurring(identifier: _deletionNotices) .cron('52 * * * *') .pruning .tellMembersOfDeletions(); // The assistant connector went with its codes and tokens (ADR 0022): what // an earlier server scheduled to forget them would call nothing. await pod.futureCalls.cancel(_connectorPruning); // The files nobody can reach any more, daily (ADR 0004, rule 5). await pod.futureCalls.cancel(_filePruning); await pod.futureCalls .callRecurring(identifier: _filePruning) .cron('47 3 * * *') .filePruning .pruneFiles(); // How many people learn each published studyset here (ADR 0005, rule 4). await pod.futureCalls.cancel(_learnerCount); await pod.futureCalls .callRecurring(identifier: _learnerCount) .cron('37 4 * * *') .learnerCount .countLearners(); // The dictionaries in the server's folder, those that changed since; in // the background, as loading takes a minute or two. Then again hourly, as // the dictionaries service replaces them weekly (and fills its volume // first after an instance's first start). unawaited(_loadDictionaries(pod)); await pod.futureCalls.cancel(_dictionaryReload); await pod.futureCalls .callRecurring(identifier: _dictionaryReload) .cron('7 * * * *') .dictionaryReload .reloadDictionaries(); } /// Loads the dictionaries in the server's folder ([dictionaryFolder]). /// [loadDictionaries] throws nothing; a session that can't be had is /// reported here, as an error in the background would end the server. Future _loadDictionaries(Serverpod pod) async { try { await pod.withSession((session) async { await loadDictionaries(session, dictionaryFolder()); // What devices download, ready before the first asks (ADR 0012). await DeviceDictionaries.of(pod).prepare(session); }); } on Object catch (error) { stderr.writeln('Loading the dictionaries failed: $error'); } } /// The identifier of the daily pruning. const _pruning = 'prune-accounts'; /// The identifier the daily pruning had until ADR 0020, D25. const _anonymousPruning = 'prune-abandoned-anonymous-people'; /// The identifier of the hourly forgetting of sign-up attempts. const _attemptPruning = 'forget-sign-up-attempts'; /// The identifier of the hourly telling of the members of deleted accounts. const _deletionNotices = 'tell-members-of-deletions'; /// The identifier of the daily forgetting of the connector's expired secrets, /// scheduled until ADR 0022. const _connectorPruning = 'forget-connector-secrets'; /// The identifier of the daily pruning of files. const _filePruning = 'prune-unreferenced-files'; /// The identifier of the hourly rereading of the dictionaries. const _dictionaryReload = 'reload-dictionaries'; /// The identifier of the daily count of each published studyset's learners. const _learnerCount = 'count-learners';