nostr_console/lib/event_ds.dart
Vishal f2356d85fa added more tests for url breaking
and improved logic too
2022-11-22 11:47:50 +05:30

1698 lines
56 KiB
Dart

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';
import 'package:http/http.dart' as http;
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
List<String> nip08PlaceHolders = ["#[0]", "#[1]", "#[2]", "#[3]", "#[4]", "#[5]", "#[6]", "#[7]", "#[8]", "#[9]" ];
// 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;
bool nip05Verified;
String nip05Id;
UserNameInfo(this.createdAt, this.name, this.about, this.picture, this.latestContactEvent, [this.createdAtKind3 = null, this.nip05Verified = false, this.nip05Id = ""]);
}
/*
* global user names from kind 0 events, mapped from public key to a 3 element array of [name, about, picture]
* JSON object {name: <username>, about: <string>, picture: <url, string>}
* only has info from latest kind 0 event
*/
Map<String, UserNameInfo> gKindONames = {};
// global reactions entry. Map of form <if of event reacted to, List of Reactors>
// reach Reactor is a list of 2-elements ( first is public id of reactor event, second is comment)
Map< String, List<List<String>> > 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<Contact>> gContactLists = {};
// returns tags as string that can be used to calculate event has. called from EventData constructor
String getStrTagsFromJson(dynamic json) {
String str = "";
int i = 0;
for( dynamic tag in json ) {
if( i != 0) {
str += ",";
}
str += "[";
int j = 0;
for(dynamic element in tag) {
if( j != 0) {
str += ",";
}
str += "\"${element.toString()}\"";
j++;
}
str += "]";
i++;
}
return str;
}
bool verifyEvent(dynamic json) {
return true;
gSpecificDebug = 0;
if(gSpecificDebug > 0) print("----\nIn verify event:");
String createdAt = json['created_at'].toString();
String strTags = getStrTagsFromJson(json['tags']);
//print("strTags = $strTags");
String id = json['id'];
String eventPubkey = json['pubkey'];
String strKind = json['kind'].toString();
String content = json['content'];
content = unEscapeChars( content);
String eventSig = json['sig'];
if( false) {
String calculatedId = getShaId(eventPubkey, createdAt.toString(), strKind, strTags, content);
bool verified = true;//verify( eventPubkey, calculatedId, eventSig);
if( !verified && !eventPubkey.startsWith("00")) {
if(gSpecificDebug > 0) printWarning("\nwrong sig event\nevent sig = $eventSig\nevent id = $id\ncalculated id = $calculatedId " );
if(gSpecificDebug > 0) print("Event: kind = $strKind\n");
//getShaId(eventPubkey, createdAt.toString(), strKind, strTags, content);
//print("$json");
//throw Exception();
} else {
if(gSpecificDebug > 0) printInColor("\nverified correct sig for event id $id\n", gCommentColor);
}
}
return true;
}
class EventData {
String id;
String pubkey;
int createdAt;
int kind;
String content;
List<List<String>> eTags;// e tags
List<String> pTags;// list of p tags for kind:1
List<List<String>> tags;
bool isNotification; // whether its to be highlighted using highlight color
String evaluatedContent; // content which has mentions expanded, and which has been translated
Set<String> newLikes; // used for notifications, are colored as notifications and then reset
List<Contact> 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
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
});
// returns the immediate kind 1 parent
String getParent(Map<String, Tree> allEventsMap) {
if( eTags.isNotEmpty) {
int numRoot = 0, numReply = 0;
String rootId = "", replyId = "";
for( int i = 0; i < eTags.length; i++) {
String eventId = eTags[i][0];
if( eTags[i].length >= 3) {
if( eTags[i][2].toLowerCase() == "root") {
numRoot++;
rootId = eventId;
} else {
if( eTags[i][2].toLowerCase() == "reply") {
numReply++;
replyId = eventId;
}
}
}
}
if( replyId.length > 0) {
if( numReply == 1) {
return replyId;
} else {
// if there are multiply reply's we can't tell which is which, so we return the one at top
if( replyId.length > 0) {
return replyId;
} else {
if( rootId.length > 0) {
return rootId;
}
}
}
} else {
if( rootId.length > 0) {
//printWarning("returning root id. no reply id found.");
return rootId;
}
}
// if reply/root tags don't work, then try to look for parent tag with the deprecated logic from NIP-10
if( gDebug > 0) log.info("using deprecated logic of nip10 for event id : $id");
for( int i = tags.length - 1; i >= 0; i--) {
if( tags[i][0] == "e") {
String eventId = tags[i][1];
// ignore this e tag if its mentioned in the body of the event
String placeholder = nip08PlaceHolders[i];
if( content.contains(placeholder)) {
continue;
}
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 "";
}
factory EventData.fromJson(dynamic json) {
List<Contact> contactList = [];
List<List<String>> eTagsRead = [];
List<String> pTagsRead = [];
List<List<String>> tagsRead = [];
var jsonTags = json['tags'];
var numTags = jsonTags.length;
//print("\n----\nIn fromJson\n");
String sig = json['sig'];
if(sig.length == 128) {
//print("found sig == 128 bytes");
//if(json['id'] == "15dd45769dd0ccb9c4ca1c69fcd27011d53c4b95c8b7c786265bf7377bc7fdad") {
// printInColor("found 15dd45769dd0ccb9c4ca1c69fcd27011d53c4b95c8b7c786265bf7377bc7fdad sig ${json['sig']}", gCommentColor);
//}
try {
verifyEvent(json);
} on Exception catch(e) {
//printWarning("verify gave exception $e");
throw Exception("in Event constructor: sig verify gave exception");
}
}
// 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") {
List<String> listTag = [];
for(int i = 1; i < tag.length; i ++) {
listTag.add(tag[i]);
}
eTagsRead.add(listTag);
} else {
if( tag[0] == "p") {
pTagsRead.add(tag[1]);
}
}
List<String> 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, Map<String, Tree> tempChildEventsMap) {
if( id == gCheckEventId) {
printInColor("in expandMentions: decoding $gCheckEventId\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
for(int i = 0; i < nip08PlaceHolders.length && i < tags.length; i++) {
int index = -1;
Pattern p = nip08PlaceHolders[i];
if( (index = content.indexOf(p)) != -1 ) {
String mentionedId = tags[i][1];
if( tags[i].length >= 2) {
if( gKindONames.containsKey(mentionedId)) {
String author = getAuthorName(mentionedId);
content = "${content.substring(0, index)}@$author${content.substring(index + 4)}";
} else {
EventData? eventData = tempChildEventsMap[mentionedId]?.event.eventData??null;
if( eventData != null) {
String quotedAuthor = getAuthorName(eventData.pubkey);
String prefixId = mentionedId.substring(0, 3);
String quote = "<Quoted event id '$prefixId' by $quotedAuthor: \"${eventData.evaluatedContent}\">";
content = "${content.substring(0, index)}$quote${content.substring(index + 4)}";
}
}
}
}
}
return content;
}
// is called only once for each event received ( or read from file)
void translateAndExpandMentions(List<DirectMessageRoom> directRooms, Map<String, Tree> tempChildEventsMap) {
if( id == gCheckEventId) {
printInColor("in translateAndExpandMensitons: decoding $gCheckEventId\n", redColor);
}
if (content == "" || evaluatedContent != "") {
if( id == gCheckEventId) {
printInColor("in translateAndExpandMensitons: returning \n", redColor);
}
return;
}
switch(kind) {
case 1:
case 42:
evaluatedContent = expandMentions(content, tempChildEventsMap);
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, tempChildEventsMap);
}
break;
} // end switch
} // end translateAndExpandMentions
// is called only once for each event received ( or read from file)
void translateAndExpand14x(List<DirectMessageRoom> directRooms, List<Channel> encryptedChannels, Map<String, Tree> 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, tempChildEventsMap);
}
break;
default:
break;
} // end switch
} // end translateAndExpand14x
String? decryptDirectMessage() {
int ivIndex = content.indexOf("?iv=");
if( ivIndex > 0) {
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;
} else {
if(gDebug > 0) print("Invalid content for dm, could not get ivIndex: $content");
return null;
}
}
Channel? getChannelForMessage(List<Channel>? 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<DirectMessageRoom> directRooms, List<Channel> encryptedChannels,Map<String, Tree> 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=");
if( ivIndex == -1) {
return "";
}
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<String> keys = [];
keys = getEncryptedChannelKeys(directRooms, tempChildEventsMap, channelId);
if( keys.length != 2) {
//print("Could not get keys for event id: $id and channelId: $channelId");
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 = "<hidden>";
strDate = "<hidden>";
tempEvaluatedContent = tempContent = "<You have hidden this post>";
}
// delete supercedes hidden
if( isDeleted) {
name = "<deleted>";
strDate = "<deleted>";
tempEvaluatedContent = tempContent = content; // content would be changed so show that
}
if( createdAt == 0) {
print("debug: createdAt == 0 for event $id $content");
}
String commentColor = "";
if( isNotification) {
commentColor = gNotificationColor;
isNotification = false;
} else {
commentColor = gCommentColor;
}
int tempEffectiveLen = name.length < gNameLengthInPost? name.length: gNameLengthInPost;
name = name.substring(0,tempEffectiveLen);
int effectiveNameFieldLen = gNameLengthInPost + 3; // get this before name is mangled by color
String nameColor = getNameColor(pubkey);
// pad name to left
name = name.padLeft(gNameLengthInPost);
name = name.substring(0, gNameLengthInPost);
name = getStrInColor(name, nameColor);
String strToPrint = "";
if(!topPost) {
strToPrint += "\n";
strToPrint += getDepthSpaces(depth);
strToPrint += " "; // in place of block for top posts
} else {
strToPrint += getDepthSpaces(depth);
strToPrint += "";
}
strToPrint += "${name}: ";
const int typicalxLen = "|id: 82b5 , 12:04 AM Sep 19".length + 5; // not sure where 5 comes from
String idDateLikes = " |id: ${maxN(id)} , $strDate ${getReactionStr(depth)}" ;
idDateLikes = idDateLikes.padRight(typicalxLen);
String temp = tempEvaluatedContent==""?tempContent: tempEvaluatedContent;
String contentShifted = makeParagraphAtDepth( temp, gSpacesPerDepth * depth + effectiveNameFieldLen);
int maxLineLen = gTextWidth - gSpacesPerDepth * depth - effectiveNameFieldLen ;
int lastLineLen = contentShifted.length;
int i = 0;
contentShifted = contentShifted.trim();
// find the effective length of the last line of the content
for(i = contentShifted.length - 1; i >= 0; i-- ) {
if( contentShifted[i] == "\n") {
break;
}
}
if( i >= 0 && contentShifted[i] == "\n") {
lastLineLen = contentShifted.length - i;
}
// effective len of last line is used to calcluate where the idDateLikes str is affixed at the end
int effectiveLastLineLen = lastLineLen - gSpacesPerDepth * depth - effectiveNameFieldLen - gNumLeftMarginSpaces;
if( contentShifted.length <= maxLineLen )
effectiveLastLineLen = contentShifted.length;
// now actually find where the likesDates string goes
if( (gSpacesPerDepth * depth + effectiveNameFieldLen + effectiveLastLineLen + idDateLikes.length ) <= gTextWidth) {
idDateLikes = idDateLikes.padLeft((gTextWidth ) - (gSpacesPerDepth * depth + effectiveNameFieldLen + effectiveLastLineLen));
} else {
idDateLikes = "\n" + idDateLikes.padLeft(gNumLeftMarginSpaces + gTextWidth);
}
// print content and the dateslikes string
strToPrint += getStrInColor(contentShifted + idDateLikes + "\n", commentColor);
stdout.write(strToPrint);
}
String getAsLine({int len = 20}) {
String contentToPrint = evaluatedContent.isEmpty? content: evaluatedContent;
if( len == 0 || len > contentToPrint.length) {
//len = contentToPrint.length;
}
contentToPrint = contentToPrint.replaceAll("\n", " ");
contentToPrint = contentToPrint.replaceAll("\r", " ");
contentToPrint = contentToPrint.replaceAll("\t", " ");
contentToPrint = contentToPrint.padRight(len).substring(0, len);
//contentToPrint = contentToPrint.padRight(len);
String strToPrint = '$contentToPrint - ${getAuthorName(pubkey).padLeft(12)}';
String paddedStrToPrint = strToPrint;
if( isNotification) {
paddedStrToPrint = "$gNotificationColor$paddedStrToPrint$gColorEndMarker";
isNotification = false;
}
return paddedStrToPrint;
}
String getStrForChannel(int depth) {
String strToPrint = "";
String name = getAuthorName(pubkey);
String strDate = getPrintableDate(createdAt);
String tempEvaluatedContent = evaluatedContent;
String tempContent = evaluatedContent.isEmpty? content: evaluatedContent;
if( isHidden) {
name = strDate = "<hidden>";
tempEvaluatedContent = tempContent = "<You have hidden this post>";
}
// delete supercedes hidden
if( isDeleted) {
name = strDate = "<deleted>";
tempEvaluatedContent = tempContent = content; // content would be changed so show that
}
if( tempEvaluatedContent=="")
tempEvaluatedContent = tempContent;
const int nameWidthDepth = 16~/gSpacesPerDepth; // how wide name will be in depth spaces
const int timeWidthDepth = 18~/gSpacesPerDepth;
int nameWidth = gSpacesPerDepth * nameWidthDepth;
// get name in color and pad it too
String nameToPrint = name.padLeft(nameWidth).substring(0, nameWidth);
nameToPrint = getStrInColor(nameToPrint, getNameColor(pubkey));
String dateToPrint = strDate.padLeft(gSpacesPerDepth * timeWidthDepth).substring(0, gSpacesPerDepth * timeWidthDepth);
// depth above + ( depth numberof spaces = 1) + (depth of time = 2) + (depth of name = 3)
int contentDepth = depth + 1 + timeWidthDepth + nameWidthDepth;
int magicNumberDepth6 = 2; // magic number for gSpacesPerDepth == 6
int finalContentDepthInSpaces = gSpacesPerDepth * contentDepth + magicNumberDepth6;
int contentPlacementColumn = finalContentDepthInSpaces + gNumLeftMarginSpaces;
String contentShifted = makeParagraphAtDepth(tempEvaluatedContent, finalContentDepthInSpaces);
Event? replyToEvent = getReplyToEvent();
String strReplyTo = "";
if( replyToEvent != null) {
//print("in getStrForChannel: got replyTo id = ${replyToEvent.eventData.id}");
if( replyToEvent.eventData.kind == 1 || replyToEvent.eventData.kind == 42 || replyToEvent.eventData.kind == 142) { // make sure its a kind 1 or 40 message
if( replyToEvent.eventData.id != id) { // basic self test
strReplyTo = 'In reply to:"${replyToEvent.eventData.evaluatedContent}"';
strReplyTo = makeParagraphAtDepth(strReplyTo, finalContentDepthInSpaces + 6); // one extra for content
// add reply to string to end of the content. How it will show:
contentShifted += ( "\n" + getNumSpaces( contentPlacementColumn + gSpacesPerDepth) + strReplyTo);
}
}
} else {
//printWarning("no reply to event for event id $id");
}
String msgId = id.substring(0, 3).padLeft(gSpacesPerDepth~/2).padRight(gSpacesPerDepth) ;
if( isNotification) {
strToPrint = "$gNotificationColor${getDepthSpaces(depth-1)}$msgId $dateToPrint $nameToPrint: $gNotificationColor" + contentShifted + gColorEndMarker;
isNotification = false;
} else {
strToPrint = "${getDepthSpaces(depth-1)}$msgId $dateToPrint $nameToPrint: " + contentShifted;
}
return strToPrint;
}
// looks up global map of reactions, if this event has any reactions, and then prints the reactions
// in appropriate color( in case one is a notification, which is stored in member variable)
String getReactionStr(int depth) {
String reactorNames = "";
if( isHidden || isDeleted) {
return "";
}
if( gReactions.containsKey(id)) {
reactorNames = "Likes: ";
int numReactions = gReactions[id]?.length??0;
List<List<String>> reactors = gReactions[id]??[];
bool firstEntry = true;
for( int i = 0; i <numReactions; i++) {
String comma = (firstEntry)?"":", ";
String reactorId = reactors[i][0];
if( newLikes.contains(reactorId) && reactors[i][1] == "+") {
// this is a notifications, print it and then later empty newLikes
reactorNames += comma + gNotificationColor + getAuthorName(reactorId) + gColorEndMarker;
firstEntry = false;
} else {
// this is normal printing of the reaction. only print for + for now
if( reactors[i][1] == "+")
reactorNames += comma + getAuthorName(reactorId);
firstEntry = false;
}
} // end for
newLikes.clear();
reactorNames += "";
}
return reactorNames;
}
// returns the last e tag as reply to event for kind 42 and 142 events
Event? getReplyToEvent() {
for(int i = tags.length - 1; i >= 0; i--) {
List tag = tags[i];
if( tag[0] == 'e') {
String replyToEventId = tag[1];
Event? eventInReplyTo = (gStore?.allChildEventsMap[replyToEventId]?.event)??null;
if( eventInReplyTo != null) {
if ( [1,42,142].contains( eventInReplyTo.eventData.kind)) {
return eventInReplyTo;
}
}
}
}
return null;
} // end getReplyToEvent()
}
// This is mostly a placeholder for EventData. TODO combine both?
class Event {
String event;
String id;
EventData eventData;
String originalJson;
List<String> seenOnRelays;
bool readFromFile;
Event(this.event, this.id, this.eventData, this.seenOnRelays, this.originalJson, [this.readFromFile = false]);
@override
bool operator ==( other) {
return (other is Event) && eventData.id == other.eventData.id;
}
factory Event.fromJson(String d, String relay, [bool fromFile = false]) {
try {
dynamic json = jsonDecode(d);
if( json.length < 3) {
String e = "";
e = json.length > 1? json[0]: "";
if( gDebug > 0) {
print("Could not create event. json.length = ${json.length} string d= $d $e");
}
throw Exception("Event json has less than 3 elements");
}
EventData newEventData = EventData.fromJson(json[2]);
if( !fromFile)
newEventData.isNotification = true;
return Event(json[0] as String, json[1] as String, newEventData, [relay], d, fromFile );
} on Exception catch(e) {
if( gDebug> 0) {
print("Could not create event. $e");
print("problem str: $d\n");
}
//print("Caught an exception in Event.fromJson");
throw e;
}
}
void printEvent(int depth, bool topPost) {
eventData.printEventData(depth, topPost);
//stdout.write("\n$originalJson --------------------------------\n\n");
}
@override
String toString() {
return '$eventData Seen on: ${seenOnRelays[0]}\n';
}
}
void addToHistogram(Map<String, int> histogram, List<String> pTags) {
Set tempPtags = {};
pTags.retainWhere((x) => tempPtags.add(x));
for(int i = 0; i < pTags.length; i++ ) {
String pTag = pTags[i];
if( histogram.containsKey(pTag)) {
int? val = histogram[pTag];
if( val != null) {
histogram[pTag] = ++val;
} else {
}
} else {
histogram[pTag] = 1;
}
}
//return histogram;
}
class HistogramEntry {
String str;
int count;
HistogramEntry(this.str, this.count);
static int histogramSorter(HistogramEntry a, HistogramEntry b) {
if( a.count < b.count ) {
return 1;
} if( a.count == b.count ) {
return 0;
} else {
return -1;
}
}
}
// return the numMostFrequent number of most frequent p tags ( user pubkeys) in the given events
List<String> getpTags(Set<Event> events, int numMostFrequent) {
List<HistogramEntry> listHistogram = [];
Map<String, int> histogramMap = {};
for(var event in events) {
addToHistogram(histogramMap, event.eventData.pTags);
}
histogramMap.forEach((key, value) {listHistogram.add(HistogramEntry(key, value));/* print("added to list of histogramEntry $key $value"); */});
listHistogram.sort(HistogramEntry.histogramSorter);
List<String> ptags = [];
for( int i = 0; i < listHistogram.length && i < numMostFrequent; i++ ) {
ptags.add(listHistogram[i].str);
}
return ptags;
}
// From the list of events provided, lookup the lastst contact information for the given user/pubkey
Event? getContactEvent(String pubkey) {
// get the latest kind 3 event for the user, which lists his 'follows' list
if( gKindONames.containsKey(pubkey)) {
Event? e = (gKindONames[pubkey]?.latestContactEvent)??null;
return e;
}
return null;
}
// for the user userPubkey, returns the relay of its contact contactPubkey
String getRelayOfUser(String userPubkey, String contactPubkey) {
if(gDebug > 0) print("In getRelayOfUser: Searching relay for contact $contactPubkey" );
String relay = "";
if( userPubkey == "" || contactPubkey == "") {
return "";
}
if( gContactLists.containsKey(userPubkey)) {
List<Contact>? contacts = gContactLists[userPubkey];
if( contacts != null) {
for( int i = 0; i < contacts.length; i++) {
if( contacts[i].id == contactPubkey) {
relay = contacts[i].relay;
return relay;
}
}
}
}
// if not found return empty string
return relay;
}
Future<http.Response> fetchNip05Info(String nip05Url) {
//print("in fetch nip: url = $nip05Url");
var retval = http.get(Uri.parse(nip05Url));
return retval;
}
// If given event is kind 0 event, then populates gKindONames with that info
// returns true if entry was created or modified, false otherwise
bool processKind0Event(Event e) {
if( e.eventData.kind != 0) {
return false;
}
String content = e.eventData.content;
if( content.isEmpty) {
return false;
}
String name = "";
String about = "";
String picture = "";
String nip05 = "";
try {
dynamic json = jsonDecode(content);
name = json["name"]??"";
about = json["about"]??"";
picture = json["picture"]??"";
nip05 = json['nip05']??"";
//String twitterId = json['twitter']??"";
//String githubId = json['github']??"";
} catch(ex) {
if( gDebug > 0) print("Error in processKind0Event: $ex for pubkey: ${e.eventData.pubkey}");
}
bool newEntry = false, entryModified = false;
if( !gKindONames.containsKey(e.eventData.pubkey)) {
gKindONames[e.eventData.pubkey] = UserNameInfo(e.eventData.createdAt, name, about, picture, e);
newEntry = true;;
} else {
int oldTime = gKindONames[e.eventData.pubkey]?.createdAt??0;
if( oldTime < e.eventData.createdAt) {
gKindONames[e.eventData.pubkey] = UserNameInfo(e.eventData.createdAt, name, about, picture, e);
entryModified = true;
}
}
if(gDebug > 0) {
print("At end of processKind0Events: for name = $name ${newEntry? "added entry": ( entryModified?"modified entry": "No change done")} ");
}
bool localDebug = false; //e.eventData.pubkey == "9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437"? true: false;
if( newEntry || entryModified) {
if(nip05.length > 0) {
List<String> urlSplit = nip05.split("@");
if( urlSplit.length == 2) {
String urlNip05 = urlSplit[1] + "/.well-known/nostr.json?name=" + urlSplit[0];
if( !urlNip05.startsWith("http")) {
urlNip05 = "http://"+ urlNip05;
}
fetchNip05Info(urlNip05)
.then((httpResponse) {
if( localDebug ) print("-----\nnip future for $urlNip05 returned body ${httpResponse.body}");
var namesInResponse;
try {
dynamic json = jsonDecode(httpResponse.body);
namesInResponse = json["names"];
if( namesInResponse.length > 0) {
for(var returntedName in namesInResponse.keys) {
if( returntedName == urlSplit[0] && namesInResponse[returntedName] == e.eventData.pubkey) {
int oldTime = 0;
if( !gKindONames.containsKey(e.eventData.pubkey)) {
//printWarning("in response handing. creating user info");
gKindONames[e.eventData.pubkey] = UserNameInfo(e.eventData.createdAt, name, about, picture, e);
} else {
oldTime = gKindONames[e.eventData.pubkey]?.createdAt??0;
//print("in response handing. user info exists with old time = $oldTime and this event time = ${e.eventData.createdAt}");
}
if( oldTime <= e.eventData.createdAt ) {
gKindONames[e.eventData.pubkey]?.nip05Verified = true;
gKindONames[e.eventData.pubkey]?.nip05Id = nip05;
}
return;
}
}
} else {
//print("names = 0");
}
} catch(ex) {
}
})
.catchError((e){if( gDebug > 0) print('in fetch nip caught error $e for url $urlNip05');});
}
}
}
return newEntry || entryModified;
}
// If given event is kind 3 event, then populates gKindONames with contact info
// returns true if entry was created or modified, false otherwise
bool processKind3Event(Event newContactEvent) {
if( newContactEvent.eventData.kind != 3) {
return false;
}
bool newEntry = false, entryModified = false;
if( !gKindONames.containsKey(newContactEvent.eventData.pubkey)) {
gKindONames[newContactEvent.eventData.pubkey] = UserNameInfo(null, null, null, null, newContactEvent, newContactEvent.eventData.createdAt);
newEntry = true;;
} else {
// if entry already exists, then check its old time and update only if we have a newer entry now
int oldTime = gKindONames[newContactEvent.eventData.pubkey]?.createdAtKind3??0;
if( oldTime < newContactEvent.eventData.createdAt) {
int? createdAt = gKindONames[newContactEvent.eventData.pubkey]?.createdAt??null;
String? name = gKindONames[newContactEvent.eventData.pubkey]?.name, about = gKindONames[newContactEvent.eventData.pubkey]?.about, picture = gKindONames[newContactEvent.eventData.pubkey]?.picture;
gKindONames[newContactEvent.eventData.pubkey] = UserNameInfo(createdAt, name, about, picture, newContactEvent, newContactEvent.eventData.createdAt );
entryModified = true;;
}
}
if(gDebug > 0) {
print("At end of processKind3Events: ${newEntry? "added entry": ( entryModified?"modified entry": "No change done")} ");
}
return newEntry || entryModified;
}
String getNip05Name( String pubkey) {
String nip05name = "";
if( gKindONames[pubkey]?.name == null || gKindONames[pubkey]?.name?.length == 0) {
nip05name = "";
}
else {
String name = gKindONames[pubkey]?.name??"";
if( gKindONames[pubkey]?.nip05Verified??false) {
nip05name = "$name (nip05: ${gKindONames[pubkey]?.nip05Id??""})";
} else {
nip05name = name;
}
}
return nip05name;
}
// returns name by looking up global list gKindONames, which is populated by kind 0 events
String getAuthorName(String pubkey, [int len = 3]) {
String max3(String v) => v.length > len? v.substring(0,len) : v.substring(0, v.length);
String name = "";
if( gKindONames[pubkey]?.name == null || gKindONames[pubkey]?.name?.length == 0)
name = max3(pubkey);
else {
name = (gKindONames[pubkey]?.name)??max3(pubkey);
}
return name;
}
// returns full public key(s) for the given username( which can be first few letters of pubkey, or the user name)
Set<String> getPublicKeyFromName(String inquiredName) {
if( inquiredName.length < 1) {
return {};
}
Set<String> pubkeys = {};
gKindONames.forEach((pubkey, userInfo) {
// check both the user name, and the pubkey to search for the user
// check username
if( userInfo.name != null) {
int minNameLen = min( inquiredName.length, (userInfo.name?.length)??0);
if( inquiredName.toLowerCase() == userInfo.name?.substring(0, minNameLen).toLowerCase()) {
pubkeys.add(pubkey);
}
}
// check public key
if( inquiredName.length >= 2 && inquiredName.length <= pubkey.length) {
if( pubkey.substring(0, inquiredName.length) == inquiredName) {
pubkeys.add(pubkey);
}
}
});
return pubkeys;
}
// returns the seconds since eponch N days ago
int getSecondsDaysAgo( int N) {
return DateTime.now().subtract(Duration(days: N)).millisecondsSinceEpoch ~/ 1000;
}
void printUnderlined(String x) => { stdout.write("$x\n${getNumDashes(x.length)}\n")};
void printDepth(int d) {
for( int i = 0; i < gSpacesPerDepth * d + gNumLeftMarginSpaces; i++) {
stdout.write(" ");
}
}
void printCenteredHeadline(displayName) {
int numDashes = 10; // num of dashes on each side
int startText = gNumLeftMarginSpaces + ( gTextWidth - (displayName.length + 2 * numDashes)) ~/ 2;
if( startText < 0)
startText = 0;
String str = getNumSpaces(startText) + getNumDashes(numDashes) + displayName + getNumDashes(numDashes);
print(str);
}
String getDepthSpaces(int d) {
String str = "";
for( int i = 0; i < gSpacesPerDepth * d + gNumLeftMarginSpaces; i++) {
str += " ";
}
return str;
}
String getNumSpaces(int num) {
String s = "";
for( int i = 0; i < num; i++) {
s += " ";
}
return s;
}
String getNumDashes(int num, [String dashType = "-"]) {
String s = "";
for( int i = 0; i < num; i++) {
s += dashType;
}
return s;
}
List<List<int>> getUrlRanges(String s) {
List<List<int>> urlRanges = [];
String regexp1 = "http[s]*:\/\/[a-zA-Z0-9]+([.a-zA-Z0-9/_\\-\\#\\+=\\&\\?]*)";
RegExp httpRegExp = RegExp(regexp1);
for( var match in httpRegExp.allMatches(s) ) {
List<int> entry = [match.start, match.end];
urlRanges.add(entry);
}
return urlRanges;
}
// make a paragraph of s that starts at numSpaces ( from screen left), and does not extend beyond gTextWidth+gNumLeftMarginSpaces. break it, or add
// a newline if it goes beyond gTextWidth + gNumLeftMarginSpaces
String makeParagraphAtDepth(String s, int depthInSpaces) {
List<List<int>> urlRanges = getUrlRanges(s);
String newString = "";
String spacesString = getNumSpaces(depthInSpaces + gNumLeftMarginSpaces);
int lenPerLine = gTextWidth - depthInSpaces;
//print("In makeParagraphAtDepth: gNumLeftMarginSpaces = $gNumLeftMarginSpaces depthInSPaces = $depthInSpaces LenPerLine = $lenPerLine gTextWidth = $gTextWidth ");
for(int startIndex = 0; startIndex < s.length; ) {
List listCulledLine = getLineWithMaxLen(s, startIndex, lenPerLine, spacesString, urlRanges);
String line = listCulledLine[0];
int lenReturned = listCulledLine[1] as int;
//print("In makeParagraphAtDepth: line len = ${line.length} lenReturned = $lenReturned");
if( line.length == 0 || lenReturned == 0) break;
newString += line;
startIndex += lenReturned;
}
return newString;
}
// returns true if n is in any of the ranges given in list
int isInRange( int n, List<List<int>> ranges ) {
for( int i = 0; i < ranges.length; i++) {
if( n >= ranges[i][0] && n < ranges[i][1]) {
return ranges[i][1];
}
}
return 0;
}
// returns from string[startIndex:] the first len number of chars. no newline is added.
List getLineWithMaxLen(String s, int startIndex, int lenPerLine, String spacesString, List<List<int>> urlRanges) {
if( startIndex >= s.length)
return ["", 0];
String line = ""; // is returned
// if length required is greater than the length of string remaing, return whatever remains
int numCharsInLine = 0;
int i = startIndex;
// i indexes over the input line ( which is the whole comment)
for(; i < startIndex + lenPerLine && i < s.length; i++) {
line += s[i];
numCharsInLine ++;
if( s[i] == "\n") {
i++;
numCharsInLine = 0;
line += spacesString;
break;
}
}
int urlEnd = 0;
if( (urlEnd = isInRange(i, urlRanges)) != 0) {
line = line + s.substring(i, urlEnd);
i = urlEnd;
} else {
if( numCharsInLine > lenPerLine || ( (numCharsInLine == lenPerLine) && (s.length > startIndex + numCharsInLine) )) {
bool lineBroken = false;
// line is broken only if the returned line is the longest it can be, and
// if its length is greater than the gMaxLenBrokenWord constant
if( line.length >= lenPerLine && line.length > gMaxLenUnbrokenWord ) {
int i = line.length - 1;
// find a whitespace character
for( ; i > 0 && !isWordSeparater(line[i]); i--);
// for ended
if( line.length - i < gMaxLenUnbrokenWord) {
// break the line here if its a word separator
if( isWordSeparater(line[i])) {
int newLineStart = i + 1;
if( line[i] != ' ')
newLineStart = i;
line = line.substring(0, i) + "\n" + spacesString + line.substring(newLineStart, line.length);
lineBroken = true;
}
}
}
if( !lineBroken ) {
if( s.length > i ) {
line += "\n";
line += spacesString;
}
}
}
}
return [line, i - startIndex];
}
bool nonEnglish(String str) {
bool result = false;
return result;
}
bool isNumeric(String s) {
return double.tryParse(s) != null;
}
bool isWordSeparater(String s) {
if( s.length != 1) {
return false;
}
return s[0] == ' ' || s[0] == '\n' || s[0] == '\r' || s[0] == '\t'
|| s[0] == ',' || s[0] == '.' || s[0] == '-' || s[0] == '('|| s[0] == ')';
}
bool isWhitespace(String s) {
if( s.length != 1) {
return false;
}
return s[0] == ' ' || s[0] == '\n' || s[0] == '\r' || s[0] == '\t';
}
extension StringX on String {
int isChannelPageNumber(int max) {
if(this.length < 2 || this[0] != '/') {
return 0;
}
String rest = this.substring(1);
//print("rest = $rest");
int? n = int.tryParse(rest);
if( n != null) {
if( n < max)
return n;
}
return 0;
}
isEnglish( ) {
// since smaller words can be smileys they should not be translated
if( length < 10)
return true;
if( !isLatinAlphabet())
return false;
if (isFrench())
return false;
return true;
}
isPortugese() {
false; // https://1000mostcommonwords.com/1000-most-common-portuguese-words/
}
bool isFrench() {
// https://www.thoughtco.com/most-common-french-words-1372759
List<String> frenchWords = ["oui", "je", "le", "un", "de", "et", "merci", "une", "ce", "pas"];
for( int i = 0; i < frenchWords.length; i++) {
if( this.toLowerCase().contains(" ${frenchWords[i]} ")) {
if( gDebug > 0) print("isFrench: Found ${this.toString()} is french");
return true;
}
}
return false;
}
isLatinAlphabet({caseSensitive = false}) {
int countLatinletters = 0;
for (int i = 0; i < length; i++) {
final target = caseSensitive ? this[i] : this[i].toLowerCase();
if ( (target.codeUnitAt(0) > 96 && target.codeUnitAt(0) < 123) || ( isNumeric(target) ) || isWhitespace(target)) {
countLatinletters++;
}
}
if( countLatinletters < ( 40.0/100 ) * length ) {
if( gDebug > 0) print("in isLatinAlphabet: latin letters: $countLatinletters and total = $length ");
return false;
} else {
return true;
}
}
}
// The contact only stores id and relay of contact. The actual name is stored in a global variable/map
class Contact {
String id, relay;
Contact(this.id, this.relay);
@override
String toString() {
return 'id: $id ( ${getAuthorName(id)}) relay: $relay';
}
}
String addEscapeChars(String str) {
String temp = "";
//temp = temp.replaceAll("\\", "\\\\");
temp = str.replaceAll("\"", "\\\"");
return temp.replaceAll("\n", "\\n");
}
String unEscapeChars(String str) {
//print("in unEscape: |$str|");
String temp = str.replaceAll("\"", "\\\"");
//temp = temp.replaceAll("\\\\", "\\");
temp = temp.replaceAll("\n", "\\n");
//print("returning |$temp|\n");
return temp;
}
String getShaId(String pubkey, String createdAt, String kind, String strTags, String content) {
String buf = '[0,"$pubkey",$createdAt,$kind,[$strTags],"$content"]';
if(gSpecificDebug > 0) print("in getShaId for buf: |$buf|");
var bufInBytes = utf8.encode(buf);
var value = sha256.convert(bufInBytes);
return value.toString();
}
// get printable date from seconds since epoch
String getPrintableDate(int createdAt) {
final df1 = DateFormat('hh:mm a');
final df2 = DateFormat(DateFormat.ABBR_MONTH_DAY);
String strDate = df1.format(DateTime.fromMillisecondsSinceEpoch(createdAt*1000));
strDate += " ${df2.format(DateTime.fromMillisecondsSinceEpoch(createdAt*1000))}";
return strDate;
}
/*
* Returns true if this is a valid direct message to just this user
*/
bool isValidDirectMessage(EventData directMessageData) {
bool validUserMessage = false;
List<String> allPtags = [];
directMessageData.tags.forEach((tag) {
if( tag.length < 2 )
return;
if( tag[0] == "p" && tag[1].length == 64) { // basic length sanity test
allPtags.add(tag[1]);
}
});
if( directMessageData.pubkey == userPublicKey && allPtags.length == 1) {
if( allPtags[0].substring(0, 32) != "0".padLeft(32, '0')) { // check that the message hasn't been sent to an invalid pubkey
validUserMessage = true; // case where this user is sender
}
} else {
if ( directMessageData.pubkey != userPublicKey) {
if( allPtags.length == 1 && allPtags[0] == userPublicKey) {
validUserMessage = true; // case where this user is recipeint
}
}
}
return validUserMessage;
}
String getRandomPrivKey() {
FortunaRandom fr = FortunaRandom();
final _sGen = Random.secure();;
fr.seed(KeyParameter(
Uint8List.fromList(List.generate(32, (_) => _sGen.nextInt(255)))));
BigInt randomNumber = fr.nextBigInteger(256);
String strKey = randomNumber.toRadixString(16);
if( strKey.length < 64) {
int numZeros = 64 - strKey.length;
for(int i = 0; i < numZeros; i++) {
strKey = "0" + strKey;
}
}
return strKey;
}
// pointy castle source https://github.com/PointyCastle/pointycastle/blob/master/tutorials/aes-cbc.md
// https://github.com/bcgit/pc-dart/blob/master/tutorials/aes-cbc.md
// 3 https://github.com/Dhuliang/flutter-bsv/blob/42a2d92ec6bb9ee3231878ffe684e1b7940c7d49/lib/src/aescbc.dart
/// Decrypt data using self private key
String myPrivateDecrypt( String privateString,
String publicString,
String b64encoded,
[String b64IV = ""]) {
Uint8List encdData = convert.base64.decode(b64encoded);
final rawData = myPrivateDecryptRaw(privateString, publicString, encdData, b64IV);
return convert.Utf8Decoder().convert(rawData.toList());
}
Map<String, List<List<int>>> gMapByteSecret = {};
Uint8List myPrivateDecryptRaw( String privateString,
String publicString,
Uint8List cipherText,
[String b64IV = ""]) {
try {
List<List<int>> byteSecret = [];
if( gMapByteSecret.containsKey(publicString)) {
byteSecret = gMapByteSecret[publicString]??[];
}
if( byteSecret.isEmpty) {
byteSecret = Kepler.byteSecret(privateString, publicString);;
gMapByteSecret[publicString] = byteSecret;
}
final secretIV = byteSecret;
final key = Uint8List.fromList(secretIV[0]);
final iv = b64IV.length > 6
? convert.base64.decode(b64IV)
: Uint8List.fromList(secretIV[1]);
CipherParameters params = new PaddedBlockCipherParameters(
new ParametersWithIV(new KeyParameter(key), iv), null);
PaddedBlockCipherImpl cipherImpl = new PaddedBlockCipherImpl(
new PKCS7Padding(), new CBCBlockCipher(new AESEngine()));
cipherImpl.init(false,
params as PaddedBlockCipherParameters<CipherParameters?,
CipherParameters?>);
final Uint8List finalPlainText = Uint8List(cipherText.length); // allocate space
var offset = 0;
while (offset < cipherText.length - 16) {
offset += cipherImpl.processBlock(cipherText, offset, finalPlainText, offset);
}
//remove padding
offset += cipherImpl.doFinal(cipherText, offset, finalPlainText, offset);
assert(offset == cipherText.length);
return finalPlainText.sublist(0, offset);
} catch(e) {
if( gDebug >= 0) print("Decryption error = $e");
return Uint8List(0);
}
}
/// Encrypt data using self private key in nostr format ( with trailing ?iv=)
String myEncrypt( String privateString,
String publicString,
String plainText) {
//print("private = ${privateString.length} public = ${publicString.length}");
//print("private = ${privateString} public = ${publicString}");
Uint8List uintInputText = convert.Utf8Encoder().convert(plainText);
final encryptedString = myEncryptRaw(privateString, publicString, uintInputText);
//print("encryptedString = $encryptedString");
return encryptedString;
}
String myEncryptRaw( String privateString,
String publicString,
Uint8List uintInputText) {
final secretIV = Kepler.byteSecret(privateString, publicString);
final key = Uint8List.fromList(secretIV[0]);
// generate iv https://stackoverflow.com/questions/63630661/aes-engine-not-initialised-with-pointycastle-securerandom
FortunaRandom fr = FortunaRandom();
final _sGen = Random.secure();;
fr.seed(KeyParameter(
Uint8List.fromList(List.generate(32, (_) => _sGen.nextInt(255)))));
final iv = fr.nextBytes(16); //Uint8List.fromList(secretIV[1]);
CipherParameters params = new PaddedBlockCipherParameters(
new ParametersWithIV(new KeyParameter(key), iv), null);
PaddedBlockCipherImpl cipherImpl = new PaddedBlockCipherImpl(
new PKCS7Padding(), new CBCBlockCipher(new AESEngine()));
cipherImpl.init(true, // means to encrypt
params as PaddedBlockCipherParameters<CipherParameters?,
CipherParameters?>);
final Uint8List outputEncodedText = Uint8List(uintInputText.length + 16); // allocate space
var offset = 0;
while (offset < uintInputText.length - 16) {
offset += cipherImpl.processBlock(uintInputText, offset, outputEncodedText, offset);
}
//add padding
offset += cipherImpl.doFinal(uintInputText, offset, outputEncodedText, offset);
assert(offset == uintInputText.length);
final Uint8List finalEncodedText = outputEncodedText.sublist(0, offset);
String stringIv = convert.base64.encode(iv);;
String outputPlainText = convert.base64.encode(finalEncodedText);
outputPlainText = outputPlainText + "?iv=" + stringIv;
return outputPlainText;
}
Set<Event> readEventsFromFile(String filename) {
Set<Event> events = {};
final File file = File(filename);
// sync read
try {
List<String> lines = file.readAsLinesSync();
for( int i = 0; i < lines.length; i++ ) {
Event e = Event.fromJson(lines[i], "", true);
events.add(e);
}
} on Exception catch(e) {
//print("cannot open file $gEventsFilename");
if( gDebug > 0) print("Could not open file. error = $e");
}
if( gDebug > 0) print("In readEventsFromFile: returning ${events.length} total events");
return events;
}
bool isValidPubkey(String pubkey) {
if( pubkey.length == 64) {
return true;
}
return false;
}
List<String> getEncryptedChannelKeys(List<DirectMessageRoom> directRooms, Map<String, Tree> tempChildEventsMap, String channelId) {
Event? e = tempChildEventsMap[channelId]?.event;
if( e != null) {
//print("\n----------------\nIn getEncryptedChannelKeys for encrypted channel $channelId");
String creatorPubKey = e.eventData.pubkey;
for( int i = 0; i < directRooms.length; i++) {
DirectMessageRoom room = directRooms[i];
if( room.otherPubkey == creatorPubKey) {
//print("got other pubkey $creatorPubKey");
for( int j = 0; j < room.messageIds.length; j++) {
String messageId = room.messageIds[j];
Event? messageEvent = tempChildEventsMap[messageId]?.event;
if( messageEvent != null) {
//print("got a message which is: ${messageEvent.eventData.evaluatedContent}");
//print(messageEvent.eventData.getStrForChannel(0));
String evaluatedContent = messageEvent.eventData.evaluatedContent;
if( evaluatedContent.startsWith("App Encrypted Channels:")) {
//print("got App");
if( evaluatedContent.contains(channelId) && evaluatedContent.length == 288) {
//print("in getEncryptedChannelKeys: success: got password in pvt message: $evaluatedContent");
//print("len of evaluated content: ${evaluatedContent.length} ");
String priKey = evaluatedContent.substring(159, 159 + 64);
String pubKey = evaluatedContent.substring(224, 224 + 64);
if( priKey.length == 64 && pubKey.length == 64) {
return [priKey, pubKey];
}
}
}
} else {
print("could not get message event");
}
}
}
}
}
return [];
}
String myGetPublicKey(String prikey) {
String pubkey = getPublicKey(prikey);
if( pubkey.length < 64) {
int numZeros = 64 - pubkey.length;
for(int i = 0; i < numZeros; i++) {
pubkey = "0" + pubkey;
}
}
return pubkey;
}