refactor(profile): handles and profiles move into halkyon_base

People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.

Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 22:20:27 +02:00
commit 4107cc3087
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
82 changed files with 38851 additions and 1197 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,141 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "public_profile",
"dartName": "PublicProfile",
"module": "halkyon_base",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 6,
"isNullable": false,
"columnDefault": "serial",
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "authUserId",
"fieldName": "authUserId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "handle",
"fieldName": "handle",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "handleKey",
"fieldName": "handleKey",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "displayName",
"fieldName": "displayName",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "bio",
"fieldName": "bio",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "links",
"fieldName": "links",
"columnType": 8,
"isNullable": false,
"dartType": "List<String>"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "verifiedLinks",
"fieldName": "verifiedLinks",
"columnType": 8,
"isNullable": true,
"dartType": "List<String>?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "avatarHash",
"fieldName": "avatarHash",
"columnType": 0,
"isNullable": true,
"dartType": "String?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "firstPublishedAt",
"fieldName": "firstPublishedAt",
"columnType": 4,
"isNullable": true,
"dartType": "DateTime?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "updatedAt",
"fieldName": "updatedAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
}
],
"foreignKeys": [],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "public_profile_person_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "authUserId"
}
],
"type": "btree",
"isUnique": true,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "public_profile_handle_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "handleKey"
}
],
"type": "btree",
"isUnique": true,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -16,6 +16,7 @@ part '20261008165051407-pdf-position/migration.dart';
part '20261009224606274-feedback/migration.dart';
part '20261012162411835-studyset-deleted-at/migration.dart';
part '20261019121406578-halkyon-base/migration.dart';
part '20261019201023295-profiles-in-halkyon-base/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -34,6 +35,7 @@ class MigrationRegistry {
_Migration20261009224606274_feedback(),
_Migration20261012162411835_studyset_deleted_at(),
_Migration20261019121406578_halkyon_base(),
_Migration20261019201023295_profiles_in_halkyon_base(),
];
/// List of all client-side database migration versions.

View file

