The server's half of D25: every account has a way in, and the anonymous accounts left from before go with the daily pruning. - The module signs nobody in anonymously: `anonymousIdp` and `gitHubUpgrade` are gone, and so is linking GitHub at the register. - A password is set only by someone whose address is theirs (a `firstPassword` code) or who signs in with GitHub alone (a fresh sign-in and a `signUp` code); an anonymous account is refused `wrongCredentials` before any code is used. - SimpleLogin refuses to link onto a caller without `identified` (`wrongCaller`), the member's endpoint too, so nothing turns an anonymous account into one with a way in. - `pruneAnonymousPeople` deletes every anonymous account left, however new, never one identified. Learn and Todo schedule it as `prune-accounts` and cancel the old `prune-abandoned-anonymous-people` entry once. - Learn: someone without `identified` keeps no files; `FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds up to 22 start with an anonymous account. - Learn's sync tests make their person and session by SQL in the development database, as Serverpod stores sessions. - Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
176 lines
10 KiB
Markdown
176 lines
10 KiB
Markdown
# Halkyon Learn
|
||
|
||
Halkyon Learn is for creating, sharing and collaborating on study material. It schedules
|
||
flashcards with [FSRS](https://github.com/open-spaced-repetition) to strengthen
|
||
retention, and organises study material together with the external references it draws
|
||
on. It is offline-first: everything works on the device, without a connection. And the
|
||
whole stack is transparent: the app, the server and everything needed to host an
|
||
instance are published, so anyone can see how an instance works and run their own.
|
||
|
||
- **Offline first.** Each device keeps its own database and syncs in the background
|
||
(`serverpod_offline_sync`). Edits made on two devices while offline are never lost:
|
||
card content is stored as append-only revisions, and a side changed differently on two
|
||
devices is shown as a conflict until the person picks a version
|
||
([ADR 0001](../../docs/adr/0001-offline-sync.md), [ADR 0003](../../docs/adr/0003-card-revisions.md)).
|
||
- **Every account has a way in**: SimpleLogin, whose ID alone the instance keeps, or an
|
||
email address with a password and an authenticator, the password never leaving the
|
||
device ([ADR 0020](../../docs/adr/0020-sign-in-with-simplelogin.md),
|
||
[ADR 0017](../../docs/adr/0017-password-totp-and-keys.md)). GitHub still signs in
|
||
those who have it, until the instance ends it.
|
||
- **No end-to-end encryption.** Content is readable by the operator of the instance, as on
|
||
Mastodon; whoever trusts no operator can run their own
|
||
([ADR 0002](../../docs/adr/0002-no-end-to-end-encryption.md)).
|
||
|
||
Targets: Linux, Windows, macOS, Android and iOS. There is no web build.
|
||
|
||
## Packages
|
||
|
||
Learn lives in `apps/learn/` of the Halkyon repository, one workspace with Halkyon's
|
||
other apps and the packages they share ([the repository's README](../../README.md)).
|
||
|
||
| Package | What it is | Licence |
|
||
|---|---|---|
|
||
| `halkyon_learn_server` | The [Serverpod](https://serverpod.dev) server: endpoints, models, migrations | SSPL-1.0 |
|
||
| `halkyon_learn_client` | The client, generated from the server's models | AGPL-3.0-or-later |
|
||
| `halkyon_learn_flutter` | The app (`app.halkyon.learn`) | AGPL-3.0-or-later |
|
||
| `halkyon_learn_curator` | Turns dictionary sources into the files the server loads | AGPL-3.0-or-later |
|
||
|
||
It builds on the shared packages in `packages/`: `halkyon_base` (sign-in, accounts, the
|
||
instance's basics; a Serverpod module), `halkyon_app` (instances, sign-in, sync) and
|
||
`halkyon_ui` (the look).
|
||
|
||
## Setting up
|
||
|
||
- **Flutter v3.47.5**, the version CI uses (`FLUTTER_VERSION` in
|
||
`.forgejo/workflows/ci.yaml`). Set up each platform you build for with Flutter's own
|
||
guides: [Linux](https://docs.flutter.dev/platform-integration/linux/setup),
|
||
[Windows](https://docs.flutter.dev/platform-integration/windows/setup),
|
||
[Android](https://docs.flutter.dev/platform-integration/android/setup),
|
||
[macOS](https://docs.flutter.dev/platform-integration/macos/setup),
|
||
[iOS](https://docs.flutter.dev/platform-integration/ios/setup).
|
||
- **The Serverpod CLI v4.0.4**: `dart install serverpod_cli@4.0.4`. Pass `--no-analytics`
|
||
to every call; otherwise it reports the commands run to Serverpod.
|
||
- **Linux**, on top of Flutter's packages, the libraries of four plugins: WebKitGTK
|
||
(pulled in by the sign-in package), libsecret (keeps the session), GStreamer (plays
|
||
sounds) and libmpv (plays videos from Jellyfin; the app doesn't start without it).
|
||
On Debian or Ubuntu, Flutter's and these together:
|
||
|
||
```sh
|
||
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev \
|
||
libwebkit2gtk-4.1-dev libsecret-1-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
|
||
libmpv-dev
|
||
```
|
||
|
||
- **Windows** needs Developer Mode (Settings › System › For developers): building makes
|
||
symbolic links, as Flutter links each plugin's native code into the build and the PDF
|
||
engine's build does too, and Windows lets ordinary users make them only in Developer
|
||
Mode.
|
||
- **No Docker** for development: `serverpod start` and the server's tests run an embedded
|
||
PostgreSQL. Docker Compose is only for running an instance (below).
|
||
- **For Learn's scripts in `apps/learn/tool/`**, run from `apps/learn`: Python 3
|
||
(`tool/app_icons.py` also needs Pillow, `python3-pil`); for releases what
|
||
[Releases](#releases) lists.
|
||
|
||
The rich-text editor comes from our fork of AppFlowy Editor
|
||
([project-halkyon/appflowy-editor](https://code.halkyon.app/project-halkyon/appflowy-editor)),
|
||
which `flutter pub get` fetches by itself.
|
||
|
||
## Running it locally
|
||
|
||
The server's secrets live in `apps/learn/halkyon_learn_server/config/passwords.yaml`,
|
||
which is never committed. In a fresh clone, create it with a `development` and a `test`
|
||
section, each holding `database`, `serverSideSessionKeyHashPepper` and `serviceSecret`
|
||
(for example from `openssl rand -base64 32`). GitHub sign-in is optional: add
|
||
`githubClientId` and `githubClientSecret` of your own GitHub OAuth app to `development`,
|
||
registered with the callback URLs `app.halkyon.learn://github-callback` and
|
||
`http://127.0.0.1/github-callback`.
|
||
|
||
Then start the server, its embedded database and the app:
|
||
|
||
```sh
|
||
cd apps/learn/halkyon_learn_server
|
||
serverpod --no-analytics start
|
||
```
|
||
|
||
`serverpod start` applies pending migrations and reloads on changes. The app talks to the
|
||
server named by `--dart-define=SERVER_URL=…`, or else to the one in
|
||
`apps/learn/halkyon_learn_flutter/assets/config.json` (`http://localhost:8080`). Without
|
||
`--no-analytics`, the Serverpod CLI reports usage to Serverpod.
|
||
|
||
## Tests
|
||
|
||
From the repository's root,
|
||
|
||
```sh
|
||
dart run tool/check.dart
|
||
```
|
||
|
||
runs the format check, the analyzer with the strict lints of `leancode_lint` and every
|
||
package's tests, Learn's server's (against an embedded PostgreSQL, no Docker needed), its
|
||
curator's and its app's among them; `dart run melos run test:flutter
|
||
--scope=halkyon_learn_flutter` runs the app's alone. To run the checks before every push:
|
||
`ln -s ../../tool/pre-push .git/hooks/pre-push`. CI runs them on every push too
|
||
(`.forgejo/workflows/ci.yaml`).
|
||
|
||
The sync tests need a running server (`serverpod start`); run them in
|
||
`apps/learn/halkyon_learn_flutter` with `flutter test -t requires-server`.
|
||
|
||
## Running an instance
|
||
|
||
`deploy/`, at the repository's root, holds a Docker Compose setup: Caddy (HTTPS), the
|
||
server and PostgreSQL. Copy `deploy/.env.example` to `deploy/.env` and fill it in; it
|
||
explains every value. The licences require offering the source of what the instance runs
|
||
to the people who use it.
|
||
|
||
learn.halkyon.eu itself deploys from `main` once CI is green: `deploy/redeploy.sh`, run by
|
||
the CI runner described in `deploy/runner/`, deploys only commits signed by a key in
|
||
`deploy/allowed-signers`. It backs up its database every hour, off-site and encrypted;
|
||
[`deploy/backups.md`](../../deploy/backups.md) says how to set that up and how to restore.
|
||
|
||
## Releases
|
||
|
||
Releases are published on [code.halkyon.app](https://code.halkyon.app/project-halkyon/halkyon/releases),
|
||
with the APK and the snap, and the APK also in the project's F-Droid repository
|
||
(`https://halkyon.app/fdroid/repo?fingerprint=E8082788756F6B7E682AA22FCEAD24B53E2A8B0912AD66D65E8C2D4F07BB94B0`,
|
||
added in the F-Droid app as a repository). The Snap Store (`halkyon-learn`) follows.
|
||
Their builds offer learn.halkyon.eu, the project's instance, at first start; people
|
||
may choose another there, and keep accounts on several.
|
||
|
||
- **Checking a download:** `SHA256SUMS` lists the checksum of every file, signed with the
|
||
maintainer's GPG key (`SHA256SUMS.asc`). The APK is signed with the app's key, whose
|
||
certificate has the SHA-256
|
||
`27:E2:43:FC:BD:C8:26:FF:98:AB:7D:BB:10:43:6C:98:C3:BE:A2:4B:2A:01:5E:94:E4:CF:C7:D1:FC:2A:00:6C`
|
||
(`apksigner verify --print-certs` shows it).
|
||
- **Versions** follow [semantic versioning](https://semver.org): `version` in
|
||
`apps/learn/halkyon_learn_flutter/pubspec.yaml`, `<major>.<minor>.<patch>+<build>`.
|
||
Since the last release, a commit typed `feat` raises the minor number, `fix` or `perf` the patch
|
||
number, and a breaking one (`feat!:` or a `BREAKING CHANGE:` line) the major number from
|
||
1.0 on, the minor one before; other commits release nothing by themselves. Every
|
||
release raises the build number: Android only installs a higher one over an older, and
|
||
the server tells apps below the build it supports to update.
|
||
- **Making one** is for maintainers, on their own machine and never in CI, from the
|
||
repository's root: `dart run tool/release.dart learn`, or with `minor` (or `major`,
|
||
`patch`) after `learn` to choose; `--dry-run` shows what it would do and changes
|
||
nothing. It is the apps' shared release tool ([ADR 0015](../../docs/adr/0015-one-repository.md)),
|
||
configured in `apps/learn/release.dart`. It needs `main` clean, pushed and green in CI, the app's key, the
|
||
F-Droid repository's key and a Forgejo token; its header says more. Tags are
|
||
`learn-vX.Y.Z`, and only commits touching Learn count for its version. It shows the version and its commits and asks
|
||
first, then writes the version into the pubspec in a signed release commit with a
|
||
signed tag, builds the APK and the snap (`snap/snapcraft.yaml`, built in LXD; its icon
|
||
comes from `apps/learn/tool/app_icons.py`), checks both, signs the checksums, pushes, and
|
||
publishes on code.halkyon.app, in the F-Droid repository and in the Snap Store's edge
|
||
channel (`snapcraft login` first). Besides Flutter and the Android SDK, it uses snapcraft with
|
||
LXD, a JDK (keytool), GnuPG, curl, jq, unzip, squashfs-tools, rsync and fdroidserver.
|
||
Release builds made without the app's key are signed with the debug key and can't
|
||
update a released app.
|
||
- **The F-Droid repository** is kept by `tool/fdroid_repo.sh` (fdroidserver v2.4.5,
|
||
e.g. `pipx install fdroidserver==2.4.5`); its settings and the app's listing are in
|
||
`tool/release/fdroid/`.
|
||
|
||
## More
|
||
|
||
- [`AGENTS.md`](AGENTS.md): how Learn's code is organised and the rules it follows; the
|
||
[repository's](../../AGENTS.md) for what holds for every app.
|
||
- [`docs/adr/`](../../docs/adr/): the architecture decisions and their reasons.
|
||
- [`CONTRIBUTING.md`](../../CONTRIBUTING.md): licences of contributions and the sign-off.
|
||
- [`LICENSE.md`](../../LICENSE.md): which licence covers what.
|