feat(discussions): questions on published studysets, answered by learners

A learner asks about a published studyset, a topic in it, or one card,
note or step of a release, and others answer, as on Stack Exchange
(ADR 0013): questions and answers in the app's editor without images,
sounds or videos, comments of up to 600 characters under the question
and each answer. There are no votes: the asker accepts an answer, which
comes first, then the oldest; the author's posts are marked. The
instance reads from git's file list which topics a question's target
sits in, so a topic lists what is asked within it and below it.

Posts live in two server-only tables, written through endpoints, so the
synced tables stay as they are and v0.17.0 keeps syncing. Everyone
signed in reads; writing needs an identity and a public profile; the
studyset's author hides posts and blocks people with feedback's blocks;
a withdrawn studyset takes no new questions; anyone writes at most 30
posts an hour. A post with others' replies under it is emptied when
deleted, else deleted; deletePerson deletes what a person wrote the same
way, and a published studyset's deletion takes its discussions with it.

The migration adds Serverpod's gen_random_uuid_v7(), discussion_post
and discussion_seen with their indexes and cascading foreign keys, and
changes nothing else.

In the app, discussions open from a copy's studyset or topic menu, a
studied card's "Ask about this", Explore's summary and the Publish
sheet; the Profile tab's inbox lists those the person follows, and its
count includes what is new in them.

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-15 23:52:40 +02:00
commit 5efa552489
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 28214 additions and 27 deletions

View file