@ -49,10 +49,6 @@ import 'package:halkyon_learn_client/src/protocol/feedback/feedback_view.dart'
as _idpwh21d;
import 'package:halkyon_learn_client/src/protocol/instance/instance_info.dart'
as _i26u8vl4;
import 'package:halkyon_learn_client/src/protocol/profile/profile.dart'
as _ihcmr71y;
import 'package:halkyon_learn_client/src/protocol/profile/profile_refusal.dart'
as _ibeo2wv6;
import 'package:halkyon_learn_client/src/protocol/publish/publication.dart'
as _ij0cqo11;
import 'package:http/http.dart' as _i85jenna;
@ -772,121 +768,6 @@ class EndpointInstance extends _isc.EndpointRef {
);
}
/// Your public profile (ADR 0005, rule 2): the handle others find you by,
/// your display name, bio and links. Only for people with an identity: a
/// profile is for others to see.
///
/// The person always comes from the session, never from the client: a
/// profile is only ever the caller's own.
/// {@category Endpoint}
class EndpointProfile extends _isc.EndpointRef {
EndpointProfile(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'profile';
/// Your public profile; null while you have none.
_ida.Future<_ihcmr71y.Profile?> mine() =>
caller.callServerEndpoint<_ihcmr71y.Profile?>(
'profile',
'mine',
{},
);
/// Saves your public profile, making it if you have none. Throws
/// [ProfileRefusedException] when it can't be saved (see
/// [ProfileRefusal]): a handle someone else has, whatever its case, or a
/// new handle once you have published.
_ida.Future<_ihcmr71y.Profile> save(
String handle,
String displayName,
String bio,
List<String> links,
) => caller.callServerEndpoint<_ihcmr71y.Profile>(
'profile',
'save',
{
'handle': handle,
'displayName': displayName,
'bio': bio,
'links': links,
},
);
/// Checks which of your links' pages link back to your profile's page
/// with `rel="me"` (ADR 0011), and keeps the answer: the app asks after a
/// link is saved, and on Check again. At most 10 times an hour, as each
/// check fetches every page linked ([ProfileRefusal.tooManyLinkChecks]).
/// Throws [NotFoundException] while you have no profile.
_ida.Future<_ihcmr71y.Profile> checkLinks() =>
caller.callServerEndpoint<_ihcmr71y.Profile>(
'profile',
'checkLinks',
{},
);
/// Why [handle] couldn't be yours, as saving it would refuse it: not a
/// handle, reserved for the instance, someone else's whatever its case,
/// or a new one once you have published; null when it can, as your own
/// can. For the app to say so while it is typed (decided 2026-10-12);
/// saving checks again. Whose a handle is, anyone can find out already
/// ([ProfilesEndpoint.byHandle]), so this tells nothing new.
_ida.Future<_ibeo2wv6.ProfileRefusal?> checkHandle(String handle) =>
caller.callServerEndpoint<_ibeo2wv6.ProfileRefusal?>(
'profile',
'checkHandle',
{'handle': handle},
);
/// Shows [png] as your avatar, in place of any before: a PNG of at most
/// 1 MB, which the app draws from the picture chosen. Throws
/// [ProfileRefusedException] for anything else, and [NotFoundException]
/// while you have no profile.
_ida.Future<_ihcmr71y.Profile> setAvatar(_idt.ByteData png) =>
caller.callServerEndpoint<_ihcmr71y.Profile>(
'profile',
'setAvatar',
{'png': png},
);
/// Shows no avatar any more. Throws [NotFoundException] while you have no
/// profile.
_ida.Future<_ihcmr71y.Profile> removeAvatar() =>
caller.callServerEndpoint<_ihcmr71y.Profile>(
'profile',
'removeAvatar',
{},
);
/// Removes your public profile, freeing its handle; nothing when you have
/// none. Throws [ProfileRefusedException] once you have published: others
/// refer to it.
_ida.Future<void> remove() => caller.callServerEndpoint<void>(
'profile',
'remove',
{},
);
}
/// The public profiles of others, found by their handles. Any signed-in
/// person may read them: they are public.
/// {@category Endpoint}
class EndpointProfiles extends _isc.EndpointRef {
EndpointProfiles(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'profiles';
/// The public profile with [handle], whatever its case. Throws
/// [NotFoundException] when there is none.
_ida.Future<_ihcmr71y.Profile> byHandle(String handle) =>
caller.callServerEndpoint<_ihcmr71y.Profile>(
'profiles',
'byHandle',
{'handle': handle},
);
}
/// Publishing your studysets (ADR 0005, rule 3): each release a commit and
/// a tag in a git repository of the studyset's own, which the server builds
/// from your synced rows. Only for people with an identity and a public
@ -1014,8 +895,6 @@ class Client extends _isc.ServerpodClientShared {
feedback = EndpointFeedback(this);
inbox = EndpointInbox(this);
instance = EndpointInstance(this);
profile = EndpointProfile(this);
profiles = EndpointProfiles(this);
publish = EndpointPublish(this);
modules = Modules(this);
}
@ -1040,10 +919,6 @@ class Client extends _isc.ServerpodClientShared {
late final EndpointInstance instance;
late final EndpointProfile profile;
late final EndpointProfiles profiles;
late final EndpointPublish publish;
late final Modules modules;
@ -1060,8 +935,6 @@ class Client extends _isc.ServerpodClientShared {
'feedback': feedback,
'inbox': inbox,
'instance': instance,
'profile': profile,
'profiles': profiles,
'publish': publish,
};

View file

@ -82,9 +82,6 @@ import 'note/note.dart' as _i9njjhyv;
import 'note/note_revision.dart' as _i6j5dng4;
import 'pdf_source/pdf_reading.dart' as _i4l20c7i;
import 'pdf_source/pdf_source.dart' as _iz838ro9;
import 'profile/profile.dart' as _ia5j8fch;
import 'profile/profile_refusal.dart' as _i6j6efd1;
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
import 'publish/publication.dart' as _i9o1kyhh;
import 'publish/publish_refusal.dart' as _iofgvwaa;
import 'publish/publish_refused_exception.dart' as _itdn8966;
@ -140,9 +137,6 @@ export 'note/note.dart';
export 'note/note_revision.dart';
export 'pdf_source/pdf_reading.dart';
export 'pdf_source/pdf_source.dart';
export 'profile/profile.dart';
export 'profile/profile_refusal.dart';
export 'profile/profile_refused_exception.dart';
export 'publish/publication.dart';
export 'publish/publish_refusal.dart';
export 'publish/publish_refused_exception.dart';
@ -2438,15 +2432,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _iz838ro9.PdfSource) {
return _iz838ro9.PdfSource.fromJson(data) as T;
}
if (t == _ia5j8fch.Profile) {
return _ia5j8fch.Profile.fromJson(data) as T;
}
if (t == _i6j6efd1.ProfileRefusal) {
return _i6j6efd1.ProfileRefusal.fromJson(data) as T;
}
if (t == _idvc5e7p.ProfileRefusedException) {
return _idvc5e7p.ProfileRefusedException.fromJson(data) as T;
}
if (t == _i9o1kyhh.Publication) {
return _i9o1kyhh.Publication.fromJson(data) as T;
}
@ -2654,19 +2639,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_iz838ro9.PdfSource?>()) {
return (data != null ? _iz838ro9.PdfSource.fromJson(data) : null) as T;
}
if (t == _isc.getType<_ia5j8fch.Profile?>()) {
return (data != null ? _ia5j8fch.Profile.fromJson(data) : null) as T;
}
if (t == _isc.getType<_i6j6efd1.ProfileRefusal?>()) {
return (data != null ? _i6j6efd1.ProfileRefusal.fromJson(data) : null)
as T;
}
if (t == _isc.getType<_idvc5e7p.ProfileRefusedException?>()) {
return (data != null
? _idvc5e7p.ProfileRefusedException.fromJson(data)
: null)
as T;
}
if (t == _isc.getType<_i9o1kyhh.Publication?>()) {
return (data != null ? _i9o1kyhh.Publication.fromJson(data) : null) as T;
}
@ -2737,12 +2709,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
.toList()
as T;
}
if (t == _isc.getType<List<String>?>()) {
return (data != null
? (data as List).map((e) => deserialize<String>(e)).toList()
: null)
as T;
}
if (t == List<_i7kd0wkp.ReleaseInfo>) {
return (data as List)
.map((e) => deserialize<_i7kd0wkp.ReleaseInfo>(e))
@ -2877,9 +2843,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
_i6j5dng4.NoteRevision => 'NoteRevision',
_i4l20c7i.PdfReading => 'PdfReading',
_iz838ro9.PdfSource => 'PdfSource',
_ia5j8fch.Profile => 'Profile',
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
_idvc5e7p.ProfileRefusedException => 'ProfileRefusedException',
_i9o1kyhh.Publication => 'Publication',
_iofgvwaa.PublishRefusal => 'PublishRefusal',
_itdn8966.PublishRefusedException => 'PublishRefusedException',
@ -2996,12 +2959,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'PdfReading';
case _iz838ro9.PdfSource():
return 'PdfSource';
case _ia5j8fch.Profile():
return 'Profile';
case _i6j6efd1.ProfileRefusal():
return 'ProfileRefusal';
case _idvc5e7p.ProfileRefusedException():
return 'ProfileRefusedException';
case _i9o1kyhh.Publication():
return 'Publication';
case _iofgvwaa.PublishRefusal():
@ -3188,15 +3145,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'PdfSource') {
return deserialize<_iz838ro9.PdfSource>(data['data']);
}
if (dataClassName == 'Profile') {
return deserialize<_ia5j8fch.Profile>(data['data']);
}
if (dataClassName == 'ProfileRefusal') {
return deserialize<_i6j6efd1.ProfileRefusal>(data['data']);
}
if (dataClassName == 'ProfileRefusedException') {
return deserialize<_idvc5e7p.ProfileRefusedException>(data['data']);
}
if (dataClassName == 'Publication') {
return deserialize<_i9o1kyhh.Publication>(data['data']);
}

