mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Require explicit AppCatalog relay acceptance before treating an app report as published
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:models/models.dart';
|
import 'package:models/models.dart';
|
||||||
import 'package:zapstore/services/notification_service.dart';
|
import 'package:zapstore/services/notification_service.dart';
|
||||||
|
import 'package:zapstore/utils/extensions.dart';
|
||||||
|
|
||||||
const kMinimumReportDescriptionLength = 20;
|
const kMinimumReportDescriptionLength = 20;
|
||||||
final _hexIdentifier = RegExp(r'^[0-9a-f]{64}$', caseSensitive: false);
|
final _hexIdentifier = RegExp(r'^[0-9a-f]{64}$', caseSensitive: false);
|
||||||
@@ -16,6 +17,25 @@ bool canSubmitAppReport({
|
|||||||
violationType != null &&
|
violationType != null &&
|
||||||
description.trim().length >= kMinimumReportDescriptionLength;
|
description.trim().length >= kMinimumReportDescriptionLength;
|
||||||
|
|
||||||
|
bool wasAppReportAccepted(PublishResponse response, String reportEventId) =>
|
||||||
|
response.results[reportEventId]?.any((result) => result.accepted) ?? false;
|
||||||
|
|
||||||
|
String appReportPublishFailure(PublishResponse response, String reportEventId) {
|
||||||
|
final results = response.results[reportEventId];
|
||||||
|
if (results == null || results.isEmpty) {
|
||||||
|
return 'No relay responded to the report. Check your connection and retry.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final rejection = results.firstWhere((result) => !result.accepted);
|
||||||
|
final reason = rejection.message?.trim();
|
||||||
|
if (reason == null || reason.isEmpty) {
|
||||||
|
return 'Relay ${rejection.relayUrl} explicitly rejected the report without '
|
||||||
|
'providing a reason.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Relay ${rejection.relayUrl} rejected the report: $reason';
|
||||||
|
}
|
||||||
|
|
||||||
bool canReportApp(App app) =>
|
bool canReportApp(App app) =>
|
||||||
reportableAppEventId(app) != null && _hexIdentifier.hasMatch(app.pubkey);
|
reportableAppEventId(app) != null && _hexIdentifier.hasMatch(app.pubkey);
|
||||||
|
|
||||||
@@ -234,8 +254,14 @@ class AppReportSheet extends HookConsumerWidget {
|
|||||||
reason: description,
|
reason: description,
|
||||||
).signWith(signer);
|
).signWith(signer);
|
||||||
|
|
||||||
await report.save();
|
await ref.storage.save({report});
|
||||||
await report.publish(relays: 'AppCatalog');
|
final response = await ref.storage.publish({
|
||||||
|
report,
|
||||||
|
}, relays: 'AppCatalog');
|
||||||
|
if (!wasAppReportAccepted(response, report.id)) {
|
||||||
|
submissionError.value = appReportPublishFailure(response, report.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# WORK-016 — Report Publish Confirmation
|
||||||
|
|
||||||
|
**Feature:** FEAT-009-nip56-app-reporting.md
|
||||||
|
**Status:** Complete
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [x] 1. Treat a NIP-56 report as published only after an AppCatalog relay
|
||||||
|
explicitly accepts its event.
|
||||||
|
- Files: `lib/widgets/app_report_sheet.dart`
|
||||||
|
- [x] 2. Preserve the report and expose a retryable failure when no relay
|
||||||
|
accepts the event, including timeouts and rejections.
|
||||||
|
- Files: `lib/widgets/app_report_sheet.dart`
|
||||||
|
- [x] 3. Cover accepted, rejected, and absent relay responses.
|
||||||
|
- Files: `test/widgets/app_report_sheet_test.dart`
|
||||||
|
- [x] 4. Display the relay's rejection reason when it responds negatively.
|
||||||
|
- Files: `lib/widgets/app_report_sheet.dart`,
|
||||||
|
`test/widgets/app_report_sheet_test.dart`
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
| Scenario | Expected | Status |
|
||||||
|
|----------|----------|--------|
|
||||||
|
| Relay accepts report event | Success is reported | [x] |
|
||||||
|
| Relay rejects or times out | Form stays open with retryable error | [x] |
|
||||||
|
| No response for report event | Form stays open with retryable error | [x] |
|
||||||
|
| Relay rejection includes a reason | User sees the relay's reason | [x] |
|
||||||
|
| Relay rejection has no reason | User sees that the relay omitted it | [x] |
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 2026-07-10 — Explicit relay acceptance
|
||||||
|
|
||||||
|
**Decision:** The report flow waits for purplebase's publish response and
|
||||||
|
considers publishing successful only when at least one AppCatalog relay accepts
|
||||||
|
the signed report event.
|
||||||
|
|
||||||
|
**Rationale:** A timeout or relay rejection is a completed transport response,
|
||||||
|
not a successful submission.
|
||||||
@@ -48,4 +48,82 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('wasAppReportAccepted', () {
|
||||||
|
test('accepts an explicit relay acceptance for the report event', () {
|
||||||
|
final response = PublishResponse()
|
||||||
|
..addEvent(
|
||||||
|
'report-id',
|
||||||
|
relayUrl: 'wss://relay.zapstore.dev',
|
||||||
|
accepted: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wasAppReportAccepted(response, 'report-id'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a relay rejection or timeout', () {
|
||||||
|
final response = PublishResponse()
|
||||||
|
..addEvent(
|
||||||
|
'report-id',
|
||||||
|
relayUrl: 'wss://relay.zapstore.dev',
|
||||||
|
accepted: false,
|
||||||
|
message: 'Timeout',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wasAppReportAccepted(response, 'report-id'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects a response without the report event', () {
|
||||||
|
final response = PublishResponse()
|
||||||
|
..addEvent(
|
||||||
|
'another-event-id',
|
||||||
|
relayUrl: 'wss://relay.zapstore.dev',
|
||||||
|
accepted: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wasAppReportAccepted(response, 'report-id'), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('appReportPublishFailure', () {
|
||||||
|
test('includes the relay rejection reason', () {
|
||||||
|
final response = PublishResponse()
|
||||||
|
..addEvent(
|
||||||
|
'report-id',
|
||||||
|
relayUrl: 'wss://relay.zapstore.dev',
|
||||||
|
accepted: false,
|
||||||
|
message: 'rate-limited: slow down chief',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
appReportPublishFailure(response, 'report-id'),
|
||||||
|
'Relay wss://relay.zapstore.dev rejected the report: '
|
||||||
|
'rate-limited: slow down chief',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explains when the report event has no relay response', () {
|
||||||
|
final response = PublishResponse();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
appReportPublishFailure(response, 'report-id'),
|
||||||
|
'No relay responded to the report. Check your connection and retry.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explains when the relay rejects without a reason', () {
|
||||||
|
final response = PublishResponse()
|
||||||
|
..addEvent(
|
||||||
|
'report-id',
|
||||||
|
relayUrl: 'wss://relay.zapstore.dev',
|
||||||
|
accepted: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
appReportPublishFailure(response, 'report-id'),
|
||||||
|
'Relay wss://relay.zapstore.dev explicitly rejected the report without '
|
||||||
|
'providing a reason.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user