199 lines
7.5 KiB
Dart
199 lines
7.5 KiB
Dart
|
|
import 'dart:async';
|
||
|
|
import 'dart:typed_data';
|
||
|
|
|
||
|
|
import 'package:halkyon_base_client/halkyon_base_client.dart' as base;
|
||
|
|
import 'package:halkyon_content/halkyon_content.dart';
|
||
|
|
import 'package:halkyon_learn_agent/src/agent_home.dart';
|
||
|
|
import 'package:halkyon_learn_agent/src/agent_library.dart';
|
||
|
|
import 'package:halkyon_learn_agent/src/agent_sync.dart';
|
||
|
|
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
|
||
|
|
import 'package:halkyon_learn_core/core/asset/data/asset_repository.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/asset/data/sync_asset_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/privacy/data/content_keys.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/privacy/data/privacy.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/privacy/data/privacy_ledger.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/privacy/data/shared_space_keys.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/privacy/data/sync_content_key_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/sync/space_router.dart';
|
||
|
|
import 'package:halkyon_learn_core/core/sync/sync_space_lookup.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/card/data/card_repository.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/card/data/sync_card_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/feedback/data/feedback_envelopes.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/feedback/data/feedback_repository.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/feedback/data/serverpod_feedback_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/feedback/data/sync_feedback_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/learning_path/data/learning_path_repository.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/learning_path/data/sync_learning_path_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/note/data/note_repository.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/note/data/sync_note_gateway.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/studyset/data/studyset_repository.dart';
|
||
|
|
import 'package:halkyon_learn_core/feature/studyset/data/sync_studyset_gateway.dart';
|
||
|
|
import 'package:serverpod_auth_core_client/serverpod_auth_core_client.dart';
|
||
|
|
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||
|
|
|
||
|
|
/// The agent signed in on its instance with its data open (Halkyon's ADR
|
||
|
|
/// 0044, D7, D43, D49): its library, its sync, and what closes them.
|
||
|
|
final class const OpenAgent({
|
||
|
|
/// What its tools read and suggest through.
|
||
|
|
required final AgentLibrary library,
|
||
|
|
|
||
|
|
/// Its sync.
|
||
|
|
required final AgentSync sync,
|
||
|
|
|
||
|
|
/// Closes its data and its connection.
|
||
|
|
required final Future<void> Function() close,
|
||
|
|
});
|
||
|
|
|
||
|
|
/// Signs the agent in on [instance] with the token kept in [home], makes
|
||
|
|
/// and sets its keys on its first sign-in, and opens its data as Learn's
|
||
|
|
/// app opens a person's (`_openPersonData`): its local database, its
|
||
|
|
/// sync, its keys and those of the spaces shared with it, and the
|
||
|
|
/// repositories its library reads and suggests through.
|
||
|
|
Future<OpenAgent> openAgent(AgentHome home, Uri instance) async {
|
||
|
|
final token =
|
||
|
|
await home.token() ??
|
||
|
|
(throw const AgentHomeRefused(
|
||
|
|
'No token yet: run `halkyon-agent sign-in` with the one your owner '
|
||
|
|
'made in their Profile.',
|
||
|
|
));
|
||
|
|
final sessions = ClientAuthSessionManager(storage: _MemoryAuthStorage());
|
||
|
|
final client = protocol.Client('$instance')..authSessionManager = sessions;
|
||
|
|
final success = await client.modules.base.agentSignIn.signIn(token: token);
|
||
|
|
await sessions.updateSignedInUser(success);
|
||
|
|
final me = success.authUserId.uuid;
|
||
|
|
final keys = await _keysOf(home, client.modules.base);
|
||
|
|
|
||
|
|
final db = await client.createSyncSession(
|
||
|
|
home.database,
|
||
|
|
persistentUserId: success.authUserId,
|
||
|
|
);
|
||
|
|
final spaceLookup = SyncSpaceLookup(db, me);
|
||
|
|
final spaces = SpaceRouter(spaceLookup);
|
||
|
|
db.db.newRowSpace = (row) async => switch (await spaces.spaceOf(row)) {
|
||
|
|
final space? => protocol.UuidValue.withValidation(space),
|
||
|
|
null => null,
|
||
|
|
};
|
||
|
|
final sync = AgentSync(
|
||
|
|
({required onMerge}) => client.offlineSync.syncOnce(
|
||
|
|
db,
|
||
|
|
onInboundMerge: (space) {
|
||
|
|
spaces.forget();
|
||
|
|
onMerge();
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
final ledger = PrivacyLedger(FilePrivacyLedgerStore(home.privacy));
|
||
|
|
await ledger.open();
|
||
|
|
final contentKeys = ContentKeys(
|
||
|
|
SyncContentKeyGateway(db, sync),
|
||
|
|
SodiumContentCipher(),
|
||
|
|
() async => keys,
|
||
|
|
seen: ledger,
|
||
|
|
// An agent always has keys (D6).
|
||
|
|
registerHasKeys: () async => true,
|
||
|
|
);
|
||
|
|
final spaceKeys = SharedSpaceKeys(
|
||
|
|
SharedKeys(
|
||
|
|
ServerpodSharedKeysGateway(client.modules.base),
|
||
|
|
KeyPins(FileKeyPinStore(home.pins)),
|
||
|
|
contentKeys.cipher,
|
||
|
|
me: me,
|
||
|
|
),
|
||
|
|
contentKeys.cipher,
|
||
|
|
() async => (await spaceLookup.sharedStudysets()).keys.toSet(),
|
||
|
|
);
|
||
|
|
final privacy = Privacy(db, contentKeys)
|
||
|
|
..requestSync = sync.requestSync
|
||
|
|
..spaces = spaces
|
||
|
|
..spaceKeys = spaceKeys;
|
||
|
|
await contentKeys.refresh();
|
||
|
|
spaceKeys.keepUpOn(sync.synced);
|
||
|
|
|
||
|
|
final studysets = StudysetRepository(SyncStudysetGateway(db, sync, privacy));
|
||
|
|
final cards = CardRepository(SyncCardGateway(db, sync, me, privacy));
|
||
|
|
final notes = NoteRepository(SyncNoteGateway(db, sync, me, privacy));
|
||
|
|
final paths = LearningPathRepository(
|
||
|
|
SyncLearningPathGateway(db, sync, me, privacy),
|
||
|
|
);
|
||
|
|
final assets = AssetRepository(
|
||
|
|
SyncAssetGateway(db, sync, privacy),
|
||
|
|
ServerpodFileServerGateway(client.modules.base),
|
||
|
|
DirectoryFileCache(home.files),
|
||
|
|
privacy,
|
||
|
|
uploadTriggers: sync.synced,
|
||
|
|
);
|
||
|
|
privacy
|
||
|
|
..files = assets
|
||
|
|
..maintainOn(sync.merges);
|
||
|
|
final envelopes = PrivacyFeedbackEnvelopes(privacy, spaces, studysets);
|
||
|
|
final library = AgentLibrary(
|
||
|
|
studysets: studysets,
|
||
|
|
cards: cards,
|
||
|
|
notes: notes,
|
||
|
|
feedback: FeedbackRepository(
|
||
|
|
SyncFeedbackGateway(db, sync, spaces),
|
||
|
|
ServerpodFeedbackGateway(client, envelopes),
|
||
|
|
cards,
|
||
|
|
notes,
|
||
|
|
paths,
|
||
|
|
assets,
|
||
|
|
envelopes,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
return OpenAgent(
|
||
|
|
library: library,
|
||
|
|
sync: sync,
|
||
|
|
close: () async {
|
||
|
|
await sync.dispose();
|
||
|
|
await assets.dispose();
|
||
|
|
await studysets.dispose();
|
||
|
|
await cards.dispose();
|
||
|
|
await notes.dispose();
|
||
|
|
await paths.dispose();
|
||
|
|
await db.close();
|
||
|
|
client.close();
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The agent's keys: those kept in [home], else new ones, kept before
|
||
|
|
/// their public halves are set on the register, once (D6). Keys kept whose
|
||
|
|
/// public halves the register already has stay as they are.
|
||
|
|
Future<UnlockedAccountKeys> _keysOf(
|
||
|
|
AgentHome home,
|
||
|
|
base.Caller agents,
|
||
|
|
) async {
|
||
|
|
final kept = await home.keys();
|
||
|
|
final keys = kept ?? await newAccountKeys();
|
||
|
|
if (kept == null) {
|
||
|
|
await home.keepKeys(keys);
|
||
|
|
}
|
||
|
|
if (!await home.keysSet()) {
|
||
|
|
try {
|
||
|
|
await agents.agents.setKeys(
|
||
|
|
encryption: ByteData.sublistView(keys.encryptionPublicKey),
|
||
|
|
signing: ByteData.sublistView(keys.signingPublicKey),
|
||
|
|
);
|
||
|
|
} on base.AgentRefusedException catch (refused) {
|
||
|
|
// Set by an earlier run that stopped before it noted so; keys made
|
||
|
|
// anew now aren't those, and the agent can't take part.
|
||
|
|
if (refused.reason != base.AgentRefusal.keysSet || kept == null) {
|
||
|
|
rethrow;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
await home.noteKeysSet();
|
||
|
|
}
|
||
|
|
return keys;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Keeps the agent's session in memory: it signs in with its token at
|
||
|
|
/// every start, for a day's session (D43).
|
||
|
|
final class _MemoryAuthStorage() implements ClientAuthSuccessStorage {
|
||
|
|
AuthSuccess? _session;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<AuthSuccess?> get() async => _session;
|
||
|
|
|
||
|
|
@override
|
||
|
|
Future<void> set(AuthSuccess? data) async => _session = data;
|
||
|
|
}
|