diff --git a/lib/src/event.dart b/lib/src/event.dart index 97fd630..d41b85c 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -2,8 +2,11 @@ import 'dart:convert'; import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; import 'package:bip340/bip340.dart' as bip340; +import 'package:json_annotation/json_annotation.dart'; import 'package:nostr/src/utils.dart'; +part 'event.g.dart'; + /// The only object type that exists is the event, which has the following format on the wire: /// /// - "id": "32-bytes hex-encoded sha256 of the the serialized event data" @@ -16,6 +19,7 @@ import 'package:nostr/src/utils.dart'; /// ], /// - "content": "arbitrary string", /// - "sig": "64-bytes signature of the sha256 hash of the serialized event data, which is the same as the 'id' field" +@JsonSerializable() class Event { /// 32-bytes hex-encoded sha256 of the the serialized event data (hex) late String id; @@ -24,28 +28,32 @@ class Event { late String pubkey; /// unix timestamp in seconds - late int createdAt; + @JsonKey( + name: 'created_at', + ) + int createdAt; /// - 0: set_metadata: the content is set to a stringified JSON object {name: , about: , picture: } describing the user who created the event. A relay may delete past set_metadata events once it gets a new one for the same pubkey. /// - 1: text_note: the content is set to the text content of a note (anything the user wants to say). Non-plaintext notes should instead use kind 1000-10000 as described in NIP-16. /// - 2: recommend_server: the content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to recommend to its followers. - late int kind; + final int kind; /// The tags array can store a tag identifier as the first element of each subarray, plus arbitrary information afterward (always as strings). /// /// This NIP defines "p" — meaning "pubkey", which points to a pubkey of someone that is referred to in the event —, and "e" — meaning "event", which points to the id of an event this event is quoting, replying to or referring to somehow. - late List> tags; + final List> tags; /// arbitrary string - String content = ""; + @JsonKey(defaultValue: '') + String content; /// 64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field late String sig; /// subscription_id is a random string that should be used to represent a subscription. + @JsonKey(includeIfNull: false, toJson: null) String? subscriptionId; - /// Default constructor Event( this.id, this.pubkey, @@ -53,8 +61,9 @@ class Event { this.kind, this.tags, this.content, - this.sig, - ) { + this.sig, { + this.subscriptionId, + }) { assert(createdAt.toString().length == 10); assert(createdAt <= currentUnixTimestampSeconds()); pubkey = pubkey.toLowerCase(); @@ -63,47 +72,36 @@ class Event { assert(bip340.verify(pubkey, id, sig)); } - /// Instanciate Event object from the minimum available data - Event.from( - {this.createdAt = 0, - required this.kind, - required this.tags, - required this.content, - required String privkey, - this.subscriptionId}) { - if (createdAt == 0) { - createdAt = currentUnixTimestampSeconds(); - } - assert(createdAt.toString().length == 10); - assert(createdAt <= currentUnixTimestampSeconds()); - pubkey = bip340.getPublicKey(privkey).toLowerCase(); - id = getEventId(); - sig = getSignature(privkey); - } + factory Event.fromJson(Map json) => _$EventFromJson(json); - /// Deserialize an event from a JSON - Event.fromJson(Map json, {this.subscriptionId}) { - id = json['id']; - pubkey = json['pubkey']; - createdAt = json['created_at']; - kind = json['kind']; - tags = (json['tags'] as List) - .map((e) => (e as List).map((e) => e as String).toList()) - .toList(); - content = json['content']; - sig = json['sig']; + Map toJson() => _$EventToJson(this); + + /// To obtain the event.id, we sha256 the serialized event. + /// The serialization is done over the UTF-8 JSON-serialized string (with no white space or line breaks) of the following structure: + /// + ///[ + /// 0, + /// , + /// , + /// , + /// , + /// + ///] + String getEventId() { + List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content]; + String serializedEvent = json.encode(data); + List hash = sha256.convert(utf8.encode(serializedEvent)).bytes; + return hex.encode(hash); } - /// Serialize an event in JSON - Map toJson() => { - 'id': id, - 'pubkey': pubkey, - 'created_at': createdAt, - 'kind': kind, - 'tags': tags, - 'content': content, - 'sig': sig - }; + /// Each user has a keypair. Signatures, public key, and encodings are done according to the Schnorr signatures standard for the curve secp256k1 + /// 64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field + String getSignature(String privateKey) { + /// aux must be 32-bytes random bytes, generated at signature time. + /// https://github.com/nbd-wtf/dart-bip340/blob/master/lib/src/bip340.dart#L10 + String aux = generate64RandomHexChars(); + return bip340.sign(privateKey, id, aux); + } /// Serialize to nostr event message /// - ["EVENT", event JSON as defined above] @@ -119,51 +117,43 @@ class Event { /// Deserialize a nostr event message /// - ["EVENT", event JSON as defined above] /// - ["EVENT", subscription_id, event JSON as defined above] - Event.deserialize(input) { + factory Event.deserialize(input) { Map json = {}; if (input.length == 2) { json = input[1] as Map; + + final event = _$EventFromJson(json); + + return event; } else if (input.length == 3) { json = input[2] as Map; - subscriptionId = input[1]; + json['subscriptionId'] = input[1]; + + final event = _$EventFromJson(json); + + return event; } else { throw Exception('invalid input'); } - id = json['id']; - pubkey = json['pubkey']; - createdAt = json['created_at']; - kind = json['kind']; - tags = (json['tags'] as List) - .map((e) => (e as List).map((e) => e as String).toList()) - .toList(); - content = json['content']; - sig = json['sig']; - } - - /// To obtain the event.id, we sha256 the serialized event. - /// The serialization is done over the UTF-8 JSON-serialized string (with no white space or line breaks) of the following structure: - /// - ///[ - /// 0, - /// , - /// , - /// , - /// , - /// - ///] - String getEventId() { - List data = [0, pubkey.toLowerCase(), createdAt, kind, tags, content]; - String serializedEvent = json.encode(data); - List hash = sha256.convert(utf8.encode(serializedEvent)).bytes; - return hex.encode(hash); } - /// Each user has a keypair. Signatures, public key, and encodings are done according to the Schnorr signatures standard for the curve secp256k1 - /// 64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field - String getSignature(String privateKey) { - /// aux must be 32-bytes random bytes, generated at signature time. - /// https://github.com/nbd-wtf/dart-bip340/blob/master/lib/src/bip340.dart#L10 - String aux = generate64RandomHexChars(); - return bip340.sign(privateKey, id, aux); + /// Instanciate Event object from the minimum available data + Event.from( + {this.createdAt = 0, + required this.kind, + required this.tags, + required this.content, + required String privkey, + this.subscriptionId}) { + if (createdAt == 0) { + createdAt = currentUnixTimestampSeconds(); + } else { + createdAt = createdAt; + } + assert(createdAt.toString().length == 10); + assert(createdAt <= currentUnixTimestampSeconds()); + pubkey = bip340.getPublicKey(privkey).toLowerCase(); + id = getEventId(); + sig = getSignature(privkey); } } diff --git a/lib/src/event.g.dart b/lib/src/event.g.dart new file mode 100644 index 0000000..03d9f67 --- /dev/null +++ b/lib/src/event.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'event.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Event _$EventFromJson(Map json) => Event( + json['id'] as String, + json['pubkey'] as String, + json['created_at'] as int, + json['kind'] as int, + (json['tags'] as List) + .map((e) => (e as List).map((e) => e as String).toList()) + .toList(), + json['content'] as String? ?? '', + json['sig'] as String, + subscriptionId: json['subscriptionId'] as String?, + ); + +Map _$EventToJson(Event instance) { + final val = { + 'id': instance.id, + 'pubkey': instance.pubkey, + 'created_at': instance.createdAt, + 'kind': instance.kind, + 'tags': instance.tags, + 'content': instance.content, + 'sig': instance.sig, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('subscriptionId', instance.subscriptionId); + return val; +} diff --git a/lib/src/filter.dart b/lib/src/filter.dart index a272936..1608449 100644 --- a/lib/src/filter.dart +++ b/lib/src/filter.dart @@ -1,4 +1,9 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'filter.g.dart'; + /// filter is a JSON object that determines what events will be sent in that subscription +@JsonSerializable(includeIfNull: false) class Filter { /// a list of event ids or prefixes List? ids; @@ -10,9 +15,11 @@ class Filter { List? kinds; /// a list of event ids that are referenced in an "e" tag + @JsonKey(name: '#e') List? e; /// a list of pubkeys that are referenced in a "p" tag + @JsonKey(name: '#p') List? p; /// a timestamp, events must be newer than this to pass @@ -25,56 +32,18 @@ class Filter { int? limit; /// Default constructor - Filter( - {this.ids, - this.authors, - this.kinds, - this.e, - this.p, - this.since, - this.until, - this.limit}); - - /// Deserialize a filter from a JSON - Filter.fromJson(Map json) { - ids = json['ids'] == null ? null : List.from(json['ids']); - authors = - json['authors'] == null ? null : List.from(json['authors']); - kinds = json['kinds'] == null ? null : List.from(json['kinds']); - e = json['#e'] == null ? null : List.from(json['#e']); - p = json['#p'] == null ? null : List.from(json['#p']); - since = json['since']; - until = json['until']; - limit = json['limit']; - } - - /// Serialize a filter in JSON - Map toJson() { - final Map data = {}; - if (ids != null) { - data['ids'] = ids; - } - if (authors != null) { - data['authors'] = authors; - } - if (kinds != null) { - data['kinds'] = kinds; - } - if (e != null) { - data['#e'] = e; - } - if (p != null) { - data['#p'] = p; - } - if (since != null) { - data['since'] = since; - } - if (until != null) { - data['until'] = until; - } - if (limit != null) { - data['limit'] = limit; - } - return data; - } + Filter({ + this.ids, + this.authors, + this.kinds, + this.e, + this.p, + this.since, + this.until, + this.limit, + }); + + factory Filter.fromJson(Map json) => _$FilterFromJson(json); + + Map toJson() => _$FilterToJson(this); } diff --git a/lib/src/filter.g.dart b/lib/src/filter.g.dart new file mode 100644 index 0000000..6407e90 --- /dev/null +++ b/lib/src/filter.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'filter.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Filter _$FilterFromJson(Map json) => Filter( + ids: (json['ids'] as List?)?.map((e) => e as String).toList(), + authors: + (json['authors'] as List?)?.map((e) => e as String).toList(), + kinds: (json['kinds'] as List?)?.map((e) => e as int).toList(), + e: (json['#e'] as List?)?.map((e) => e as String).toList(), + p: (json['#p'] as List?)?.map((e) => e as String).toList(), + since: json['since'] as int?, + until: json['until'] as int?, + limit: json['limit'] as int?, + ); + +Map _$FilterToJson(Filter instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('ids', instance.ids); + writeNotNull('authors', instance.authors); + writeNotNull('kinds', instance.kinds); + writeNotNull('#e', instance.e); + writeNotNull('#p', instance.p); + writeNotNull('since', instance.since); + writeNotNull('until', instance.until); + writeNotNull('limit', instance.limit); + return val; +} diff --git a/pubspec.yaml b/pubspec.yaml index 7619ab9..f35c3a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,9 +7,12 @@ environment: sdk: '>=2.18.5 <3.0.0' dev_dependencies: + build_runner: ^2.3.0 lints: ^2.0.0 test: ^1.16.0 + json_serializable: ^6.1.3 dependencies: bip340: ^0.0.4 convert: ^3.1.1 crypto: ^3.0.2 + json_annotation: ^4.8.0 diff --git a/test/event_test.dart b/test/event_test.dart index 50586cc..346442f 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -106,7 +106,7 @@ void main() { Map json = { "pubkey": "9be7376ef6c0d493235ddf9018ff675a04bfcaf34dc1f97a1d270470f28c0ae0", - "content": "How does this work? 👀", + "content": "How does this work? 👀", "id": "883334badc17315fc61f0a13eec84c8c87df9d504ce0788f5eeab4a3527ddc97", "created_at": 1672477967, @@ -128,6 +128,7 @@ void main() { ] ] }; + Event event = Event.fromJson(json); Map toJson = event.toJson(); expect(toJson, json); @@ -150,6 +151,7 @@ void main() { "246970954e7b74e7fe381a4c818fed739ee59444cb536dadf45fbbce33bd7455ae7cd678c347c4a0c6e0a4483d18c7e26b7abe76f4cc73234f774e0e0d65204b", } ]; + var serializedWithSubscriptionId = [ "EVENT", "subscription_id", @@ -202,7 +204,7 @@ void main() { "052acd328f1c1d48e86fff3e34ada4bfc60578116f4f68f296602530529656a2" ] ], - "content": "How does this work? 👀", + "content": "How does this work? 👀", "sig": "3ce34915e90505f9760a463eb8f9e8b1748fd4c10c4cfddc09a2930ecce249ce8dd499eeffd6e24a215bb2f8265b68085f7104eb7d506f8d9b76a7c5312b09fd", } @@ -240,8 +242,11 @@ void main() { }); test('Generated from decoded json', () { - Event event = Event.fromJson(jsonDecode( - '{"kind": 1, "pubkey":"0ba0206887bd61579bf65ec09d7806bea32c64be1cf2c978cf031a811cd238db","content": "dart-nostr","tags": [["p","052acd328f1c1d48e86fff3e34ada4bfc60578116f4f68f296602530529656a2",""]],"created_at": 1672477962,"sig":"246970954e7b74e7fe381a4c818fed739ee59444cb536dadf45fbbce33bd7455ae7cd678c347c4a0c6e0a4483d18c7e26b7abe76f4cc73234f774e0e0d65204b","id": "047663d895d56aefa3f528935c7ce7dc8939eb721a0ec76ef2e558a8257955d2"}')); + Event event = Event.fromJson( + jsonDecode( + '{"id":"28da0ba726ace0541719267975b25ebc1b31e2465436a1fb18440edc999bbab6","pubkey":"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245","created_at":1674982433,"kind":1,"tags":[["e","9aa92f2a3e22f635341de94dce3bc2d93b80ea3af2eef4ce3ad224d7cf66c8b9",""],["e","33a5d8547fad29d509c735f2d56261d73c7437498802afe30d680fbaf473653a"],["p","0b118e40d6f3dfabb17f21a94a647701f140d8b063a9e84fe6e483644edc09cb"]],"content":"yeah it seems arbitrary at this point","sig":"89a2872b667abae8638e3ac346e6f0b3f5c239005beb71de8df03db730d13b4ed107920b77ef0cfb32a93fd08c50684d3ca14111381d17c10f4c36af45e7644a"}', + ), + ); expect(event.tags[0][2], equals("")); }); } diff --git a/test/message_test.dart b/test/message_test.dart index b8f32fd..b7d23fb 100644 --- a/test/message_test.dart +++ b/test/message_test.dart @@ -5,7 +5,7 @@ void main() { group('Message', () { test('EVENT', () { String payload = - '["EVENT","3979053091133091",{"id":"a60679692533b308f1d862c2a5ca5c08a304e5157b1df5cde0ff0454b9920605","pubkey":"7c579328cf9028a4548d5117afa4f8448fb510ca9023f576b7bc90fc5be6ce7e","created_at":1674405882,"kind":1,"tags":[],"content":"GM gm gm! Currently bathing my brain in coffee ☕️ hahaha. How many other nostrinos love coffee? 🤪🤙","sig":"10262aa6a83e0b744cda2097f06f7354357512b82846f6ef23ef7d997136b64815c343b613a0635a27da7e628c96ac2475f66dd72513c1fb8ce6560824eb25b8"}]'; + '["EVENT","3979053091133091",{"id":"a60679692533b308f1d862c2a5ca5c08a304e5157b1df5cde0ff0454b9920605","kind":1,"pubkey":"7c579328cf9028a4548d5117afa4f8448fb510ca9023f576b7bc90fc5be6ce7e","created_at":1674405882,"content":"GM gm gm! Currently bathing my brain in coffee ☕️ hahaha. How many other nostrinos love coffee? 🤪🤙","tags":[],"sig":"10262aa6a83e0b744cda2097f06f7354357512b82846f6ef23ef7d997136b64815c343b613a0635a27da7e628c96ac2475f66dd72513c1fb8ce6560824eb25b8"}]'; var msg = Message.deserialize(payload); expect(msg.type, "EVENT"); expect(msg.message.id,