@ -0,0 +1,831 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:halkyon_learn_server/src/auth/account.dart';
import 'package:halkyon_learn_server/src/auth/scopes.dart';
import 'package:halkyon_learn_server/src/discussion/discussion_rules.dart';
import 'package:halkyon_learn_server/src/generated/protocol.dart';
import 'package:halkyon_learn_server/src/publish/release_repository.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
import 'package:test/test.dart';
import '../support/auth_fixtures.dart';
import '../support/sync_database.dart';
import '../test_tools/serverpod_test_tools.dart';
/// Rich text as the app's editor writes it, holding [text].
String rich(String text) => jsonEncode({
'formatVersion': 1,
'document': {
'type': 'page',
'children': [
{
'type': 'paragraph',
'data': {
'delta': [
{'insert': text},
],
},
},
],
},
});
void main() {
withServerpod(
'Discussions',
(sessionBuilder, endpoints) {
late Session session;
late UuidValue alice;
late UuidValue bob;
late UuidValue carol;
final people = <UuidValue>[];
/// A person signed in with an identity, or none.
TestSessionBuilder signedIn(UuidValue person, {bool identity = true}) =>
sessionBuilder.copyWith(
authentication: AuthenticationOverride.authenticationInfo(
person.uuid,
identity ? {identified} : {},
),
);
/// A person, with an identity unless [anonymous], and a public profile
/// under [handle] unless null.
Future<UuidValue> person(String? handle, {bool anonymous = false}) async {
final id = (await AuthServices.instance.authUsers.create(
session,
scopes: anonymous ? const {} : {identified},
)).id;
people.add(id);
if (handle != null) {
await endpoints.profile.save(signedIn(id), handle, '', '', []);
}
return id;
}
setUp(() async {
Serverpod.instance.initializeOfflineSync(syncTables: syncTables);
useServerAuth();
session = sessionBuilder.build();
repositoriesFolder = await Directory.systemTemp.createTemp('repos-');
alice = await person('alice');
bob = await person('bob');
carol = await person('carol');
});
tearDown(() async {
for (final person in people) {
await deletePerson(session, person);
}
people.clear();
await repositoriesFolder.delete(recursive: true);
repositoriesFolder = null;
});
ByteData json(Map<String, Object?> json) =>
ByteData.sublistView(utf8.encode(jsonEncode(json)));
late UuidValue root;
late UuidValue grammar;
late UuidValue particles;
late UuidValue kanji;
late UuidValue card;
/// Alice's Japanese, published: Grammar with Particles inside, holding
/// a card, and Kanji beside it. Returns its public id.
Future<UuidValue> japanese() async {
await asPerson(session, alice, (transaction) async {
Future<UuidValue> studyset(String title, {UuidValue? parent}) async =>
(await Studyset.db.insertRow(
session,
Studyset(
parentId: parent,
position: 'a0',
payload: json({
'schemaVersion': 5,
'title': title,
'license': 'CC-BY-SA-4.0',
}),
),
transaction: transaction,
)).id!;
root = await studyset('Japanese');
grammar = await studyset('Grammar', parent: root);
particles = await studyset('Particles', parent: grammar);
kanji = await studyset('Kanji', parent: root);
card = (await Card.db.insertRow(
session,
Card(studysetId: particles, position: 'a0'),
transaction: transaction,
)).id!;
await CardRevision.db.insertRow(
session,
CardRevision(
cardId: card,
parents: [],
authorId: alice,
payload: json({'schemaVersion': 9, 'type': 'frontBack'}),
),
transaction: transaction,
);
});
return (await endpoints.publish.publish(
signedIn(alice),
root,
'japanese',
'',
)).publicId;
}
DiscussionTarget about(String kind, UuidValue item, {int version = 1}) =>
DiscussionTarget(kind: kind, item: item.uuid, version: version);
Future<UuidValue> ask(
UuidValue by,
UuidValue publication, {
DiscussionTarget? target,
String title = 'Meaning of は here?',
String body = 'Why は and not が?',
}) => endpoints.discussion.ask(
signedIn(by),
publication,
target ?? about('card', card),
title,
rich(body),
);
Future<DiscussionView> read(UuidValue by, UuidValue question) =>
endpoints.discussions.read(signedIn(by), question);
Matcher refused(DiscussionRefusal reason) => throwsA(
isA<DiscussionRefusedException>().having(
(error) => error.reason,
'reason',
reason,
),
);
group('asking', () {
test('a person with an identity and a public profile asks about an '
'item of the release; everyone signed in reads it, its writer '
'named', () async {
final publication = await japanese();
final id = await ask(bob, publication);
final anonymous = await person(null, anonymous: true);
final [summary] = await endpoints.discussions.list(
signedIn(anonymous, identity: false),
publication,
);
expect(summary.questionId, id);
expect(summary.title, 'Meaning of は here?');
expect(summary.asker?.handle, 'bob');
expect(summary.askerIsAuthor, isFalse);
expect(summary.target.toJson(), about('card', card).toJson());
expect(summary.studysetTitle, 'Japanese');
expect(summary.answers, 0);
final view = await read(carol, id);
expect(view.question.title, 'Meaning of は here?');
expect(view.question.body, rich('Why は and not が?'));
expect(view.question.mine, isFalse);
expect(view.location, endsWith('/@alice/japanese'));
expect(view.asker, isFalse);
expect(view.author, isFalse);
expect(view.refusal, isNull);
expect((await read(bob, id)).asker, isTrue);
expect((await read(alice, id)).author, isTrue);
});
test('a topic lists what is asked within it and its topics, and '
'nothing else; the published studyset lists everything', () async {
final publication = await japanese();
final onCard = await ask(bob, publication);
final onGrammar = await ask(
bob,
publication,
target: about('studyset', grammar),
);
final onKanji = await ask(
carol,
publication,
target: about('studyset', kanji),
);
final onRoot = await ask(
carol,
publication,
target: about('studyset', root),
);
Future<Set<UuidValue>> within(UuidValue? topic) async => {
for (final summary in await endpoints.discussions.list(
signedIn(carol),
publication,
topic: topic?.uuid,
))
summary.questionId,
};
expect(await within(grammar), {onCard, onGrammar});
expect(await within(particles), {onCard});
expect(await within(kanji), {onKanji});
expect(await within(root), {onCard, onGrammar, onKanji, onRoot});
expect(await within(null), {onCard, onGrammar, onKanji, onRoot});
expect(
[
for (final summary in await endpoints.discussions.list(
signedIn(carol),
publication,
item: about('card', card),
))
summary.questionId,
],
[onCard],
);
});
test(
'a question about the studyset as a whole may name no item',
() async {
final publication = await japanese();
final id = await ask(
bob,
publication,
target: DiscussionTarget(kind: 'studyset', item: '', version: 1),
);
expect((await read(carol, id)).target.item, root.uuid);
},
);
test('finds questions by what their titles say, wildcards as '
'themselves', () async {
final publication = await japanese();
final wa = await ask(bob, publication, title: 'Meaning of は');
await ask(bob, publication, title: 'Reading of 100%');
Future<List<UuidValue>> search(String query) async => [
for (final summary in await endpoints.discussions.list(
signedIn(carol),
publication,
query: query,
))
summary.questionId,
];
expect(await search('meaning'), [wa]);
expect(await search('%'), hasLength(1));
expect(await search('_'), isEmpty);
});
test('needs an identity to write, and signing in to read', () async {
final publication = await japanese();
await expectLater(
endpoints.discussion.ask(
signedIn(bob, identity: false),
publication,
about('card', card),
'Why?',
rich('Why?'),
),
throwsA(isA<ServerpodInsufficientAccessException>()),
);
await expectLater(
endpoints.discussions.list(
sessionBuilder.copyWith(
authentication: AuthenticationOverride.unauthenticated(),
),
publication,
),
throwsA(isA<ServerpodUnauthenticatedException>()),
);
});
test('refuses a person without a public profile, or one the author '
'blocked, and says so to them when they read', () async {
final publication = await japanese();
final dave = await person(null);
final question = await ask(bob, publication);
await expectLater(
ask(dave, publication),
refused(DiscussionRefusal.noProfile),
);
expect(
(await read(dave, question)).refusal,
DiscussionRefusal.noProfile,
);
await endpoints.inbox.block(signedIn(alice), 'carol');
await expectLater(
ask(carol, publication),
refused(DiscussionRefusal.blocked),
);
await expectLater(
endpoints.discussion.answer(signedIn(carol), question, rich('No')),
refused(DiscussionRefusal.blocked),
);
expect(
(await read(carol, question)).refusal,
DiscussionRefusal.blocked,
);
final anonymous = await person(null, anonymous: true);
expect(
(await endpoints.discussions.read(
signedIn(anonymous, identity: false),
question,
)).refusal,
DiscussionRefusal.noIdentity,
);
});
test('refuses a question about what the release lacks, or in a '
'release that does not exist; a studyset never published is '
'missing', () async {
final publication = await japanese();
await expectLater(
ask(bob, publication, target: about('card', grammar)),
refused(DiscussionRefusal.notInRelease),
);
await expectLater(
ask(bob, publication, target: about('card', card, version: 2)),
refused(DiscussionRefusal.notInRelease),
);
await expectLater(
ask(bob, publication, target: about('chapter', card)),
refused(DiscussionRefusal.notInRelease),
);
await expectLater(
ask(bob, const Uuid().v4obj()),
throwsA(isA<NotFoundException>()),
);
await expectLater(
endpoints.discussions.list(signedIn(bob), const Uuid().v4obj()),
throwsA(isA<NotFoundException>()),
);
});
test('a studyset withdrawn keeps its discussions readable, but takes '
'no new questions; answers go on', () async {
final publication = await japanese();
final question = await ask(bob, publication);
await endpoints.publish.withdraw(signedIn(alice), root);
await expectLater(
ask(bob, publication),
refused(DiscussionRefusal.withdrawn),
);
expect(
await endpoints.discussions.list(signedIn(carol), publication),
hasLength(1),
);
await endpoints.discussion.answer(
signedIn(carol),
question,
rich('Topic marker.'),
);
expect((await read(bob, question)).answers, hasLength(1));
});
test('refuses an empty or too long title, text or comment, and more '
'posts in an hour than allowed', () async {
final publication = await japanese();
await expectLater(
ask(bob, publication, title: ' '),
refused(DiscussionRefusal.empty),
);
await expectLater(
ask(bob, publication, title: 'x' * (maxTitleLength + 1)),
refused(DiscussionRefusal.tooLong),
);
await expectLater(
endpoints.discussion.ask(
signedIn(bob),
publication,
about('card', card),
'Why?',
'',
),
refused(DiscussionRefusal.empty),
);
await expectLater(
endpoints.discussion.ask(
signedIn(bob),
publication,
about('card', card),
'Why?',
'not rich text',
),
refused(DiscussionRefusal.empty),
);
await expectLater(
ask(bob, publication, body: 'x' * maxBodyBytes),
refused(DiscussionRefusal.tooLong),
);
final question = await ask(bob, publication);
await expectLater(
endpoints.discussion.comment(
signedIn(carol),
question,
'x' * (maxCommentLength + 1),
),
refused(DiscussionRefusal.tooLong),
);
for (var post = 1; post < maxPostsAnHour; post++) {
await endpoints.discussion.comment(
signedIn(bob),
question,
'$post',
);
}
await expectLater(
endpoints.discussion.comment(signedIn(bob), question, 'more'),
refused(DiscussionRefusal.tooMany),
);
});
});
group('answering', () {
test('anyone with a profile answers and comments; answers come oldest '
'first, the accepted one first, comments beneath what they are '
'on', () async {
final publication = await japanese();
final question = await ask(bob, publication);
final first = await endpoints.discussion.answer(
signedIn(carol),
question,
rich('Topic marker.'),
);
final second = await endpoints.discussion.answer(
signedIn(alice),
question,
rich('は marks the topic; が the subject.'),
);
final onQuestion = await endpoints.discussion.comment(
signedIn(carol),
question,
'Good question.',
);
final onAnswer = await endpoints.discussion.comment(
signedIn(bob),
second,
'Thanks!',
);
var view = await read(carol, question);
expect(
[for (final answer in view.answers) answer.id],
[
first,
second,
],
);
expect(view.answers.last.fromAuthor, isTrue);
expect(view.answers.first.mine, isTrue);
expect(
[
for (final comment in view.comments)
(comment.id, comment.parentId),
],
[(onQuestion, question), (onAnswer, second)],
);
expect(view.comments.first.body, 'Good question.');
await endpoints.discussion.accept(signedIn(bob), question, second);
view = await read(carol, question);
expect(view.acceptedAnswerId, second);
expect(
[for (final answer in view.answers) answer.id],
[
second,
first,
],
);
final [summary] = await endpoints.discussions.list(
signedIn(carol),
publication,
);
expect(summary.answers, 2);
expect(summary.accepted, isTrue);
await endpoints.discussion.accept(signedIn(bob), question, null);
expect((await read(carol, question)).acceptedAnswerId, isNull);
});
test('only the asker accepts, only an answer of the question; '
'anything else is as missing', () async {
final publication = await japanese();
final question = await ask(bob, publication);
final answer = await endpoints.discussion.answer(
signedIn(carol),
question,
rich('Topic marker.'),
);
final other = await ask(carol, publication);
final elsewhere = await endpoints.discussion.answer(
signedIn(bob),
other,
rich('Elsewhere.'),
);
for (final (by, on, accepted) in [
(alice, question, answer),
(carol, question, answer),
(bob, question, elsewhere),
(bob, question, question),
(bob, answer, answer),
]) {
await expectLater(
endpoints.discussion.accept(signedIn(by), on, accepted),
throwsA(isA<NotFoundException>()),
);
}
expect((await read(bob, question)).acceptedAnswerId, isNull);
});
test('a comment goes on a question or an answer, an answer on a '
'question; anything else is as missing', () async {
final publication = await japanese();
final question = await ask(bob, publication);
final comment = await endpoints.discussion.comment(
signedIn(carol),
question,
'Hm.',
);
final answer = await endpoints.discussion.answer(
signedIn(carol),
question,
rich('Topic marker.'),
);
await expectLater(
endpoints.discussion.comment(signedIn(bob), comment, 'Re: hm.'),
throwsA(isA<NotFoundException>()),
);
for (final on in [answer, comment, const Uuid().v7obj()]) {
await expectLater(
endpoints.discussion.answer(signedIn(bob), on, rich('No.')),
throwsA(isA<NotFoundException>()),
);
}
});
});
group('editing and deleting', () {
test('people edit what they wrote, marked edited; what others wrote '
'is as missing', () async {
final publication = await japanese();
final question = await ask(bob, publication);
final comment = await endpoints.discussion.comment(
signedIn(carol),
question,
'Hm.',
);
await endpoints.discussion.edit(
signedIn(bob),
question,
rich('Why は here?'),
title: 'は or が?',
);
await endpoints.discussion.edit(signedIn(carol), comment, 'Hmm.');
final view = await read(alice, question);
expect(view.question.title, 'は or が?');
expect(view.question.body, rich('Why は here?'));
expect(view.question.editedAt, isNotNull);
expect(view.comments.single.body, 'Hmm.');
for (final (by, post) in [(carol, question), (bob, comment)]) {
await expectLater(
endpoints.discussion.edit(signedIn(by), post, rich('Mine now')),
throwsA(isA<NotFoundException>()),
);
}
await expectLater(
endpoints.discussion.delete(signedIn(carol), question),
throwsA(isA<NotFoundException>()),
);
});
test('deleting a post nobody else replied to removes it; with '
"others' replies it stays, its title, text and writer gone, the "
'replies kept', () async {
final publication = await japanese();
final lonely = await ask(bob, publication, title: 'Alone');
final answered = await ask(bob, publication, title: 'Answered');
await endpoints.discussion.comment(signedIn(bob), lonely, 'Bump.');
final answer = await endpoints.discussion.answer(
signedIn(carol),
answered,
rich('Topic marker.'),
);
await endpoints.discussion.delete(signedIn(bob), lonely);
await endpoints.discussion.delete(signedIn(bob), answered);
final [summary] = await endpoints.discussions.list(
signedIn(carol),
publication,
);
expect(summary.questionId, answered);
expect(summary.deleted, isTrue);
expect(summary.title, isEmpty);
expect(summary.asker, isNull);
final view = await read(carol, answered);
expect(view.question.body, isEmpty);
expect(view.question.by, isNull);
expect(view.answers.single.id, answer);
await expectLater(
read(carol, lonely),
throwsA(isA<NotFoundException>()),
);
// A deleted question takes no new answers.
await expectLater(
endpoints.discussion.answer(signedIn(carol), answered, rich('?')),
throwsA(isA<NotFoundException>()),
);
});
test("deleting a person keeps others' answers under their questions, "
'and removes the rest of what they wrote', () async {
final publication = await japanese();
final dave = await person('dave');
final answered = await ask(dave, publication, title: 'Answered');
final lonely = await ask(dave, publication, title: 'Alone');
final answer = await endpoints.discussion.answer(
signedIn(carol),
answered,
rich('Topic marker.'),
);
await endpoints.discussion.answer(
signedIn(dave),
answered,
rich('Thanks, I see.'),
);
await endpoints.discussion.markSeen(signedIn(dave), answered);
await deletePerson(session, dave);
people.remove(dave);
final view = await read(carol, answered);
expect(view.question.deleted, isTrue);
expect(view.question.title, isEmpty);
expect([for (final answer in view.answers) answer.id], [answer]);
await expectLater(
read(carol, lonely),
throwsA(isA<NotFoundException>()),
);
expect(
await DiscussionPost.db.count(
session,
where: (t) => t.authUserId.equals(dave),
),
0,
);
expect(
await DiscussionSeen.db.count(
session,
where: (t) => t.authUserId.equals(dave),
),
0,
);
});
test(
"deleting the author's studyset takes its discussions with it",
() async {
final publication = await japanese();
await ask(bob, publication);
await deletePerson(session, alice);
people.remove(alice);
expect(await DiscussionPost.db.count(session), 0);
},
);
});
group("the author's say", () {
test('the author hides posts on their studysets: then only their '
'writer and the author see them; nobody else hides', () async {
final publication = await japanese();
final question = await ask(bob, publication);
final answer = await endpoints.discussion.answer(
signedIn(carol),
question,
rich('Spam.'),
);
await expectLater(
endpoints.discussion.hide(signedIn(bob), answer, true),
throwsA(isA<NotFoundException>()),
);
await endpoints.discussion.hide(signedIn(alice), answer, true);
expect((await read(bob, question)).answers, isEmpty);
expect((await read(carol, question)).answers.single.hidden, isTrue);
expect((await read(alice, question)).answers.single.hidden, isTrue);
await endpoints.discussion.hide(signedIn(alice), question, true);
await expectLater(
read(carol, question),
throwsA(isA<NotFoundException>()),
);
expect(
await endpoints.discussions.list(signedIn(carol), publication),
isEmpty,
);
expect((await read(bob, question)).question.hidden, isTrue);
await endpoints.discussion.hide(signedIn(alice), question, false);
await endpoints.discussion.hide(signedIn(alice), answer, false);
expect((await read(carol, question)).answers, hasLength(1));
});
test(
'what a person the author blocked wrote shows to them alone',
() async {
final publication = await japanese();
final question = await ask(bob, publication);
await endpoints.discussion.answer(
signedIn(carol),
question,
rich('Spam.'),
);
final theirs = await ask(carol, publication);
await endpoints.inbox.block(signedIn(alice), 'carol');
expect((await read(bob, question)).answers, isEmpty);
expect((await read(carol, question)).answers, hasLength(1));
await expectLater(
read(bob, theirs),
throwsA(isA<NotFoundException>()),
);
expect(
[
for (final summary in await endpoints.discussions.list(
signedIn(bob),
publication,
))
summary.questionId,
],
[question],
);
},
);
});
group('the inbox', () {
test('holds the questions on the person’s studysets and those they '
'wrote in, new until read, again when others write', () async {
final publication = await japanese();
final question = await ask(bob, publication);
var [inbox] = await endpoints.discussion.inbox(signedIn(alice));
expect(inbox.questionId, question);
expect(inbox.unseen, isTrue);
expect(await endpoints.discussion.unseen(signedIn(alice)), 1);
expect(await endpoints.discussion.inbox(signedIn(carol)), isEmpty);
[inbox] = await endpoints.discussion.inbox(signedIn(bob));
expect(inbox.unseen, isFalse, reason: 'Bob wrote all of it');
await endpoints.discussion.markSeen(signedIn(alice), question);
expect(await endpoints.discussion.unseen(signedIn(alice)), 0);
final answer = await endpoints.discussion.answer(
signedIn(carol),
question,
rich('Topic marker.'),
);
expect(await endpoints.discussion.unseen(signedIn(alice)), 1);
expect(await endpoints.discussion.unseen(signedIn(bob)), 1);
expect(await endpoints.discussion.unseen(signedIn(carol)), 0);
await endpoints.discussion.markSeen(signedIn(carol), question);
await endpoints.discussion.accept(signedIn(bob), question, answer);
expect(
await endpoints.discussion.unseen(signedIn(carol)),
1,
reason: 'her answer was accepted',
);
});
test('needs an identity', () async {
await expectLater(
endpoints.discussion.inbox(signedIn(bob, identity: false)),
throwsA(isA<ServerpodInsufficientAccessException>()),
);
});
});
},
databaseInterceptor: syncDatabaseInterceptor,
rollbackDatabase: RollbackDatabase.disabled,
);
}