View file

@ -1,14 +1,20 @@
import 'dart:typed_data';
import 'package:halkyon_base_client/halkyon_base_client.dart' as base;
import 'package:halkyon_learn_client/halkyon_learn_client.dart' as protocol;
import 'package:halkyon_learn_flutter/src/feature/profile/data/profile_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/profile/model/public_profile.dart';
/// [ProfileGateway] asking the instance through the Serverpod client.
/// [ProfileGateway] asking the instance through the Serverpod client: the
/// profile endpoints are halkyon_base's, as the instance's register keeps
/// people's profiles (ADR 0016).
class ServerpodProfileGateway(final protocol.Client _client)
implements ProfileGateway {
/// The caller's own profile's endpoint.
base.EndpointProfile get _profiles => _client.modules.base.profile;
@override
Future<PublicProfile?> mine() async => switch (await _client.profile.mine()) {
Future<PublicProfile?> mine() async => switch (await _profiles.mine()) {
final profile? => _profile(profile),
null => null,
};
@ -21,22 +27,22 @@ class ServerpodProfileGateway(final protocol.Client _client)
required List<String> links,
}) => _refusing(
() async => _profile(
await _client.profile.save(handle, displayName, bio, links),
await _profiles.save(handle, displayName, bio, links),
),
);
@override
Future<PublicProfile?> byHandle(String handle) async {
try {
return _profile(await _client.profiles.byHandle(handle));
} on protocol.NotFoundException {
return _profile(await _client.modules.base.profiles.byHandle(handle));
} on base.ProfileNotFoundException {
return null;
}
}
@override
Future<ProfileProblem?> checkHandle(String handle) async =>
switch (await _client.profile.checkHandle(handle)) {
switch (await _profiles.checkHandle(handle)) {
null => null,
// A reason this app doesn't know reads as an invalid handle.
final reason =>
@ -45,29 +51,29 @@ class ServerpodProfileGateway(final protocol.Client _client)
};
@override
Future<void> remove() => _refusing(_client.profile.remove);
Future<void> remove() => _refusing(_profiles.remove);
@override
Future<PublicProfile> setAvatar(Uint8List png) => _refusing(
() async => _profile(
await _client.profile.setAvatar(ByteData.sublistView(png)),
await _profiles.setAvatar(ByteData.sublistView(png)),
),
);
@override
Future<PublicProfile> removeAvatar() =>
_refusing(() async => _profile(await _client.profile.removeAvatar()));
_refusing(() async => _profile(await _profiles.removeAvatar()));
@override
Future<PublicProfile> checkLinks() =>
_refusing(() async => _profile(await _client.profile.checkLinks()));
_refusing(() async => _profile(await _profiles.checkLinks()));
/// [call], its refusal as a [ProfileRefused]; a reason this app doesn't
/// know reads as an invalid handle.
Future<T> _refusing<T>(Future<T> Function() call) async {
try {
return await call();
} on protocol.ProfileRefusedException catch (error) {
} on base.ProfileRefusedException catch (error) {
throw ProfileRefused(
ProfileProblem.values.asNameMap()[error.reason.name] ??
ProfileProblem.handleInvalid,
@ -75,7 +81,7 @@ class ServerpodProfileGateway(final protocol.Client _client)
}
}
PublicProfile _profile(protocol.Profile profile) => PublicProfile(
PublicProfile _profile(base.Profile profile) => PublicProfile(
handle: profile.handle,
address: profile.address,
displayName: profile.displayName,

View file

@ -71,6 +71,10 @@ dependencies:
# data opened and closed, and syncing it, with texts of its own.
halkyon_app:
path: ../../../packages/halkyon_app
# The client side of halkyon_base: people's public profiles, which the
# instance's register keeps (ADR 0016).
halkyon_base_client:
path: ../../../packages/halkyon_base_client
# Halkyon's low-level parts (a shared package): the queues the repositories
# run their work in (SerialQueue, CoalescingRunner).
halkyon_core:

View file

@ -8,7 +8,6 @@ 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/instance/jellyfin_suggestion.dart';
import 'package:halkyon_learn_server/src/profile/profile_page.dart';
import 'package:serverpod_auth_idp_server/core.dart';
/// The starting point of the Serverpod server.
@ -33,7 +32,8 @@ Future<void> run(List<String> args) async {
// The assistant connector, on the web server (ADR 0006).
addConnectorRoutes(pod);
// Profile pages, `/@handle`, which profile links link back to (ADR 0011).
// 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);

View file

@ -2,7 +2,6 @@ import 'package:halkyon_base_server/base.dart';
import 'package:halkyon_learn_server/src/asset/asset_endpoint.dart';
import 'package:halkyon_learn_server/src/discussion/discussion_delete.dart';
import 'package:halkyon_learn_server/src/generated/protocol.dart';
import 'package:halkyon_learn_server/src/profile/profile_rules.dart';
import 'package:halkyon_learn_server/src/publish/release_repository.dart';
import 'package:serverpod/protocol.dart' show CloudStorageEntry;
import 'package:serverpod/serverpod.dart';
@ -14,10 +13,11 @@ import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart
/// re-upload everything) and before their identities and the person go.
/// Their personal space goes with all its content (the foreign keys cascade
/// into every synced table and the sync engine's bookkeeping) and its stored
/// files; their profile, what they wrote in discussions, their published
/// files; what they wrote in discussions, their published
/// studysets, blocks, what they read and the assistants they connected; and
/// once the deletion holds, their releases' repositories: what learners
/// copied stays theirs (ADR 0005, rule 4).
/// copied stays theirs (ADR 0005, rule 4). Their profile is halkyon_base's,
/// which deletes it after this (ADR 0016).
Future<AfterDeletion?> deleteLearnData(
Session session,
UuidValue authUserId,
@ -50,18 +50,6 @@ Future<AfterDeletion?> deleteLearnData(
transaction: transaction,
noReturn: true,
);
// DELETE FROM "public_profile" WHERE "authUserId" = $1, and its avatar
// unless another profile shows the same picture.
final profiles = await PublicProfile.db.deleteWhere(
session,
where: (t) => t.authUserId.equals(authUserId),
transaction: transaction,
);
for (final profile in profiles) {
if (profile.avatarHash case final hash?) {
await forgetAvatar(session, hash, transaction: transaction);
}
}
// What they wrote in discussions: for good, or kept without them for
// others' replies under it (ADR 0013); what they read.
await deleteDiscussionsOf(session, authUserId, transaction: transaction);

View file

@ -10,16 +10,23 @@ const upstreamSourceCodeUrl =
'https://code.halkyon.app/project-halkyon/halkyon';
/// How Halkyon Learn sets halkyon_base up (ADR 0014), the module giving every
/// Halkyon server its sign-in, the account and its deletion, the pruning and
/// the instance's basics: the instance says it serves `learn` (the app accepts
/// only such an instance; never change it), sign-in mails name the app, the
/// instance names this project's source, works with apps from [minimumAppBuild] on, and deletes
/// Learn's own data of a person who goes ([deleteLearnData]). The server sets
/// it up before it starts (`server.dart`), and so do tests that delete people.
/// Halkyon server its sign-in, the account and its deletion, the pruning, the
/// instance's basics and people's public profiles: the instance says it
/// serves `learn` (the app accepts only such an instance; never change it),
/// sign-in mails name the app, the instance names this project's source,
/// works with apps from [minimumAppBuild] on, deletes Learn's own data of a
/// person who goes ([deleteLearnData]), and profile pages name the app at
/// their foot. The server sets it up before it starts (`server.dart`), and so
/// do tests that delete people or read profiles.
HalkyonBase learnBase() => HalkyonBase(
app: 'learn',
appName: 'Halkyon Learn',
upstreamSourceCodeUrl: upstreamSourceCodeUrl,
minimumAppBuild: minimumAppBuild,
deletePersonData: const [deleteLearnData],
profilePageNote: (
english: 'On Halkyon Learn, a spaced-repetition learning app:',
german: 'Auf Halkyon Learn, einer Lern-App mit verteilter Wiederholung:',
link: Uri.parse('https://halkyon.app/learn'),
),
);

View file

@ -1,6 +1,5 @@
import 'package:halkyon_base_server/base.dart';
import 'package:halkyon_learn_server/src/generated/protocol.dart';
import 'package:halkyon_learn_server/src/profile/profile_rules.dart';
import 'package:serverpod/serverpod.dart';
// What a person sees of a published studyset's discussions (ADR 0013), and
@ -69,13 +68,8 @@ final class DiscussionReading {
for (final posts in reading._inQuestion.values)
for (final post in posts) ?post.authUserId,
};
// SELECT … FROM "public_profile" WHERE "authUserId" = ANY($1)
for (final profile in await PublicProfile.db.find(
session,
where: (t) => t.authUserId.inSet(people),
)) {
reading._profiles[profile.authUserId] = profile;
}
// Their public profiles, which the instance's register keeps (ADR 0016).
reading._profiles.addAll(await profilesOf(session, people));
// SELECT … FROM "feedback_block" WHERE "authorId" = ANY($1)
for (final block in await FeedbackBlock.db.find(
session,
@ -108,10 +102,9 @@ final class DiscussionReading {
final _inQuestion = <UuidValue, List<DiscussionPost>>{};
final _published = <int, PublishedStudyset>{};
final _titles = <int, String>{};
final _profiles = <UuidValue, PublicProfile>{};
final _profiles = <UuidValue, Profile>{};
final _blocked = <(UuidValue, UuidValue)>{};
final _seen = <UuidValue, DateTime>{};
final _avatars = <String, String>{};
/// The published studyset [post] is on.
PublishedStudyset publishedOf(DiscussionPost post) =>
@ -278,14 +271,7 @@ final class DiscussionReading {
return FeedbackPerson(
handle: profile.handle,
displayName: profile.displayName,
avatarUrl: switch (profile.avatarHash) {
final hash? =>
_avatars[hash] ??= (await _session.storage.publicDownloadUrl(
storageId: avatarStorage,
path: avatarPath(hash),
)).toString(),
null => null,
},
avatarUrl: profile.avatarUrl,
);
}
}

View file

@ -3,9 +3,9 @@ import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:halkyon_base_server/base.dart';
import 'package:halkyon_learn_server/src/explore/learner_count.dart';
import 'package:halkyon_learn_server/src/generated/protocol.dart';
import 'package:halkyon_learn_server/src/profile/profile_rules.dart';
import 'package:halkyon_learn_server/src/publish/release_repository.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
@ -254,7 +254,10 @@ class ExploreEndpoint extends Endpoint {
/// The latest release of each published studyset not withdrawn, with its
/// author, matching every one of [where] (SQL on `latest`), the newest
/// first; a page at a time when [paged].
/// first; a page at a time when [paged]. The authors' profiles are joined
/// from the profiles' table, which is halkyon_base's: Halkyon Learn holds
/// the instance's register wherever it runs (ADR 0016), so its database has
/// every profile, and the search over authors stays one query.
Future<List<ExploreEntry>> _latest(
Session session, {
required List<String> where,

View file

@ -141,15 +141,12 @@ class InboxEndpoint extends Endpoint {
))
block.blockedId,
};
// SELECT … FROM "public_profile" WHERE "authUserId" = ANY($1)
// Their handles, from the instance's register (ADR 0016), in the order
// of their lower case.
return [
for (final profile in await PublicProfile.db.find(
session,
where: (t) => t.authUserId.inSet(blocked),
orderBy: (t) => t.handleKey,
))
for (final profile in (await profilesOf(session, blocked)).values)
profile.handle,
];
]..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
}
/// The file [sha256] that the suggestion [threadId] names, for the author
@ -199,17 +196,9 @@ class InboxEndpoint extends Endpoint {
}
/// The person with [handle]; throws [NotFoundException] for none.
Future<UuidValue> _personOf(Session session, String handle) async {
// SELECT "authUserId" FROM "public_profile" WHERE "handleKey" = $1
final profile = await PublicProfile.db.findFirstRow(
session,
where: (t) => t.handleKey.equals(handle.trim().toLowerCase()),
);
if (profile == null) {
throw NotFoundException(what: 'person');
}
return profile.authUserId;
}
Future<UuidValue> _personOf(Session session, String handle) async =>
await personWithHandle(session, handle.trim()) ??
(throw NotFoundException(what: 'person'));
/// Whether the thread's [payload] names the file [sha256] among those of
/// its proposed content (`files`).

View file

@ -3,7 +3,6 @@ import 'dart:typed_data';
import 'package:halkyon_base_server/base.dart';
import 'package:halkyon_learn_server/src/generated/protocol.dart';
import 'package:halkyon_learn_server/src/profile/profile_rules.dart';
import 'package:halkyon_learn_server/src/publish/release_license.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
@ -92,14 +91,8 @@ Future<List<FeedbackView>> gatherFeedback(
...personOf.values,
for (final studyset in published.values) studyset.authUserId,
};
// SELECT … FROM "public_profile" WHERE "authUserId" = ANY($1)
final profiles = {
for (final profile in await PublicProfile.db.find(
session,
where: (t) => t.authUserId.inSet(people),
))
profile.authUserId: profile,
};
// Their public profiles, which the instance's register keeps (ADR 0016).
final profiles = await profilesOf(session, people);
// SELECT … FROM "serverpod_auth_core_user" WHERE "id" = ANY($1)
final identifiedPeople = {
for (final person in await AuthUser.db.find(
@ -129,20 +122,11 @@ Future<List<FeedbackView>> gatherFeedback(
};
final domain = accountDomain(session.serverpod);
final avatars = <String, String?>{};
Future<FeedbackPerson> personFor(PublicProfile profile) async =>
FeedbackPerson(
handle: profile.handle,
displayName: profile.displayName,
avatarUrl: switch (profile.avatarHash) {
final hash? =>
avatars[hash] ??= (await session.storage.publicDownloadUrl(
storageId: avatarStorage,
path: avatarPath(hash),
)).toString(),
null => null,
},
);
FeedbackPerson personFor(Profile profile) => FeedbackPerson(
handle: profile.handle,
displayName: profile.displayName,
avatarUrl: profile.avatarUrl,
);
final views = <(DateTime, FeedbackView)>[];
for (final thread in threads) {
@ -213,7 +197,7 @@ Future<List<FeedbackView>> gatherFeedback(
replies.add(
FeedbackMessageView(
id: message.id!,
by: await personFor(by == author ? authorProfile : writerProfile),
by: personFor(by == author ? authorProfile : writerProfile),
fromAuthor: by == author,
payload: text,
createdAt: _timeOf(message.id!),
@ -243,7 +227,7 @@ Future<List<FeedbackView>> gatherFeedback(
studysetTitle: release.title,
location: '$domain/@${authorProfile.handle}/${studyset.slug}',
kind: thread.kind,
writer: await personFor(writerProfile),
writer: personFor(writerProfile),
payload: payload,
createdAt: createdAt,
messages: replies,

View file

@ -10,7 +10,6 @@
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_server/halkyon_base_server.dart' as _ik8molhd;
import 'package:halkyon_learn_server/src/generated/discussion/discussion_target.dart'
as _ike2gc23;
@ -33,7 +32,6 @@ import '../discussion/discussion_endpoint.dart' as _ienplr78;
import '../explore/explore_endpoint.dart' as _i897jtwc;
import '../feedback/feedback_endpoint.dart' as _i3n4p88h;
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
import '../profile/profile_endpoint.dart' as _i6ky944g;
import '../publish/publish_endpoint.dart' as _iyiav298;
export 'future_calls.dart' show ServerpodFutureCallsGetter;
@ -101,18 +99,6 @@ class Endpoints extends _is.EndpointDispatch {
'instance',
null,
),
'profile': _i6ky944g.ProfileEndpoint()
..initialize(
server,
'profile',
null,
),
'profiles': _i6ky944g.ProfilesEndpoint()
..initialize(
server,
'profiles',
null,
),
'publish': _iyiav298.PublishEndpoint()
..initialize(
server,
@ -1154,152 +1140,6 @@ class Endpoints extends _is.EndpointDispatch {
),
},
);
connectors['profile'] = _is.EndpointConnector(
name: 'profile',
endpoint: endpoints['profile']!,
methodConnectors: {
'mine': _is.MethodConnector(
name: 'mine',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['profile'] as _i6ky944g.ProfileEndpoint)
.mine(session),
),
'save': _is.MethodConnector(
name: 'save',
params: {
'handle': _is.ParameterDescription(
name: 'handle',
type: _is.getType<String>(),
nullable: false,
),
'displayName': _is.ParameterDescription(
name: 'displayName',
type: _is.getType<String>(),
nullable: false,
),
'bio': _is.ParameterDescription(
name: 'bio',
type: _is.getType<String>(),
nullable: false,
),
'links': _is.ParameterDescription(
name: 'links',
type: _is.getType<List<String>>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['profile'] as _i6ky944g.ProfileEndpoint).save(
session,
params['handle'],
params['displayName'],
params['bio'],
params['links'],
),
),
'checkLinks': _is.MethodConnector(
name: 'checkLinks',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['profile'] as _i6ky944g.ProfileEndpoint)
.checkLinks(session),
),
'checkHandle': _is.MethodConnector(
name: 'checkHandle',
params: {
'handle': _is.ParameterDescription(
name: 'handle',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['profile'] as _i6ky944g.ProfileEndpoint)
.checkHandle(
session,
params['handle'],
),
),
'setAvatar': _is.MethodConnector(
name: 'setAvatar',
params: {
'png': _is.ParameterDescription(
name: 'png',
type: _is.getType<_idt.ByteData>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async =>
(endpoints['profile'] as _i6ky944g.ProfileEndpoint).setAvatar(
session,
params['png'],
),
),
'removeAvatar': _is.MethodConnector(
name: 'removeAvatar',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['profile'] as _i6ky944g.ProfileEndpoint)
.removeAvatar(session),
),
'remove': _is.MethodConnector(
name: 'remove',
params: {},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['profile'] as _i6ky944g.ProfileEndpoint)
.remove(session),
),
},
);
connectors['profiles'] = _is.EndpointConnector(
name: 'profiles',
endpoint: endpoints['profiles']!,
methodConnectors: {
'byHandle': _is.MethodConnector(
name: 'byHandle',
params: {
'handle': _is.ParameterDescription(
name: 'handle',
type: _is.getType<String>(),
nullable: false,
),
},
call:
(
_is.Session session,
Map<String, dynamic> params,
) async => (endpoints['profiles'] as _i6ky944g.ProfilesEndpoint)
.byHandle(
session,
params['handle'],
),
),
},
);
connectors['publish'] = _is.EndpointConnector(
name: 'publish',
endpoint: endpoints['publish']!,

View file

@ -91,10 +91,6 @@ import 'note/note.dart' as _i9njjhyv;
import 'note/note_revision.dart' as _i6j5dng4;
import 'pdf_source/pdf_reading.dart' as _i4l20c7i;
import 'pdf_source/pdf_source.dart' as _iz838ro9;
import 'profile/profile.dart' as _ia5j8fch;
import 'profile/profile_refusal.dart' as _i6j6efd1;
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
import 'profile/public_profile.dart' as _ioppe2ep;
import 'publish/publication.dart' as _i9o1kyhh;
import 'publish/publish_refusal.dart' as _iofgvwaa;
import 'publish/publish_refused_exception.dart' as _itdn8966;
@ -161,10 +157,6 @@ export 'note/note.dart';
export 'note/note_revision.dart';
export 'pdf_source/pdf_reading.dart';
export 'pdf_source/pdf_source.dart';
export 'profile/profile.dart';
export 'profile/profile_refusal.dart';
export 'profile/profile_refused_exception.dart';
export 'profile/public_profile.dart';
export 'publish/publication.dart';
export 'publish/publish_refusal.dart';
export 'publish/publish_refused_exception.dart';
@ -3224,111 +3216,6 @@ class Protocol extends _is.DatabaseSerializationManager {
],
managed: true,
),
_isp.TableDefinition(
name: 'public_profile',
dartName: 'PublicProfile',
schema: 'public',
module: 'halkyon_learn',
columns: [
_isp.ColumnDefinition(
name: 'id',
columnType: _isp.ColumnType.bigint,
isNullable: false,
dartType: 'int?',
columnDefault: 'serial',
),
_isp.ColumnDefinition(
name: 'authUserId',
columnType: _isp.ColumnType.uuid,
isNullable: false,
dartType: 'UuidValue',
),
_isp.ColumnDefinition(
name: 'handle',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'handleKey',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'displayName',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'bio',
columnType: _isp.ColumnType.text,
isNullable: false,
dartType: 'String',
),
_isp.ColumnDefinition(
name: 'links',
columnType: _isp.ColumnType.json,
isNullable: false,
dartType: 'List<String>',
),
_isp.ColumnDefinition(
name: 'verifiedLinks',
columnType: _isp.ColumnType.json,
isNullable: true,
dartType: 'List<String>?',
),
_isp.ColumnDefinition(
name: 'avatarHash',
columnType: _isp.ColumnType.text,
isNullable: true,
dartType: 'String?',
),
_isp.ColumnDefinition(
name: 'firstPublishedAt',
columnType: _isp.ColumnType.timestampWithoutTimeZone,
isNullable: true,
dartType: 'DateTime?',
),
_isp.ColumnDefinition(
name: 'updatedAt',
columnType: _isp.ColumnType.timestampWithoutTimeZone,
isNullable: false,
dartType: 'DateTime',
),
],
foreignKeys: [],
indexes: [
_isp.IndexDefinition(
indexName: 'public_profile_person_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'authUserId',
),
],
type: 'btree',
isUnique: true,
isPrimary: false,
),
_isp.IndexDefinition(
indexName: 'public_profile_handle_idx',
tableSpace: null,
elements: [
_isp.IndexElementDefinition(
type: _isp.IndexElementDefinitionType.column,
definition: 'handleKey',
),
],
type: 'btree',
isUnique: true,
isPrimary: false,
),
],
managed: true,
),
_isp.TableDefinition(
name: 'published_studyset',
dartName: 'PublishedStudyset',
@ -4112,18 +3999,6 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _iz838ro9.PdfSource) {
return _iz838ro9.PdfSource.fromJson(data) as T;
}
if (t == _ia5j8fch.Profile) {
return _ia5j8fch.Profile.fromJson(data) as T;
}
if (t == _i6j6efd1.ProfileRefusal) {
return _i6j6efd1.ProfileRefusal.fromJson(data) as T;
}
if (t == _idvc5e7p.ProfileRefusedException) {
return _idvc5e7p.ProfileRefusedException.fromJson(data) as T;
}
if (t == _ioppe2ep.PublicProfile) {
return _ioppe2ep.PublicProfile.fromJson(data) as T;
}
if (t == _i9o1kyhh.Publication) {
return _i9o1kyhh.Publication.fromJson(data) as T;
}
@ -4376,23 +4251,6 @@ class Protocol extends _is.DatabaseSerializationManager {
if (t == _is.getType<_iz838ro9.PdfSource?>()) {
return (data != null ? _iz838ro9.PdfSource.fromJson(data) : null) as T;
}
if (t == _is.getType<_ia5j8fch.Profile?>()) {
return (data != null ? _ia5j8fch.Profile.fromJson(data) : null) as T;
}
if (t == _is.getType<_i6j6efd1.ProfileRefusal?>()) {
return (data != null ? _i6j6efd1.ProfileRefusal.fromJson(data) : null)
as T;
}
if (t == _is.getType<_idvc5e7p.ProfileRefusedException?>()) {
return (data != null
? _idvc5e7p.ProfileRefusedException.fromJson(data)
: null)
as T;
}
if (t == _is.getType<_ioppe2ep.PublicProfile?>()) {
return (data != null ? _ioppe2ep.PublicProfile.fromJson(data) : null)
as T;
}
if (t == _is.getType<_i9o1kyhh.Publication?>()) {
return (data != null ? _i9o1kyhh.Publication.fromJson(data) : null) as T;
}
@ -4471,12 +4329,6 @@ class Protocol extends _is.DatabaseSerializationManager {
.toList()
as T;
}
if (t == _is.getType<List<String>?>()) {
return (data != null
? (data as List).map((e) => deserialize<String>(e)).toList()
: null)
as T;
}
if (t == List<_i7kd0wkp.ReleaseInfo>) {
return (data as List)
.map((e) => deserialize<_i7kd0wkp.ReleaseInfo>(e))
@ -4623,10 +4475,6 @@ class Protocol extends _is.DatabaseSerializationManager {
_i6j5dng4.NoteRevision => 'NoteRevision',
_i4l20c7i.PdfReading => 'PdfReading',
_iz838ro9.PdfSource => 'PdfSource',
_ia5j8fch.Profile => 'Profile',
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
_idvc5e7p.ProfileRefusedException => 'ProfileRefusedException',
_ioppe2ep.PublicProfile => 'PublicProfile',
_i9o1kyhh.Publication => 'Publication',
_iofgvwaa.PublishRefusal => 'PublishRefusal',
_itdn8966.PublishRefusedException => 'PublishRefusedException',
@ -4763,14 +4611,6 @@ class Protocol extends _is.DatabaseSerializationManager {
return 'PdfReading';
case _iz838ro9.PdfSource():
return 'PdfSource';
case _ia5j8fch.Profile():
return 'Profile';
case _i6j6efd1.ProfileRefusal():
return 'ProfileRefusal';
case _idvc5e7p.ProfileRefusedException():
return 'ProfileRefusedException';
case _ioppe2ep.PublicProfile():
return 'PublicProfile';
case _i9o1kyhh.Publication():
return 'Publication';
case _iofgvwaa.PublishRefusal():
@ -4992,18 +4832,6 @@ class Protocol extends _is.DatabaseSerializationManager {
if (dataClassName == 'PdfSource') {
return deserialize<_iz838ro9.PdfSource>(data['data']);
}
if (dataClassName == 'Profile') {
return deserialize<_ia5j8fch.Profile>(data['data']);
}
if (dataClassName == 'ProfileRefusal') {
return deserialize<_i6j6efd1.ProfileRefusal>(data['data']);
}
if (dataClassName == 'ProfileRefusedException') {
return deserialize<_idvc5e7p.ProfileRefusedException>(data['data']);
}
if (dataClassName == 'PublicProfile') {
return deserialize<_ioppe2ep.PublicProfile>(data['data']);
}
if (dataClassName == 'Publication') {
return deserialize<_i9o1kyhh.Publication>(data['data']);
}
@ -5162,8 +4990,6 @@ class Protocol extends _is.DatabaseSerializationManager {
return _i4l20c7i.PdfReading.t;
case _iz838ro9.PdfSource:
return _iz838ro9.PdfSource.t;
case _ioppe2ep.PublicProfile:
return _ioppe2ep.PublicProfile.t;
case _irp51t0o.PublishedStudyset:
return _irp51t0o.PublishedStudyset.t;
case _i6qpha3x.StudysetRelease:

View file

@ -52,16 +52,6 @@ inbox:
- file:
instance:
- info:
profile:
- mine:
- save:
- checkLinks:
- checkHandle:
- setAvatar:
- removeAvatar:
- remove:
profiles:
- byHandle:
publish:
- publication:
- published:

Some files were not shown because too many files have changed in this diff Show more