feat(social): standard NIP-56 reporting — ReportTransport in commons_core, report sheet on offers and chats, local hide + optional block
This commit is contained in:
parent
11b242edbf
commit
0e7faaeb90
15 changed files with 560 additions and 15 deletions
|
|
@ -15,6 +15,7 @@ export 'src/social/certification.dart';
|
|||
export 'src/social/geohash.dart';
|
||||
export 'src/social/message_transport.dart';
|
||||
export 'src/social/nostr_ids.dart';
|
||||
export 'src/social/nostr/nip99_codec.dart';
|
||||
export 'src/social/nostr/nostr_channel.dart';
|
||||
export 'src/social/nostr/nostr_sync_transport.dart';
|
||||
export 'src/social/nostr/nostr_connection.dart';
|
||||
|
|
@ -22,6 +23,7 @@ export 'src/social/nostr/nostr_message_transport.dart';
|
|||
export 'src/social/nostr/nostr_offer_transport.dart';
|
||||
export 'src/social/nostr/nostr_profile_transport.dart';
|
||||
export 'src/social/nostr/nostr_rating_transport.dart';
|
||||
export 'src/social/nostr/nostr_report_transport.dart';
|
||||
export 'src/social/nostr/nostr_trust_transport.dart';
|
||||
export 'src/social/nostr/relay_pool.dart';
|
||||
export 'src/social/offer.dart';
|
||||
|
|
@ -29,6 +31,7 @@ export 'src/social/offer_transport.dart';
|
|||
export 'src/social/profile_transport.dart';
|
||||
export 'src/social/rating.dart';
|
||||
export 'src/social/rating_transport.dart';
|
||||
export 'src/social/report_transport.dart';
|
||||
export 'src/social/sync_transport.dart';
|
||||
export 'src/social/trust_transport.dart';
|
||||
export 'src/social/web_of_trust.dart';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import 'package:nostr/nostr.dart';
|
||||
|
||||
import '../report_transport.dart';
|
||||
import 'nostr_channel.dart';
|
||||
|
||||
/// Reports carried as standard NIP-56 events on the shared [NostrConnection]:
|
||||
/// kind 1984, the reported key in a typed `p` tag (and the offending event in
|
||||
/// a typed `e` tag when given), free text in the content. Standard on purpose
|
||||
/// so any relay operator or moderation tool can act on them.
|
||||
class NostrReportTransport implements ReportTransport {
|
||||
NostrReportTransport(this._conn);
|
||||
|
||||
final NostrChannel _conn;
|
||||
|
||||
/// NIP-56 reporting kind.
|
||||
static const kindReport = 1984;
|
||||
|
||||
@override
|
||||
Future<void> report({
|
||||
required String subjectPubkey,
|
||||
String? eventId,
|
||||
String? eventAddress,
|
||||
required ReportReason reason,
|
||||
String details = '',
|
||||
}) async {
|
||||
final type = reason.name;
|
||||
final event = Event.from(
|
||||
kind: kindReport,
|
||||
content: details,
|
||||
tags: [
|
||||
if (eventId != null) ['e', eventId, type],
|
||||
if (eventAddress != null) ['a', eventAddress, type],
|
||||
['p', subjectPubkey, type],
|
||||
],
|
||||
secretKey: _conn.privateKeyHex,
|
||||
);
|
||||
final r = await _conn.publish(event);
|
||||
if (!r.accepted) throw StateError('relay rejected report: ${r.message}');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() => _conn.close();
|
||||
}
|
||||
23
packages/commons_core/lib/src/social/report_transport.dart
Normal file
23
packages/commons_core/lib/src/social/report_transport.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/// The standard report categories (NIP-56). Kept as an enum so the app maps
|
||||
/// its human wording onto interoperable values.
|
||||
enum ReportReason { nudity, malware, profanity, illegal, spam, impersonation, other }
|
||||
|
||||
/// Flagging bad actors/content over the shared connection. Reports are public
|
||||
/// signed statements ("this key/event breaks the rules") that relays and
|
||||
/// moderation tools act on — the network-side half of the report/block story
|
||||
/// (the local half is each user's own blocklist, which lives in the app).
|
||||
abstract interface class ReportTransport {
|
||||
/// Publishes a report about [subjectPubkey]; when the complaint is about one
|
||||
/// specific event pass its [eventId], or — for addressable content such as
|
||||
/// offers — its address ([eventAddress], `kind:pubkey:d`). [details] is
|
||||
/// optional free text for a human moderator.
|
||||
Future<void> report({
|
||||
required String subjectPubkey,
|
||||
String? eventId,
|
||||
String? eventAddress,
|
||||
required ReportReason reason,
|
||||
String details = '',
|
||||
});
|
||||
|
||||
Future<void> close();
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:nostr/nostr.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../support/mini_relay.dart';
|
||||
|
||||
/// Reports are standard NIP-56 events (kind 1984): a `p` tag naming the
|
||||
/// reported key (with the report type), optionally an `e` tag naming the
|
||||
/// offending event, and free text in the content. Standard on purpose — any
|
||||
/// Nostr relay or moderation tool understands them.
|
||||
void main() {
|
||||
late MiniRelay relay;
|
||||
setUp(() async => relay = await MiniRelay.start());
|
||||
tearDown(() async => relay.stop());
|
||||
|
||||
Future<NostrIdentity> idFor(int fill) => NostrKeyDerivation.deriveFromSeed(
|
||||
Uint8List(32)..fillRange(0, 32, fill),
|
||||
);
|
||||
|
||||
test('reporting a person publishes a kind-1984 event with the typed p tag',
|
||||
() async {
|
||||
final alice = await idFor(1);
|
||||
final conn = await NostrConnection.connect(relay.url, identity: alice);
|
||||
final reports = NostrReportTransport(conn);
|
||||
|
||||
await reports.report(
|
||||
subjectPubkey: 'ab' * 32,
|
||||
reason: ReportReason.spam,
|
||||
details: 'floods the market with fake offers',
|
||||
);
|
||||
|
||||
final stored = await conn
|
||||
.reqOnce(Filter(kinds: const [NostrReportTransport.kindReport]));
|
||||
expect(stored, hasLength(1));
|
||||
final event = stored.single;
|
||||
expect(event.pubkey, alice.publicKeyHex);
|
||||
expect(event.content, 'floods the market with fake offers');
|
||||
expect(event.tags, anyElement(equals(['p', 'ab' * 32, 'spam'])));
|
||||
await reports.close();
|
||||
});
|
||||
|
||||
test('reporting an offending event adds the typed e tag too', () async {
|
||||
final alice = await idFor(1);
|
||||
final conn = await NostrConnection.connect(relay.url, identity: alice);
|
||||
final reports = NostrReportTransport(conn);
|
||||
|
||||
await reports.report(
|
||||
subjectPubkey: 'cd' * 32,
|
||||
eventId: 'ef' * 32,
|
||||
reason: ReportReason.illegal,
|
||||
);
|
||||
|
||||
final stored = await conn
|
||||
.reqOnce(Filter(kinds: const [NostrReportTransport.kindReport]));
|
||||
final event = stored.single;
|
||||
expect(event.tags, anyElement(equals(['e', 'ef' * 32, 'illegal'])));
|
||||
expect(event.tags, anyElement(equals(['p', 'cd' * 32, 'illegal'])));
|
||||
expect(event.content, isEmpty);
|
||||
await reports.close();
|
||||
});
|
||||
|
||||
test('reporting addressable content (an offer) adds the typed a tag',
|
||||
() async {
|
||||
final alice = await idFor(1);
|
||||
final conn = await NostrConnection.connect(relay.url, identity: alice);
|
||||
final reports = NostrReportTransport(conn);
|
||||
final address = '30402:${'cd' * 32}:tomate-1';
|
||||
|
||||
await reports.report(
|
||||
subjectPubkey: 'cd' * 32,
|
||||
eventAddress: address,
|
||||
reason: ReportReason.spam,
|
||||
);
|
||||
|
||||
final stored = await conn
|
||||
.reqOnce(Filter(kinds: const [NostrReportTransport.kindReport]));
|
||||
expect(
|
||||
stored.single.tags,
|
||||
anyElement(equals(['a', address, 'spam'])),
|
||||
);
|
||||
await reports.close();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue