import 'dart:io'; import 'dart:convert'; import 'dart:math'; import 'package:intl/intl.dart'; import 'package:translator/translator.dart'; import 'package:crypto/crypto.dart'; import 'package:nostr_console/settings.dart'; import "dart:typed_data"; import 'dart:convert' as convert; import "package:pointycastle/export.dart"; import 'package:kepler/kepler.dart'; int gDebug = 0; // translate GoogleTranslator? translator; // initialized in main when argument given const int gNumTranslateDays = 2;// translate for this number of days bool gTranslate = false; // translate flag // Structure to store kind 0 event meta data, and kind 3 meta data for each user. Will have info from latest // kind 0 event and/or kind 3 event, both with their own time stamps. class UserNameInfo { int? createdAt; String? name, about, picture; int? createdAtKind3; Event ?latestContactEvent; UserNameInfo(this.createdAt, this.name, this.about, this.picture, this.latestContactEvent, [this.createdAtKind3 = null]); } /* * global user names from kind 0 events, mapped from public key to a 3 element array of [name, about, picture] * JSON object {name: , about: , picture: } * only has info from latest kind 0 event */ Map gKindONames = {}; // global reactions entry. Map of form // reach Reactor is a list of 2-elements ( first is public id of reactor event, second is comment) Map< String, List> > gReactions = {}; // global contact list of each user, including of the logged in user. // maps from pubkey of a user, to the latest contact list of that user, which is the latest kind 3 message // is updated as kind 3 events are received Map< String, List> gContactLists = {}; class EventData { String id; String pubkey; int createdAt; int kind; String content; List eTags;// e tags List pTags;// list of p tags for kind:1 List> tags; bool isNotification; // whether its to be highlighted using highlight color String evaluatedContent; // content which has mentions expanded, and which has been translated Set newLikes; // user for notifications, are colored as notifications and then reset List contactList = []; // used for kind:3 events, which is contact list event bool isHidden; // hidden by sending a reaction kind 7 event to this event, by the logged in user bool isDeleted; // deleted by kind 5 event String getParent() { if( eTags.isNotEmpty) { return eTags[eTags.length - 1]; } return ""; } EventData(this.id, this.pubkey, this.createdAt, this.kind, this.content, this.eTags, this.pTags, this.contactList, this.tags, this.newLikes, {this.isNotification = false, this.evaluatedContent = "", this.isHidden = false, this.isDeleted = false}); factory EventData.fromJson(dynamic json) { List contactList = []; List eTagsRead = []; List pTagsRead = []; List> tagsRead = []; var jsonTags = json['tags']; var numTags = jsonTags.length; // NIP 02: if the event is a contact list type, then populate contactList if(json['kind'] == 3) { for( int i = 0; i < numTags; i++) { var tag = jsonTags[i]; if( tag.length < 2) { if( gDebug > 0) print("In event fromjson: invalid p tag of size 1"); continue; } String server = defaultServerUrl; if( tag.length >=3 ) { server = tag[2].toString(); if( server == 'wss://nostr.rocks' || server == "wss://nostr.bitcoiner.social") { server = defaultServerUrl; } } if( tag[0] == "p" && tag[1].length == 64) { Contact c = Contact(tag[1] as String, server); contactList.add(c); } } } else { int eKind = json['kind']; if ( eKind == 1 || eKind == 7 || eKind == 42 || eKind == 5 || eKind == 4) { for( int i = 0; i < numTags; i++) { var tag = jsonTags[i]; //stdout.write(tag); //print(tag.runtimeType); if( tag.isEmpty) { continue; } if( tag[0] == "e") { eTagsRead.add(tag[1]); } else { if( tag[0] == "p") { pTagsRead.add(tag[1]); } } List t = []; t.add(tag[0]); t.add(tag[1]); tagsRead.add(t); // TODO add other tags } } } if( json['id'] == gCheckEventId) { print("\n----------------------------------------Creating EventData with content: ${json['content']}"); print("In Event fromJson: got message: $gCheckEventId"); } return EventData(json['id'] as String, json['pubkey'] as String, json['created_at'] as int, json['kind'] as int, json['content'].trim() as String, eTagsRead, pTagsRead, contactList, tagsRead, {}); } String expandMentions(String content) { if( tags.isEmpty) { return content; } // just check if there is any square bracket in comment, if not we return String squareBracketStart = "[", squareBracketEnd = "]"; if( !content.contains(squareBracketStart) || !content.contains(squareBracketEnd) ) { return content; } // replace the patterns List placeHolders = ["#[0]", "#[1]", "#[2]", "#[3]", "#[4]", "#[5]", "#[6]", "#[7]" ]; for(int i = 0; i < placeHolders.length && i < tags.length; i++) { int index = -1; Pattern p = placeHolders[i]; if( (index = content.indexOf(p)) != -1 ) { if( tags[i].length >= 2) { String author = getAuthorName(tags[i][1]); content = "${content.substring(0, index)} @$author${content.substring(index + 4)}"; } } } return content; } // is called only once for each event received ( or read from file) void translateAndExpandMentions() { if (content == "" || evaluatedContent != "") { return; } switch(kind) { case 1: case 42: evaluatedContent = expandMentions(content); if( translator != null && gTranslate && !evaluatedContent.isEnglish()) { if( gDebug > 0) print("found that this comment is non-English: $evaluatedContent"); // only translate for latest events if( DateTime.fromMillisecondsSinceEpoch(createdAt *1000).compareTo( DateTime.now().subtract(Duration(days:gNumTranslateDays)) ) > 0 ) { if( gDebug > 0) print("Sending google request: translating $content"); if( translator != null) { try { translator?.translate(content, to: 'en') .then( (result) => { evaluatedContent = "$evaluatedContent\n\nTranslation: ${result.toString()}" , if( gDebug > 0) print("Google translate returned successfully for one call.")} ); } on Exception catch(err) { if( gDebug >= 0) print("Info: Error in trying to use google translate: $err"); } } } } break; case 4: if( userPrivateKey == ""){ // cant process if private key not given break; } //if( pubkey == userPublicKey ) break; // crashes right now otherwise if(!isValidDirectMessage(this)) { break; } String? decrypted = decryptContent(); if( decrypted != null) { evaluatedContent = decrypted; } break; } // end switch } // end translateAndExpandMentions String? decryptContent() { int ivIndex = content.indexOf("?iv="); var iv = content.substring( ivIndex + 4, content.length); var enc_str = content.substring(0, ivIndex); String userKey = userPrivateKey ; String otherUserPubKey = "02" + pubkey; if( pubkey == userPublicKey) { // if user themselve is the sender change public key used to decrypt userKey = userPrivateKey; int numPtags = 0; tags.forEach((tag) { if(tag[0] == "p" ) { otherUserPubKey = "02" + tag[1]; numPtags++; } }); // if there are more than one p tags, we don't know who its for if( numPtags != 1) { if( gDebug >= 0) print(" in translateAndExpand: got event $id with number of p tags != one : $numPtags . not decrypting"); return null; } } //print("going to decrypt eventid : $id to be decrypted content: $enc_str"); //print("original message: $content"); var decrypted = myPrivateDecrypt( userKey, otherUserPubKey, enc_str, iv); // use bob's privatekey and alic's publickey means bob can read message from alic //print("decrypted: $evaluatedContent\n---------------"); return decrypted; } // only applicable for kind 42 event String getChatRoomId() { if( kind != 42) { return ""; } return getParent(); } // prints event data in the format that allows it to be shown in tree form by the Tree class void printEventData(int depth) { if( !(kind == 1 || kind == 4 || kind == 42)) { return; // only print kind 1 and 42 and 4 } int n = 4; String maxN(String v) => v.length > n? v.substring(0,n) : v.substring(0, v.length); void printInColor(String s, String commentColor) => stdout.supportsAnsiEscapes ?stdout.write("$commentColor$s$gColorEndMarker"):stdout.write(s); String getStrInColor(String s, String commentColor) => stdout.supportsAnsiEscapes ?"$commentColor$s$gColorEndMarker":s; String name = getAuthorName(pubkey); String strDate = getPrintableDate(createdAt); String tempEvaluatedContent = evaluatedContent; String tempContent = content; if( isHidden) { name = ""; strDate = ""; tempEvaluatedContent = tempContent = "