import 'dart:io'; import 'dart:convert'; import 'dart:math'; import 'package:bip340/bip340.dart'; import 'package:intl/intl.dart'; import 'package:nostr_console/tree_ds.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'; String getStrInColor(String s, String commentColor) => stdout.supportsAnsiEscapes ?"$commentColor$s$gColorEndMarker":s; void printInColor(String s, String commentColor) => stdout.supportsAnsiEscapes ?stdout.write("$commentColor$s$gColorEndMarker"):stdout.write(s); void printWarning(String s) => stdout.supportsAnsiEscapes ?stdout.write("$gWarningColor$s$gColorEndMarker\n"):stdout.write("$s\n"); // 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; // used 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 // returns the immediate kind 1 parent String getParent(Map allEventsMap) { if( eTags.isNotEmpty) { for( int i = eTags.length - 1; i >= 0; i--) { String eventId = eTags[i]; if( allEventsMap[eventId]?.event.eventData.kind == 1) { String? parentId = allEventsMap[eventId]?.event.eventData.id; if( parentId != null) { return parentId; } } else { // if first e tag ( from end, which is the immediate parent) does not exist in the store, then return that eventID still. // Child comment would get a dummy parent, and called could then fetch that event return eventId; } } } 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 || eKind == 140 || eKind == 141 || eKind == 142) { for( int i = 0; i < numTags; i++) { var tag = jsonTags[i]; 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( gDebug > 0 && 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( id == gCheckEventId) { printInColor("in expandMentions: decoding ee810ea73072af056cceaa6d051b4fcce60739247f7bcc752e72fa5defb64f09\n", redColor); } 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(List directRooms, Map tempChildEventsMap) { if( id == gCheckEventId) { printInColor("in translateAndExpandMensitons: decoding ee810ea73072af056cceaa6d051b4fcce60739247f7bcc752e72fa5defb64f09\n", redColor); } if (content == "" || evaluatedContent != "") { if( id == gCheckEventId) { printInColor("in translateAndExpandMensitons: returning \n", redColor); } 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.")} ) .onError((error, stackTrace) { if( gDebug > 0) print("Translate error = $error\n for content = $content\n"); return {} ; } ); } 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; } if( id == gCheckEventId) { printInColor("in translateAndExpandMensitons: gonna decrypt \n", redColor); } String? decrypted = decryptDirectMessage(); if( decrypted != null) { evaluatedContent = decrypted; evaluatedContent = expandMentions(evaluatedContent); } break; } // end switch } // end translateAndExpandMentions // is called only once for each event received ( or read from file) void translateAndExpand14x(List directRooms, List encryptedChannels, Map tempChildEventsMap) { if( id == gCheckEventId) { printInColor("in translateAndExpandMensitons: decoding ee810ea73072af056cceaa6d051b4fcce60739247f7bcc752e72fa5defb64f09\n", redColor); } if (content == "" || evaluatedContent != "") { if( id == gCheckEventId) { printInColor("in translateAndExpandMensitons: returning \n", redColor); } return; } switch(kind) { case 142: String? decrypted = decryptEncryptedChannelMessage(directRooms, encryptedChannels, tempChildEventsMap); if( decrypted != null) { evaluatedContent = decrypted; evaluatedContent = expandMentions(evaluatedContent); } break; default: break; } // end switch } // end translateAndExpand14x String? decryptDirectMessage() { 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) printInColor(" in translateAndExpand: got event $id with number of p tags != one : $numPtags . not decrypting", redColor); return null; } } var decrypted = myPrivateDecrypt( userKey, otherUserPubKey, enc_str, iv); // use bob's privatekey and alic's publickey means bob can read message from alic return decrypted; } Channel? getChannelForMessage(List? listChannel, String messageId) { if( listChannel == null) { return null; } for(int i = 0; i < listChannel.length; i++) { if( listChannel[i].messageIds.contains(messageId)) { return listChannel[i]; } } return null; } String? decryptEncryptedChannelMessage(List directRooms, List encryptedChannels,Map tempChildEventsMap) { Channel? channel = getChannelForMessage( encryptedChannels, id); if( channel == null) { print("could not find channel"); return null; } if(!channel.participants.contains(userPublicKey)) { return null; } if(!channel.participants.contains(pubkey)) { return null; } if( id == "865c9352de11a3959c06fce5350c5a1b9fa0475d3234078a1bb45d152b370f0b") { // known issue //print("\n\ngoing to decrypt b1ab66ac50f00f3c3bbc91e5b9e03fc8e79e3fdb9f6d5c9ae9777aa6ca3020a2"); //print(channel.participants); return ""; } //print("Going to decrypt event id: $id"); //print("In decryptEncryptedChannelMessage: for event of kind 142 with event id = $id"); int ivIndex = content.indexOf("?iv="); var iv = content.substring( ivIndex + 4, content.length); var enc_str = content.substring(0, ivIndex); String channelId = getChannelIdForMessage(); //print("In decryptEncryptedChannelMessage: got channel id $channelId"); List keys = []; keys = getEncryptedChannelKeys(directRooms, tempChildEventsMap, channelId); if( keys.length != 2) { printInColor("Could not get keys for event id: $id and channelId: $channelId", redColor); return ""; } //print("\nevent id: $id"); //print(keys); String priKey = keys[0]; String pubKey = "02" + keys[1]; var decrypted = myPrivateDecrypt( priKey, pubKey, enc_str, iv); // use bob's privatekey and alic's publickey means bob can read message from alic return decrypted; } // only applicable for kind 42/142 event; returns the channel 40/140 id of which the event is part of String getChannelIdForMessage() { if( kind != 42 && kind != 142 && kind!=141) { return ""; } //print("in getChannelIdForMessage tag length = ${tags.length}"); // get first e tag, which should be the channel of which this is part of for( int i = 0; i < tags.length; i++) { List tag = tags[i]; if( tag[0] == 'e') { return tag[1]; } } return ''; } // prints event data in the format that allows it to be shown in tree form by the Tree class void printEventData(int depth, bool topPost) { 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); String name = getAuthorName(pubkey); String strDate = getPrintableDate(createdAt); String tempEvaluatedContent = evaluatedContent; String tempContent = content; if( isHidden) { name = ""; strDate = ""; tempEvaluatedContent = tempContent = "