nostr_console/lib/event_ds.dart

916 lines
30 KiB
Dart

import 'dart:ffi';
import 'dart:io';
import 'dart:convert';
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 = 1;// 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: <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 = {};
class EventData {
String id;
String pubkey;
int createdAt;
int kind;
String content;
List<String> eTags;// rest of 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; //
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
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<Contact> contactList = [];
List<String> eTagsRead = [];
List<String> pTagsRead = [];
List<List<String>> 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<String> 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<String> 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;
}
void translateAndExpandMentions() {
if (content == "" || evaluatedContent != "") {
return;
}
switch(kind) {
case 1:
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(!isUserDirectMessage(this)) {
break;
}
int ivIndex = content.indexOf("?iv=");
var enc_str = content.substring(0, ivIndex);
var iv = content.substring( ivIndex + 4, content.length);
String userKey = userPrivateKey ;
String otherUserPubKey = "02" + pubkey;
if( pubkey == userPublicKey) {
userKey = userPrivateKey;
otherUserPubKey = "02" + pubkey;
}
var decryptd = myPrivateDecrypt( userKey, otherUserPubKey, enc_str, iv); // use bob's privatekey and alic's publickey means bob can read message from alic
evaluatedContent = decryptd;
break;
} // end switch
} // end translateAndExpandMentions
// 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 = "<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 $content");
}
String contentShifted = rightShiftContent(tempEvaluatedContent==""?tempContent: tempEvaluatedContent, gSpacesPerDepth * depth + 10);
String strToPrint = "";
strToPrint += getDepthSpaces(depth); // printDepth(depth);
strToPrint += ("+-------+\n"); // stdout.write("+-------+\n");
strToPrint += getDepthSpaces(depth); // printDepth(depth);
strToPrint += "|Author : $name id: ${maxN(id)} Time: $strDate\n"; //stdout.write("|Author : $name id: ${maxN(id)} Time: $strDate\n");
strToPrint += getReactionStr(depth); // only prints if there are any likes/reactions
strToPrint += getDepthSpaces(depth); // printDepth(depth);
strToPrint += "|Message: "; //stdout.write("|Message: ");
String commentColor = "";
if( isNotification) {
commentColor = gNotificationColor;
isNotification = false;
} else {
commentColor = gCommentColor;
}
strToPrint += getStrInColor(contentShifted , commentColor);
stdout.write(strToPrint);
}
String getAsLine({int len = 20}) {
if( len == 0 || len > content.length) {
len = content.length;
}
return '"${content.substring(0, len)}..." - ${getAuthorName(pubkey)}';
}
String getStrForChannel(int depth) {
String strToPrint = "";
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
}
String nameToPrint = name.padLeft(gSpacesPerDepth * 2).substring(0, gSpacesPerDepth * 2);
String dateToPrint = strDate.padLeft(gSpacesPerDepth * 2).substring(0, gSpacesPerDepth * 2);
strToPrint = "${getDepthSpaces(depth)} $dateToPrint$nameToPrint: ";
int contentDepth = depth + 4 + 1;
String contentShifted = rightShiftContent(tempEvaluatedContent==""?tempContent: tempEvaluatedContent, gSpacesPerDepth * contentDepth);
strToPrint += 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 = getDepthSpaces(depth) + "|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 += gNotificationColor + getAuthorName(reactorId) + gColorEndMarker + comma;
firstEntry = false;
} else {
// this is normal printing of the reaction. only print for + for now
if( reactors[i][1] == "+")
reactorNames += getAuthorName(reactorId);
firstEntry = false;
}
} // end for
newLikes.clear();
reactorNames += "\n";
}
return reactorNames;
}
@override
String toString() {
if( id == "non") {
return '';
}
String max3(String v) => v.length > 3? v.substring(0,3) : v.substring(0, v.length);
DateTime dTime = DateTime.fromMillisecondsSinceEpoch(createdAt *1000);
if( createdAt == 0) {
print("createdAt == 0 for event $content");
}
return '\n-------+-------------\nAuthor : ${max3(pubkey)}\nMessage: $content\n\nid : ${max3(id)} Time: $dTime Kind: $kind';
}
}
// 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]: "";
return Event(e,"",EventData("non","", 0, 0, "", [], [], [], [[]], {}), [relay], "[json]", fromFile);
}
return Event(json[0] as String, json[1] as String, EventData.fromJson(json[2]), [relay], d, fromFile );
} on Exception catch(e) {
if( gDebug> 0) {
print("Could not create event. returning dummy event. $e");
}
return Event("","",EventData("non","", 0, 0, "", [], [], [], [[]], {}), [relay], "[json]", fromFile);
}
}
void printEvent(int depth) {
eventData.printEventData(depth);
//stdout.write("\n$originalJson \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++ ) {
//print ( "${listHistogram[i].str} ${listHistogram[i].count} ");
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( gDebug > 0) print( contacts[i].toString() );
if( contacts[i].id == contactPubkey) {
relay = contacts[i].relay;
//if(gDebug > 0) print("In getRelayOfUser: found relay $relay for contact $contactPubkey" );
return relay;
}
}
}
}
// if not found return empty string
return relay;
}
// 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 = "";
try {
dynamic json = jsonDecode(content);
name = json["name"];
about = json["about"];
picture = json["picture"];
} catch(ex) {
//if( gDebug != 0) print("Warning: In processKind0Event: caught exception for content: ${e.eventData.content}");
if( name.isEmpty) {
//return false;
}
}
bool newEntry = false, entryModified = false;
if( !gKindONames.containsKey(e.eventData.pubkey)) {
gKindONames[e.eventData.pubkey] = UserNameInfo(e.eventData.createdAt, name, about, picture, null);
newEntry = true;;
} else {
int oldTime = gKindONames[e.eventData.pubkey]?.createdAt??0;
if( oldTime < e.eventData.createdAt) {
Event? oldContactEvent = gKindONames[e.eventData.pubkey]?.latestContactEvent;
gKindONames[e.eventData.pubkey] = UserNameInfo(e.eventData.createdAt, name, about, picture, oldContactEvent);
entryModified = true;;
}
}
if(gDebug > 0) {
print("At end of processKind0Events: for name = $name ${newEntry? "added entry": ( entryModified?"modified entry": "No change done")} ");
}
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;
}
// 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 = 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 userName) {
Set<String> pubkeys = {};
//if(gDebug > 0) print("In getPublicKeyFromName: doing lookup for $userName len of gKindONames= ${gKindONames.length}");
gKindONames.forEach((pk, userInfo) {
// check both the user name, and the pubkey to search for the user
//print(userInfo.name);
if( userName == userInfo.name) {
pubkeys.add(pk);
}
if( userName.length <= pk.length) {
//print("$pk $userName" );
if( pk.substring(0, userName.length) == userName) {
pubkeys.add(pk);
}
}
});
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) => { print("$x\n${getNumDashes(x.length)}")};
void printDepth(int d) {
for( int i = 0; i < gSpacesPerDepth * d + gNumLeftMarginSpaces; i++) {
stdout.write(" ");
}
}
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 s = "";
for( int i = 0; i < num; i++) {
s += "-";
}
return s;
}
String rightShiftContent(String s, int numSpaces) {
String newString = "";
int newlineCounter = 0;
String spacesString = getNumSpaces(numSpaces + gNumLeftMarginSpaces);
for(int i = 0; i < s.length; i++) {
if( s[i] == '\n') {
newString += "\n";
newString += spacesString;
newlineCounter = 0;
} else {
if( newlineCounter >= (gTextWidth - numSpaces)) {
newString += "\n";
newString += spacesString;
newlineCounter = 0;
}
newString += s[i];
}
newlineCounter++;
}
return newString;
}
bool nonEnglish(String str) {
bool result = false;
return result;
}
bool isNumeric(String s) {
return double.tryParse(s) != null;
}
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 {
isChannelPageNumber(int max) {
int? n = int.tryParse(this);
if( n != null) {
if( n < max)
return true;
}
return false;
}
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;
}
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) {
return str.replaceAll("\"", "\\\"");
}
String getShaId(String pubkey, int createdAt, String kind, String strTags, String content) {
String buf = '[0,"$pubkey",$createdAt,$kind,[$strTags],"$content"]';
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;
}
bool isUserDirectMessage(EventData directMessageData) {
if( directMessageData.pubkey == userPublicKey) {
return true;
}
bool sentToUser = false;
directMessageData.tags.forEach((tag) {
if( tag.length < 2 )
return;
if( tag[0] == "p" && tag[1] == userPublicKey) {
//print("in isUserDirectMessage ${tag[1]}");
sentToUser = true;
}
});
return sentToUser;
}
/// 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);
convert.Utf8Decoder decode = const convert.Utf8Decoder();
return decode.convert(rawData.toList());
}
Uint8List myPrivateDecryptRaw( String privateString,
String publicString,
Uint8List cipherText,
[String b64IV = ""]) {
final secretIV = Kepler.byteSecret(privateString, publicString);
final key = Uint8List.fromList(secretIV[0]);
final iv = b64IV.length > 6
? convert.base64.decode(b64IV)
: Uint8List.fromList(secretIV[1]);
bool debug = false;
if( debug) print("iv = $iv ");
// pointy castle source https://github.com/PointyCastle/pointycastle/blob/master/tutorials/aes-cbc.md
final cbc = CBCBlockCipher(AESFastEngine())
..init(false, ParametersWithIV(KeyParameter(key), iv) ) ;
// 2
//PaddedBlockCipher('AES/CBC/PKCS7').KeyParameter = KeyParameter;
PaddedBlockCipher p = PaddedBlockCipher('AES/CBC/PKCS7');
if( debug) print("p cipher: ${p.cipher}");
p.cipher.init(false, ParametersWithIV(KeyParameter(key), iv) ) ;
PaddedBlockCipherParameters paddedParams = PaddedBlockCipherParameters ( ParametersWithIV(KeyParameter(key), iv), null );
final pbc = CBCBlockCipher(p)..init(false, ParametersWithIV(paddedParams, iv) ) ;
// 3 https://github.com/Dhuliang/flutter-bsv/blob/42a2d92ec6bb9ee3231878ffe684e1b7940c7d49/lib/src/aescbc.dart
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 paddedPlainText = Uint8List(cipherText.length); // allocate space
var offset = 0;
while (offset < cipherText.length) {
convert.Utf8Decoder decode = const convert.Utf8Decoder();
offset += cipherImpl.processBlock(cipherText, offset, paddedPlainText, offset);
}
assert(offset == cipherText.length);
final pd = PKCS7Padding ();
Uint8List retval = paddedPlainText;// p.process(false, paddedPlainText);
return retval.sublist(0, retval.length);
}
ParametersWithIV<KeyParameter>
buildParams( Uint8List key, Uint8List iv) {
return ParametersWithIV<KeyParameter>(KeyParameter(key), iv);
}
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;
}
void readjustAlignment() {
// align the text again in case the window size has been changed
if( gAlignment == "center") {
try {
if( gTextWidth > stdout.terminalColumns) {
gTextWidth = stdout.terminalColumns - 5;
}
gNumLeftMarginSpaces = (stdout.terminalColumns - gTextWidth )~/2;
} on StdoutException catch (e) {
print("Terminal information not available");
if( gDebug>0) print("${e.message}");
gNumLeftMarginSpaces = 0;
}
}
}