added support for reading and writing events from/to a file with command line argument. Display and other improvements: printed user logged in for information.

This commit is contained in:
vishalxl 2022-08-21 00:11:50 +05:30
parent 0d09e43717
commit d4cc9fbcae
7 changed files with 191 additions and 94 deletions

View File

@ -19,6 +19,8 @@ usage: dart run bin/nostr_console.dart [OPTIONS]
--days <N as num> The latest number of days for which events are shown. Default is 1. Same as -d
--request <REQ string> This request is sent verbatim to the default relay. It can be used to recieve all events
from a relay. If not provided, then events for default or given user are shown. Same as -q
--file <filename> Read from given file, if it is present, and at the end of the program execution, write
to it all the events (including the ones read, and any new received). Same as -f
UI Options
--align <left> When "left" is given as option to this argument, then the text is aligned to left. By default
the posts or text is aligned to the center of the terminal. Same as -a
@ -33,20 +35,27 @@ usage: dart run bin/nostr_console.dart [OPTIONS]
To get ALL the latest messages for last 3 days (on linux which allows backtick execution):
```
dart run bin/nostr_console.dart --request=`echo "[\"REQ\",\"l\",{\"since\":$(date -d '-3 day' +%s)}]"`
```
nostr_console.exe --request=`echo "[\"REQ\",\"l\",{\"since\":$(date -d '-3 day' +%s)}]"`
```
To get the latest messages for user with private key K ( that is also used to sign posted/sent messages):
```
dart run bin/nostr_console.dart --prikey=K
nostr_console.exe --prikey=K
```
To get the latest messages for user with private key K for last 4 days ( default is 1) from relay R:
```
dart run bin/nostr_console.dart --prikey=K --relay=R --days=4
```
```
nostr_console.exe --prikey=K --relay=R --days=4
```
To write events to a file ( and later read from it too), for any given private key K:
```
nostr_console.exe --file=eventsFile.txt --prikey=K
```
# Screenshots

View File

@ -17,6 +17,7 @@ const String helpArg = "help";
const String alignArg = "align"; // can be "left"
const String widthArg = "width";
const String maxDepthArg = "maxdepth";
const String eventFileArg = "file";
void printUsage() {
String usage = """$exename version $version
@ -34,6 +35,8 @@ usage: $exename [OPTIONS]
--days <N as num> The latest number of days for which events are shown. Default is 1. Same as -d
--request <REQ string> This request is sent verbatim to the default relay. It can be used to recieve all events
from a relay. If not provided, then events for default or given user are shown. Same as -q
--file <filename> Read from given file, if it is present, and at the end of the program execution, write
to it all the events (including the ones read, and any new received). Same as -f
UI Options
--align <left> When "left" is given as option to this argument, then the text is aligned to left. By default
the posts or text is aligned to the center of the terminal. Same as -a
@ -52,7 +55,8 @@ Future<void> main(List<String> arguments) async {
final parser = ArgParser()..addOption(requestArg, abbr: 'q') ..addOption(pubkeyArg, abbr:"p")..addOption(prikeyArg, abbr:"k")
..addOption(lastdaysArg, abbr:"d") ..addOption(relayArg, abbr:"r")
..addFlag(helpArg, abbr:"h", defaultsTo: false)..addOption(alignArg, abbr:"a")
..addOption(widthArg, abbr:"w")..addOption(maxDepthArg, abbr:"m");
..addOption(widthArg, abbr:"w")..addOption(maxDepthArg, abbr:"m")
..addOption(eventFileArg, abbr:"f");
try {
ArgResults argResults = parser.parse(arguments);
@ -131,6 +135,14 @@ Future<void> main(List<String> arguments) async {
});
return;
}
if( argResults[eventFileArg] != null) {
gEventsFilename = argResults[eventFileArg];
if( gEventsFilename != "") {
print("Going to use file to read from and store events: $gEventsFilename");
}
}
} on FormatException catch (e) {
print(e.message);
return;
@ -139,26 +151,37 @@ Future<void> main(List<String> arguments) async {
return;
}
int numFileEvents = 0, numUserEvents = 0, numFeedEvents = 0, numOtherEvents = 0;
if( gEventsFilename != "") {
stdout.write('Reading events from the given file.......');
List<Event> eventsFromFile = readEventsFromFile(gEventsFilename);
setRelaysIntialEvents(eventsFromFile);
getRecievedEvents().forEach((element) { element.eventData.kind == 1? numFileEvents++: numFileEvents;});
print("read $numFileEvents posts from file \"$gEventsFilename\"");
//numFileEvents = getRecievedEvents().length;
}
// the default in case no arguments are given is:
// get a user's events, then from its type 3 event, gets events of its follows,
// then get the events of user-id's mentioned in p-tags of received events
// then display them all
getUserEvents(defaultServerUrl, userPublicKey, 1000, 0);
int numUserEvents = 0, numFeedEvents = 0, numOtherEvents = 0;
const int numWaitSeconds = 2500;
stdout.write('Waiting for user events to come in.....');
stdout.write('Waiting for user posts to come in.....');
Future.delayed(const Duration(milliseconds: numWaitSeconds), () {
// count user events
getRecievedEvents().forEach((element) { element.eventData.kind == 1? numUserEvents++: numUserEvents;});
stdout.write("...received ${getRecievedEvents().length} events made by the user\n");
numUserEvents -= numFileEvents;
stdout.write("...received $numUserEvents posts made by the user\n");
// get the latest kind 3 event for the user, which lists his 'follows' list
int latestContactsTime = 0, latestContactIndex = -1;
for( int i = 0; i < getRecievedEvents().length; i++) {
var e = getRecievedEvents()[i];
if( e.eventData.kind == 3 && latestContactsTime < e.eventData.createdAt) {
if( e.eventData.pubkey == userPublicKey && e.eventData.kind == 3 && latestContactsTime < e.eventData.createdAt) {
latestContactIndex = i;
latestContactsTime = e.eventData.createdAt;
}
@ -170,24 +193,24 @@ Future<void> main(List<String> arguments) async {
contactList = getContactFeed(getRecievedEvents()[latestContactIndex].eventData.contactList, 300);
}
stdout.write('Waiting for feed to come in...............');
stdout.write('Waiting for feed to come in..............');
Future.delayed(const Duration(milliseconds: numWaitSeconds * 1), () {
// count feed events
getRecievedEvents().forEach((element) { element.eventData.kind == 1? numFeedEvents++: numFeedEvents;});
numFeedEvents = numFeedEvents - numUserEvents;
stdout.write("received $numFeedEvents events from the follows\n");
numFeedEvents = numFeedEvents - numUserEvents - numFileEvents;
stdout.write("received $numFeedEvents posts from the follows\n");
// get mentioned ptags, and then get the events for those users
List<String> pTags = getpTags(getRecievedEvents());
getMultiUserEvents(defaultServerUrl, pTags, 300);
stdout.write('Waiting for rest of events to come in.....');
stdout.write('Waiting for rest of posts to come in.....');
Future.delayed(const Duration(milliseconds: numWaitSeconds * 2), () {
// count other events
getRecievedEvents().forEach((element) { element.eventData.kind == 1? numOtherEvents++: numOtherEvents;});
numOtherEvents = numOtherEvents - numFeedEvents - numUserEvents;
stdout.write("received $numOtherEvents other events\n");
numOtherEvents = numOtherEvents - numFeedEvents - numUserEvents - numFileEvents;
stdout.write("received $numOtherEvents other posts\n");
// get all events in Tree form
Tree node = getTree(getRecievedEvents());

View File

@ -100,10 +100,8 @@ Future<void> mainMenuUi(Tree node, var contactList) async {
// align the text again in case the window size has been changed
if( gAlignment == "center") {
try {
// can be computed only after textWidth has been found
gNumLeftMarginSpaces = (stdout.terminalColumns - gTextWidth )~/2;
} on StdoutException catch (e) {
//print("Cannot find terminal size. Left aligning by default.");
gNumLeftMarginSpaces = 0;
}
}
@ -114,7 +112,7 @@ Future<void> mainMenuUi(Tree node, var contactList) async {
Future.delayed(const Duration(milliseconds: waitMilliSeconds), () {
List<String> newEventsId = node.insertEvents(getRecievedEvents());
node.printNotifications(newEventsId);
node.printNotifications(newEventsId, getAuthorName(userPublicKey));
clearEvents();
});
@ -183,7 +181,9 @@ Future<void> mainMenuUi(Tree node, var contactList) async {
print("\nFinished fetching feed for user $userPublicKey ($authorName), whose contact list has ${contactList.length} profiles.\n ");
contactList.forEach((x) => stdout.write("${getAuthorName(x)}, "));
stdout.write("\n");
//await node.writeEventsToFile("nostrConsoleEventsStore.txt");
if( gEventsFilename != "") {
await node.writeEventsToFile(gEventsFilename);
}
exit(0);
}
} // end while

View File

@ -18,6 +18,7 @@ int gTextWidth = gDefaultTextWidth; // is changed by --width option
const int gSpacesPerDepth = 8; // constant
int gNumLeftMarginSpaces = 0; // this number is modified in main
String gAlignment = "center"; // is modified in main if --align argument is given
const int gapBetweenTopTrees = 1;
// after depth of maxDepthAllowed the thread is re-aligned to left by leftShiftThreadBy
const int gMinimumDepthAllowed = 2;
@ -55,6 +56,9 @@ List<String> gBots = [ "3b57518d02e6acfd5eb7198530b2e351e5a52278fb2499d14b66db2
"f4161c88558700d23af18d8a6386eb7d7fed769048e1297811dcc34e86858fb2" // bitcoin_bot
];
//const String gDefaultEventsFilename = "events_store_nostr.txt";
String gEventsFilename = ""; // is set in arguments, and if set, then file is read from and written to
int gDebug = 0;
void printDepth(int d) {
@ -194,7 +198,8 @@ class EventData {
return EventData(json['id'] as String, json['pubkey'] as String,
json['created_at'] as int, json['kind'] as int,
json['content'] as String, eTagsRead, pTagsRead,
json['content'].trim() as String,
eTagsRead, pTagsRead,
contactList, tagsRead,
{});
}
@ -317,13 +322,17 @@ class Event {
Event(this.event, this.id, this.eventData, this.seenOnRelays, this.originalJson);
factory Event.fromJson(String d, String relay) {
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]");
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]");
}
return Event(json[0] as String, json[1] as String, EventData.fromJson(json[2]), [relay], d );
} on Exception catch(e) {
return Event("","",EventData("non","", 0, 0, "", [], [], [], [[]], {}), [relay], "[json]");
}
return Event(json[0] as String, json[1] as String, EventData.fromJson(json[2]), [relay], d );
}
void printEvent(int depth) {
@ -402,3 +411,34 @@ void printUserInfo(List<Event> events, String pub) {
}
print("Number of user events for user ${getAuthorName(pub)} : $numUserEvents");
}
List<Event> readEventsFromFile(String filename) {
List<Event> events = [];
final File file = File(filename);
// sync
try {
List<String> lines = file.readAsLinesSync();
for( int i = 0; i < lines.length; i++ ) {
Event e = Event.fromJson(lines[i], "");
events.add(e);
}
} on Exception catch(err) {
print("Cannot open file $gEventsFilename");
}
/*
while(true) {
try {
String strEvent = file.readAsStringSync();
//print(strEvent);
}
on Exception catch(e) {
break;
}
}*/
return events;
}

View File

@ -267,3 +267,9 @@ List<Event> getRecievedEvents() {
void clearEvents() {
relays.rEvents = [];
}
void setRelaysIntialEvents(eventsFromFile) {
relays.rEvents = eventsFromFile;
}

View File

@ -8,6 +8,8 @@ class Tree {
List<String> eventsWithoutParent;
Tree(this.e, this.children, this.allChildEventsMap, this.eventsWithoutParent);
static const List<int> typesInEventMap = [0, 1, 3, 7]; // 0 meta, 1 post, 3 follows list, 7 reactions
// @method create top level Tree from events.
// first create a map. then process each element in the map by adding it to its parent ( if its a child tree)
factory Tree.fromEvents(List<Event> events) {
@ -17,7 +19,12 @@ class Tree {
// create a map from list of events, key is eventId and value is event itself
Map<String, Tree> allChildEventsMap = {};
events.forEach((event) { allChildEventsMap[event.eventData.id] = Tree(event, [], {}, []); });
events.forEach((event) {
// only add in map those kinds that are supported or supposed to be added ( 0 1 3 7)
if( typesInEventMap.contains(event.eventData.kind)) {
allChildEventsMap[event.eventData.id] = Tree(event, [], {}, []);
}
});
// this will become the children of the main top node. These are events without parents, which are printed at top.
List<Tree> topLevelTrees = [];
@ -25,6 +32,7 @@ class Tree {
List<String> tempWithoutParent = [];
allChildEventsMap.forEach((key, value) {
// only posts areadded to this tree structure
if( value.e.eventData.kind != 1) {
return;
}
@ -34,16 +42,20 @@ class Tree {
//stdout.write("added to parent a child\n");
String id = key;
String parentId = value.e.eventData.getParent();
if( allChildEventsMap[parentId]?.e.eventData.kind != 1) { // since parent can only be a kind 1 event
return;
if( allChildEventsMap.containsKey(parentId)) {
}
if(allChildEventsMap.containsKey( parentId)) {
allChildEventsMap[parentId]?.addChildNode(value); // in this if condition this will get called
if( allChildEventsMap[parentId]?.e.eventData.kind != 1) { // since parent can only be a kind 1 event
print("In fromEvents: got an event whose parent is not a type 1 post: $id");
return;
}
allChildEventsMap[parentId]?.addChildNode(value); // in this if condition this will get called
} else {
// in case where the parent of the new event is not in the pool of all events,
// then we create a dummy event and put it at top ( or make this a top event?) TODO handle so that this can be replied to, and is fetched
Tree dummyTopNode = Tree(Event("","",EventData("Unk" ,gDummyAccountPubkey, value.e.eventData.createdAt , 0, "Unknown parent event", [], [], [], [[]], {}), [""], "[json]"), [], {}, []);
Tree dummyTopNode = Tree(Event("","",EventData("Unk" ,gDummyAccountPubkey, value.e.eventData.createdAt , 1, "Unknown parent event", [], [], [], [[]], {}), [""], "[json]"), [], {}, []);
dummyTopNode.addChildNode(value);
tempWithoutParent.add(value.e.eventData.id);
@ -56,13 +68,11 @@ class Tree {
// add parent trees as top level child trees of this tree
for( var value in allChildEventsMap.values) {
if( value.e.eventData.kind == 1 && value.e.eventData.eTagsRest.isEmpty) { // if its a parent
if( value.e.eventData.kind == 1 && value.e.eventData.eTagsRest.isEmpty) { // only posts which are parents
topLevelTrees.add(value);
}
}
// add tempWithoutParent to topLevelTrees too
if(gDebug != 0) print("number of events without parent in fromEvents = ${tempWithoutParent.length}");
return Tree( events[0], topLevelTrees, allChildEventsMap, tempWithoutParent); // TODO remove events[0]
} // end fromEvents()
@ -78,7 +88,7 @@ class Tree {
newEvents.forEach((newEvent) {
// don't process if the event is already present in the map
// this condition also excludes any duplicate events sent as newEvents
if( allChildEventsMap[newEvent.eventData.id] != null) {
if( allChildEventsMap.containsKey(newEvent.eventData.id)) {
return;
}
@ -87,30 +97,28 @@ class Tree {
String reactedTo = processReaction(newEvent);
if( reactedTo != "") {
newEventsId.add(newEvent.eventData.id);
newEventsId.add(newEvent.eventData.id); // add here to process/give notification about this new reaction
if(gDebug > 0) print("got a new reaction by: ${newEvent.eventData.id} to $reactedTo");
} else {
return;
}
}
// only kind 1 events are added to map, return otherwise
if( newEvent.eventData.kind != 1 && newEvent.eventData.kind != 7) {
// only kind 0, 1, 3, 7 events are added to map, return otherwise
if( !typesInEventMap.contains(newEvent.eventData.kind) ) {
return;
}
allChildEventsMap[newEvent.eventData.id] = Tree(newEvent, [], {}, []);
newEventsId.add(newEvent.eventData.id);
});
//print("In insertEvents num eventsId: ${newEventsId.length}");
// now go over the newly inserted event, and then find its parent, or if its a top tree
// now go over the newly inserted event, and add its to the tree. only for kind 1 events
newEventsId.forEach((newId) {
Tree? newTree = allChildEventsMap[newId]; // this should return true because we just inserted this event in the allEvents in block above
// in case the event is already present in the current collection of events (main Tree)
if( newTree != null) {
// now kind 7 events are also returned and are not added to the tree structure itself
if( newTree.e.eventData.kind == 7) {
// only kind 1 events are added to the overall tree structure
if( newTree.e.eventData.kind != 1) {
return;
}
@ -132,6 +140,8 @@ class Tree {
int printTree(int depth, bool onlyPrintChildren, var newerThan) {
if( e.eventData.kind != 1) {
print("Warning: In print tree found non kind 1 event");
//e.printEvent(depth);
return 0; // for kind 7 event or any other
}
@ -158,8 +168,9 @@ class Tree {
continue;
}
stdout.write("\n");
printDepth(depth+1);
stdout.write("\n\n\n");
for( int i = 0; i < gapBetweenTopTrees; i++ ) {
stdout.write("\n");
}
}
// if the thread becomes too 'deep' then reset its depth, so that its
@ -193,24 +204,27 @@ class Tree {
* @printNotifications Add the given events to the Tree, and print the events as notifications
* It should be ensured that these are only kind 1 events
*/
void printNotifications(List<String> newEventsId) {
// remove duplicate
void printNotifications(List<String> newEventsId, String userName) {
// remove duplicates
Set temp = {};
newEventsId.retainWhere((event) => temp.add(newEventsId));
stdout.write("\n---------------------------------------\nNotifications: ");
String strToWrite = "Notifications: ";
if( newEventsId.isEmpty) {
stdout.write("No new replies/posts.\nTotal posts: ${count()}\n\n");
strToWrite += "No new replies/posts.\n";
stdout.write("${getNumDashes(strToWrite.length - 1)}\n$strToWrite");
stdout.write("Total posts : ${count()}\n");
stdout.write("Signed in as : $userName\n\n");
return;
}
// TODO call count() less
stdout.write("Number of new replies/posts = ${newEventsId.length}\nTotal posts: ${count()}\n");
strToWrite += "Number of new replies/posts = ${newEventsId.length}\n";
stdout.write("${getNumDashes(strToWrite.length -1 )}\n$strToWrite");
stdout.write("Total posts : ${count()}\n");
stdout.write("Signed in as : $userName\n");
stdout.write("\nHere are the threads with new replies or new likes: \n\n");
List<Tree> topTrees = []; // collect all top tress to display in this list. only unique tress will be displayed
newEventsId.forEach((eventID) {
// ignore if not in Tree. Should ideally not happen. TODO write warning otherwise
if( allChildEventsMap[eventID] == null) {
@ -219,39 +233,40 @@ class Tree {
Tree ?t = allChildEventsMap[eventID];
if( t != null) {
if( t.e.eventData.kind == 1 ) {
t.e.eventData.isNotification = true;
Tree topTree = getTopTree(t);
topTrees.add(topTree);
} else {
//t.e.eventData.newLikes = ;
Event event = t.e;
if(gDebug >= 0) ("Got notification of type 7");
String reactorId = event.eventData.pubkey;
String comment = event.eventData.content;
int lastEIndex = event.eventData.eTagsRest.length - 1;
String reactedTo = event.eventData.eTagsRest[lastEIndex];
Event? reactedToEvent = allChildEventsMap[reactedTo]?.e;
if( reactedToEvent != null) {
Tree? reactedToTree = allChildEventsMap[reactedTo];
if( reactedToTree != null) {
reactedToTree.e.eventData.newLikes.add( reactorId);
Tree topTree = getTopTree(reactedToTree);
topTrees.add(topTree);
}
}
switch(t.e.eventData.kind) {
case 1:
t.e.eventData.isNotification = true;
Tree topTree = getTopTree(t);
topTrees.add(topTree);
break;
case 7:
Event event = t.e;
if(gDebug >= 0) ("Got notification of type 7");
String reactorId = event.eventData.pubkey;
int lastEIndex = event.eventData.eTagsRest.length - 1;
String reactedTo = event.eventData.eTagsRest[lastEIndex];
Event? reactedToEvent = allChildEventsMap[reactedTo]?.e;
if( reactedToEvent != null) {
Tree? reactedToTree = allChildEventsMap[reactedTo];
if( reactedToTree != null) {
reactedToTree.e.eventData.newLikes.add( reactorId);
Tree topTree = getTopTree(reactedToTree);
topTrees.add(topTree);
}
}
break;
default:
break;
}
}
});
// remove identidal entries
// remove duplicate events
// remove duplicate top trees
Set ids = {};
topTrees.retainWhere((t) => ids.add(t.e.eventData.id));
topTrees.forEach( (t) { t.printTree(0, false, 0); });
print("\n");
}
// Write the tree's events to file as one event's json per line
@ -259,17 +274,24 @@ class Tree {
//print("opening $filename to write to");
try {
final File file = File(filename);
// empty the file
await file.writeAsString("", mode: FileMode.writeOnly).then( (file) => file);
int eventCounter = 0;
String nLinesStr = "";
int countPosts = 0;
const int numLinesTogether = 200;
const int numLinesTogether = 100; // number of lines to write in one write call
int linesWritten = 0;
for( var k in allChildEventsMap.keys) {
Tree? t = allChildEventsMap[k];
if( t != null) {
String line = "${t.e.originalJson}\n";
nLinesStr += line;
eventCounter++;
eventCounter++;
if( t.e.eventData.kind == 1) {
countPosts++;
}
}
if( eventCounter % numLinesTogether == 0) {
@ -284,10 +306,12 @@ class Tree {
nLinesStr = "";
}
int len = await file.length();
} on Exception catch (e) {
//int len = await file.length();
print("\n\nWrote total $eventCounter events to file \"$gEventsFilename\" of which ${countPosts + 1} are posts.") ; // TODO remove extra 1
} on Exception catch (err) {
print("Could not open file $filename.");
}
}
return;
}
@ -319,11 +343,6 @@ class Tree {
}
}
}
if( latestEventId.isEmpty) {
// search for it in the dummy event id's
}
//print("latestEventId = $latestEventId");
if( latestEventId.isNotEmpty) {
@ -438,8 +457,8 @@ Tree getTree(List<Event> events) {
//print("Got a reaction for $reactedTo. Total number of reactions = ${gReactions[reactedTo]?.length}");
}
// remove all events other than kind 1, 7 and 3 ( posts)
events.removeWhere( (item) => item.eventData.kind != 1 && item.eventData.kind != 7 && item.eventData.kind != 3);
// remove all events other than kind 0, 1, 3 and 7
events.removeWhere( (item) => !Tree.typesInEventMap.contains(item.eventData.kind));
// remove bot events
events.removeWhere( (item) => gBots.contains(item.eventData.pubkey));

View File

@ -1,6 +1,6 @@
name: nostr_console
description: A nostr client built for terminal/console.
version: 0.0.3
version: 0.0.4
homepage: https://github.com/vishalxl/nostr_console
environment: