feat(base): handles hidden until their people go public, every answer about a new handle counted (ADR 0020, D5)
A handle's page and profiles.byHandle answer as for a handle nobody has until its person goes public: a handle fixed by a publish, or a display name, a bio, a link or an avatar (isPublic). A person's own profile, and the host's own lookups (personWithHandle), are unchanged. So every answer about a new handle now tells that an account has it, and countHandleCheck counts them together on the register, 30 an hour per person (or attempt) and 60 per network, refused beyond with ProfileRefusal.tooManyHandleChecks: profile.checkHandle and saving a profile with a handle new for the person, simpleLogin.checkHandle and signUp, keys.set with a handle, and Halkyon Learn's inbox.block and unblock, which look people up by handle. A person's own handle and what is no handle aren't counted. The SimpleLogin limiters of commit 3 give way to it, so a network's 60 are shared between people signed in and people new. The register's checkHandle, saveProfile, signUpWithSimpleLogin and setFirstKeys carry the visitor's network from the member that asked. Halkyon Learn's handle dialog words the new refusal (English, German). Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e5b9578916
commit
7eb1961b9f
13 changed files with 674 additions and 134 deletions
File diff suppressed because one or more lines are too long
|
|
@ -719,7 +719,9 @@ class EndpointInbox extends _isc.EndpointRef {
|
|||
|
||||
/// Keeps the person with [handle] from writing on the caller's published
|
||||
/// studysets: what they wrote there, and write, shows to them alone.
|
||||
/// Throws [NotFoundException] for a handle nobody has.
|
||||
/// Throws [NotFoundException] for a handle nobody has; at most 30 an hour
|
||||
/// with the caller's other answers about handles, as checking one
|
||||
/// (`ProfileRefusal.tooManyHandleChecks`).
|
||||
_ida.Future<void> block(String handle) => caller.callServerEndpoint<void>(
|
||||
'inbox',
|
||||
'block',
|
||||
|
|
@ -727,7 +729,7 @@ class EndpointInbox extends _isc.EndpointRef {
|
|||
);
|
||||
|
||||
/// Lets the person with [handle] write on the caller's published
|
||||
/// studysets again.
|
||||
/// studysets again; counted as [block] is.
|
||||
_ida.Future<void> unblock(String handle) => caller.callServerEndpoint<void>(
|
||||
'inbox',
|
||||
'unblock',
|
||||
|
|
|
|||
|
|
@ -541,6 +541,7 @@
|
|||
"profileProblemLinkInvalid": "Links beginnen mit https:// oder http://.",
|
||||
"profileProblemAvatarInvalid": "Dieses Bild lässt sich nicht verwenden: Wähle ein PNG-, JPEG-, WebP- oder GIF-Bild.",
|
||||
"profileProblemTooManyLinkChecks": "Die Links wurden zu oft geprüft. Versuche es in einer Stunde noch einmal.",
|
||||
"profileProblemTooManyHandleChecks": "Es wurden zu viele Handles ausprobiert. Versuche es in einer Stunde noch einmal.",
|
||||
"publicProfilePicture": "Bild",
|
||||
"publicProfileChoosePicture": "Bild wählen",
|
||||
"publicProfileRemovePicture": "Bild entfernen",
|
||||
|
|
|
|||
|
|
@ -612,6 +612,7 @@
|
|||
"profileProblemLinkInvalid": "Links start with https:// or http://.",
|
||||
"profileProblemAvatarInvalid": "This picture can't be used: choose a PNG, JPEG, WebP or GIF image.",
|
||||
"profileProblemTooManyLinkChecks": "The links were checked too often. Try again in an hour.",
|
||||
"profileProblemTooManyHandleChecks": "Too many handles were tried. Try again in an hour.",
|
||||
"publicProfilePicture": "Picture",
|
||||
"publicProfileChoosePicture": "Choose picture",
|
||||
"publicProfileRemovePicture": "Remove picture",
|
||||
|
|
|
|||
|
|
@ -3244,6 +3244,12 @@ abstract class AppLocalizations {
|
|||
/// **'The links were checked too often. Try again in an hour.'**
|
||||
String get profileProblemTooManyLinkChecks;
|
||||
|
||||
/// No description provided for @profileProblemTooManyHandleChecks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Too many handles were tried. Try again in an hour.'**
|
||||
String get profileProblemTooManyHandleChecks;
|
||||
|
||||
/// No description provided for @publicProfilePicture.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1963,6 +1963,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get profileProblemTooManyLinkChecks =>
|
||||
'Die Links wurden zu oft geprüft. Versuche es in einer Stunde noch einmal.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyHandleChecks =>
|
||||
'Es wurden zu viele Handles ausprobiert. Versuche es in einer Stunde noch einmal.';
|
||||
|
||||
@override
|
||||
String get publicProfilePicture => 'Bild';
|
||||
|
||||
|
|
|
|||
|
|
@ -1944,6 +1944,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get profileProblemTooManyLinkChecks =>
|
||||
'The links were checked too often. Try again in an hour.';
|
||||
|
||||
@override
|
||||
String get profileProblemTooManyHandleChecks =>
|
||||
'Too many handles were tried. Try again in an hour.';
|
||||
|
||||
@override
|
||||
String get publicProfilePicture => 'Picture';
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,10 @@ enum ProfileProblem() {
|
|||
avatarInvalid,
|
||||
|
||||
/// The links were checked too often in the last hour.
|
||||
tooManyLinkChecks;
|
||||
tooManyLinkChecks,
|
||||
|
||||
/// Too many handles were asked about in the last hour.
|
||||
tooManyHandleChecks;
|
||||
|
||||
/// The problems with a handle.
|
||||
static const handleProblems = {
|
||||
|
|
@ -118,6 +121,7 @@ enum ProfileProblem() {
|
|||
handleReserved,
|
||||
handleTaken,
|
||||
published,
|
||||
tooManyHandleChecks,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -679,20 +679,22 @@ class const _Picture({
|
|||
}
|
||||
}
|
||||
|
||||
String _problemText(AppLocalizations l10n, ProfileProblem problem) =>
|
||||
switch (problem) {
|
||||
ProfileProblem.handleInvalid => l10n.profileProblemHandleInvalid,
|
||||
ProfileProblem.handleReserved => l10n.profileProblemHandleReserved,
|
||||
ProfileProblem.handleTaken => l10n.profileProblemHandleTaken,
|
||||
ProfileProblem.published => l10n.profileProblemPublished,
|
||||
ProfileProblem.displayNameTooLong =>
|
||||
l10n.profileProblemDisplayNameTooLong,
|
||||
ProfileProblem.bioTooLong => l10n.profileProblemBioTooLong,
|
||||
ProfileProblem.tooManyLinks => l10n.profileProblemTooManyLinks,
|
||||
ProfileProblem.linkInvalid => l10n.profileProblemLinkInvalid,
|
||||
ProfileProblem.avatarInvalid => l10n.profileProblemAvatarInvalid,
|
||||
ProfileProblem.tooManyLinkChecks => l10n.profileProblemTooManyLinkChecks,
|
||||
};
|
||||
String _problemText(
|
||||
AppLocalizations l10n,
|
||||
ProfileProblem problem,
|
||||
) => switch (problem) {
|
||||
ProfileProblem.handleInvalid => l10n.profileProblemHandleInvalid,
|
||||
ProfileProblem.handleReserved => l10n.profileProblemHandleReserved,
|
||||
ProfileProblem.handleTaken => l10n.profileProblemHandleTaken,
|
||||
ProfileProblem.published => l10n.profileProblemPublished,
|
||||
ProfileProblem.displayNameTooLong => l10n.profileProblemDisplayNameTooLong,
|
||||
ProfileProblem.bioTooLong => l10n.profileProblemBioTooLong,
|
||||
ProfileProblem.tooManyLinks => l10n.profileProblemTooManyLinks,
|
||||
ProfileProblem.linkInvalid => l10n.profileProblemLinkInvalid,
|
||||
ProfileProblem.avatarInvalid => l10n.profileProblemAvatarInvalid,
|
||||
ProfileProblem.tooManyLinkChecks => l10n.profileProblemTooManyLinkChecks,
|
||||
ProfileProblem.tooManyHandleChecks => l10n.profileProblemTooManyHandleChecks,
|
||||
};
|
||||
|
||||
/// The profile's page on the web, to link back to, with a way to copy its
|
||||
/// address, and what linking back to it does (ADR 0011).
|
||||
|
|
|
|||
|
|
@ -103,6 +103,27 @@ void main() {
|
|||
expect(find.text('Saved'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the dialog says when the instance answers no more about '
|
||||
'handles this hour, and nothing can be chosen', (tester) async {
|
||||
final profiles = FakeProfileGateway()..checksSpent = true;
|
||||
await showProfile(tester, profiles);
|
||||
|
||||
await tester.tap(find.text('Choose a handle'));
|
||||
await tester.pumpAndSettle();
|
||||
await typeHandle(tester, 'raisondetredev');
|
||||
|
||||
expect(
|
||||
find.text('Too many handles were tried. Try again in an hour.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<FilledButton>(find.widgetWithText(FilledButton, 'Choose'))
|
||||
.onPressed,
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a profile saves a field when it is left or Enter is pressed, '
|
||||
'only what changed, without losing what is being written', (
|
||||
tester,
|
||||
|
|
|
|||
|
|
@ -39,13 +39,18 @@ class FakeProfileGateway({
|
|||
/// The handles checked, in order.
|
||||
final checked = <String>[];
|
||||
|
||||
/// Whether the instance answers no more about handles this hour.
|
||||
bool checksSpent = false;
|
||||
|
||||
@override
|
||||
Future<ProfileProblem?> checkHandle(String handle) async {
|
||||
if (fail case final error?) {
|
||||
throw error;
|
||||
}
|
||||
checked.add(handle);
|
||||
return _problemOf(handle);
|
||||
return checksSpent
|
||||
? ProfileProblem.tooManyHandleChecks
|
||||
: _problemOf(handle);
|
||||
}
|
||||
|
||||
ProfileProblem? _problemOf(String handle) {
|
||||
|
|
|
|||
|
|
@ -94,7 +94,9 @@ class InboxEndpoint extends Endpoint {
|
|||
|
||||
/// Keeps the person with [handle] from writing on the caller's published
|
||||
/// studysets: what they wrote there, and write, shows to them alone.
|
||||
/// Throws [NotFoundException] for a handle nobody has.
|
||||
/// Throws [NotFoundException] for a handle nobody has; at most 30 an hour
|
||||
/// with the caller's other answers about handles, as checking one
|
||||
/// (`ProfileRefusal.tooManyHandleChecks`).
|
||||
Future<void> block(Session session, String handle) async {
|
||||
final author = session.authenticated!.authUserId;
|
||||
final blocked = await _personOf(session, handle);
|
||||
|
|
@ -119,7 +121,7 @@ class InboxEndpoint extends Endpoint {
|
|||
}
|
||||
|
||||
/// Lets the person with [handle] write on the caller's published
|
||||
/// studysets again.
|
||||
/// studysets again; counted as [block] is.
|
||||
Future<void> unblock(Session session, String handle) async {
|
||||
final author = session.authenticated!.authUserId;
|
||||
final blocked = await _personOf(session, handle);
|
||||
|
|
@ -195,10 +197,19 @@ class InboxEndpoint extends Endpoint {
|
|||
);
|
||||
}
|
||||
|
||||
/// The person with [handle]; throws [NotFoundException] for none.
|
||||
Future<UuidValue> _personOf(Session session, String handle) async =>
|
||||
await personWithHandle(session, handle.trim()) ??
|
||||
(throw NotFoundException(what: 'person'));
|
||||
/// The person with [handle]; throws [NotFoundException] for none. Counted
|
||||
/// with the caller's other answers about handles (`countHandleCheck`),
|
||||
/// since whose a handle is tells that an account has it, while handles
|
||||
/// stay hidden until their people go public (ADR 0020, D5).
|
||||
Future<UuidValue> _personOf(Session session, String handle) async {
|
||||
await countHandleCheck(
|
||||
session,
|
||||
asker: session.authenticated!.authUserId.uuid,
|
||||
network: networkOf(session),
|
||||
);
|
||||
return 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`).
|
||||
|
|
|
|||
|
|
@ -261,6 +261,33 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test('blocking and unblocking count among the answers about handles '
|
||||
'(30 an hour), as whose a handle is tells that an account has '
|
||||
'it', () async {
|
||||
// Her handle, new as her profile was made, counted one.
|
||||
for (var i = 1; i < handleChecksPerAsker; i++) {
|
||||
await countHandleCheck(session, asker: alice.uuid, network: null);
|
||||
}
|
||||
|
||||
for (final call in [
|
||||
() => endpoints.inbox.block(signedIn(alice), 'bob'),
|
||||
() => endpoints.inbox.unblock(signedIn(alice), 'bob'),
|
||||
]) {
|
||||
await expectLater(
|
||||
call(),
|
||||
throwsA(
|
||||
isA<ProfileRefusedException>().having(
|
||||
(error) => error.reason,
|
||||
'reason',
|
||||
ProfileRefusal.tooManyHandleChecks,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(await endpoints.inbox.blockedPeople(signedIn(alice)), isEmpty);
|
||||
await endpoints.inbox.block(signedIn(bob), 'alice');
|
||||
});
|
||||
|
||||
test("a suggestion's files go to the author alone", () async {
|
||||
final publication = await japanese();
|
||||
final image = utf8.encode('an image');
|
||||
|
|
|
|||
Loading…
Reference in a new issue