mirror of
https://github.com/vishalxl/nostr_console.git
synced 2025-11-30 07:47:31 +01:00
Where Tree was formed first, and then lot events were being inserted into it. This is not supported yet. So now events are collected initially, and then Tree is formed. also fixed issue that new kind 0 events were not getting handled, which result that lists were not printing names.
833 lines
27 KiB
Dart
833 lines
27 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> eTagsRest;// 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( eTagsRest.isNotEmpty) {
|
|
return eTagsRest[eTagsRest.length - 1];
|
|
}
|
|
return "";
|
|
}
|
|
|
|
EventData(this.id, this.pubkey, this.createdAt, this.kind, this.content, this.eTagsRest, 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( pubkey == userPublicKey) break; // crashes right now otherwise
|
|
if(!isUserDirectMessage(this)) {
|
|
break;
|
|
}
|
|
//print("in translateAndExpandMentions() for a dm \npubkey = $pubkey \ncontent = $content");
|
|
//print("tags = $tags\n");
|
|
int ivIndex = content.indexOf("?iv=");
|
|
var enc_str = content.substring(0, ivIndex);
|
|
var iv = content.substring( ivIndex + 4, content.length);
|
|
//print("enc_str = $enc_str ; iv = $iv");
|
|
|
|
String userKey = userPrivateKey ;
|
|
String otherUserPubKey = "02" + pubkey;
|
|
if( pubkey == userPublicKey) {
|
|
userKey = userPrivateKey;
|
|
otherUserPubKey = "02" + pubkey;
|
|
//iv = "";
|
|
}
|
|
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( id == gCheckEventId) {
|
|
if(gDebug > 0) {
|
|
print("In Event printEventData: got message: $gCheckEventId");
|
|
isNotification = true;
|
|
}
|
|
}
|
|
|
|
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 strDate = getPrintableDate(createdAt);
|
|
if( createdAt == 0) {
|
|
print("debug: createdAt == 0 for event $content");
|
|
}
|
|
|
|
String contentShifted = rightShiftContent(evaluatedContent==""?content: evaluatedContent, gSpacesPerDepth * depth + 10);
|
|
|
|
printDepth(depth);
|
|
stdout.write("+-------+\n");
|
|
printDepth(depth);
|
|
String name = getAuthorName(pubkey);
|
|
stdout.write("|Author : $name id: ${maxN(id)} Time: $strDate\n");
|
|
printReaction(depth); // only prints if there are any likes/reactions
|
|
printDepth(depth);
|
|
stdout.write("|Message: ");
|
|
if( isNotification) {
|
|
printInColor(contentShifted, gNotificationColor);
|
|
isNotification = false;
|
|
} else {
|
|
printInColor(contentShifted, gCommentColor);
|
|
}
|
|
}
|
|
|
|
String getAsLine({int len = 20}) {
|
|
if( len == 0 || len > content.length) {
|
|
len = content.length;
|
|
}
|
|
|
|
return '"${content.substring(0, len)}..." - ${getAuthorName(pubkey)}';
|
|
}
|
|
|
|
// 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)
|
|
void printReaction(int depth) {
|
|
if( gReactions.containsKey(id)) {
|
|
String reactorNames = "|Likes : ";
|
|
printDepth(depth);
|
|
//print("All Likes:");
|
|
int numReactions = gReactions[id]?.length??0;
|
|
List<List<String>> reactors = gReactions[id]??[];
|
|
for( int i = 0; i <numReactions; i++) {
|
|
String reactorId = reactors[i][0];
|
|
if( newLikes.contains(reactorId)) {
|
|
// colorify
|
|
reactorNames += gNotificationColor + getAuthorName(reactorId) + gColorEndMarker;
|
|
} else {
|
|
reactorNames += getAuthorName(reactorId);
|
|
}
|
|
|
|
if( i < numReactions -1) {
|
|
reactorNames += ", ";
|
|
}
|
|
}
|
|
print(reactorNames);
|
|
newLikes.clear();
|
|
}
|
|
}
|
|
|
|
@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 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;
|
|
}
|
|
|