View file

@ -34,6 +34,14 @@ import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_key_hit
as _iymncpv9;
import 'package:halkyon_learn_server/src/generated/dictionary/dictionary_source.dart'
as _i4at7f2s;
import 'package:halkyon_learn_server/src/generated/discussion/discussion_refusal.dart'
as _i41gpail;
import 'package:halkyon_learn_server/src/generated/discussion/discussion_summary.dart'
as _iqo3lug2;
import 'package:halkyon_learn_server/src/generated/discussion/discussion_target.dart'
as _ike2gc23;
import 'package:halkyon_learn_server/src/generated/discussion/discussion_view.dart'
as _ikk5e562;
import 'package:halkyon_learn_server/src/generated/explore/explore_entry.dart'
as _iscq690j;
import 'package:halkyon_learn_server/src/generated/explore/explore_query.dart'
@ -211,6 +219,10 @@ class TestEndpoints {
late final _DictionaryEndpoint dictionary;
late final _DiscussionsEndpoint discussions;
late final _DiscussionEndpoint discussion;
late final _ExploreEndpoint explore;
late final _FeedbackEndpoint feedback;
@ -273,6 +285,14 @@ class _InternalTestEndpoints extends TestEndpoints
endpoints,
serializationManager,
);
discussions = _DiscussionsEndpoint(
endpoints,
serializationManager,
);
discussion = _DiscussionEndpoint(
endpoints,
serializationManager,
);
explore = _ExploreEndpoint(
endpoints,
serializationManager,
@ -1256,6 +1276,467 @@ class _DictionaryEndpoint {
}
}
class _DiscussionsEndpoint {
_DiscussionsEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<List<_iqo3lug2.DiscussionSummary>> list(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue publicId, {
String? topic,
_ike2gc23.DiscussionTarget? item,
String? query,
int? page,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussions',
method: 'list',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussions',
methodName: 'list',
parameters: _ist.testObjectToJson({
'publicId': publicId,
'topic': topic,
'item': item,
'query': query,
'page': page,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_iqo3lug2.DiscussionSummary>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_ikk5e562.DiscussionView> read(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue questionId,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussions',
method: 'read',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussions',
methodName: 'read',
parameters: _ist.testObjectToJson({'questionId': questionId}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_ikk5e562.DiscussionView>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_i41gpail.DiscussionRefusal?> askRefusal(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue publicId,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussions',
method: 'askRefusal',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussions',
methodName: 'askRefusal',
parameters: _ist.testObjectToJson({'publicId': publicId}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_i41gpail.DiscussionRefusal?>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _DiscussionEndpoint {
_DiscussionEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<_is.UuidValue> ask(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue publicId,
_ike2gc23.DiscussionTarget target,
String title,
String body,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'ask',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'ask',
parameters: _ist.testObjectToJson({
'publicId': publicId,
'target': target,
'title': title,
'body': body,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_is.UuidValue>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_is.UuidValue> answer(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue questionId,
String body,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'answer',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'answer',
parameters: _ist.testObjectToJson({
'questionId': questionId,
'body': body,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_is.UuidValue>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<_is.UuidValue> comment(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue onId,
String text,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'comment',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'comment',
parameters: _ist.testObjectToJson({
'onId': onId,
'text': text,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<_is.UuidValue>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> edit(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue postId,
String body, {
String? title,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'edit',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'edit',
parameters: _ist.testObjectToJson({
'postId': postId,
'body': body,
'title': title,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> delete(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue postId,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'delete',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'delete',
parameters: _ist.testObjectToJson({'postId': postId}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> accept(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue questionId,
_is.UuidValue? answerId,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'accept',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'accept',
parameters: _ist.testObjectToJson({
'questionId': questionId,
'answerId': answerId,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> hide(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue postId,
bool hidden,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'hide',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'hide',
parameters: _ist.testObjectToJson({
'postId': postId,
'hidden': hidden,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<List<_iqo3lug2.DiscussionSummary>> inbox(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'inbox',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'inbox',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_iqo3lug2.DiscussionSummary>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<int> unseen(_ist.TestSessionBuilder sessionBuilder) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'unseen',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'unseen',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<int>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<void> markSeen(
_ist.TestSessionBuilder sessionBuilder,
_is.UuidValue questionId,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'discussion',
method: 'markSeen',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'discussion',
methodName: 'markSeen',
parameters: _ist.testObjectToJson({'questionId': questionId}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _ExploreEndpoint {
_ExploreEndpoint(
this._endpointDispatch,