improved channel logic

fix: put messages in order in channels

improved dm menu. encryption etc still todo.
This commit is contained in:
Vishal 2022-09-01 21:55:51 +05:30
parent 582c422792
commit 74541890b4
5 changed files with 314 additions and 77 deletions

View File

@ -88,6 +88,27 @@ Future<void> sendChatMessage(Store node, String channelId, String messageToSend)
sendRequest( gListRelayUrls, toSendMessage);
}
// is same as above. remove it TODO
Future<void> sendDirectMessage(Store node, String otherPubkey, String messageToSend) async {
print("TBD");
return;
// TODO implement
String replyKind = "4";
String strTags = node.getTagStr(otherPubkey, exename);
int createdAt = DateTime.now().millisecondsSinceEpoch ~/1000;
String id = getShaId(userPublicKey, createdAt, replyKind, strTags, messageToSend);
String sig = sign(userPrivateKey, id, "12345612345612345612345612345612");
String toSendMessage = '["EVENT",{"id":"$id","pubkey":"$userPublicKey","created_at":$createdAt,"kind":$replyKind,"tags":[$strTags],"content":"$messageToSend","sig":"$sig"}]';
//relays.sendRequest(defaultServerUrl, toSendMessage);
sendRequest( gListRelayUrls, toSendMessage);
}
// sends event e; used to send kind 3 event
Future<void> sendEvent(Store node, Event e) async {
String strTags = "";
@ -436,7 +457,7 @@ Future<void> channelMenuUI(Store node) async {
int option = showMenu([ 'Show public channels', // 1
'Enter a public channel', // 2
'See personal Inbox',
'Send a direct message',
'Reply or Send a direct message',
'Go back to main menu'], // 5
"Channel Menu"); // name of menu
print('You picked: $option');
@ -493,9 +514,58 @@ Future<void> channelMenuUI(Store node) async {
break;
case 3:
print("total direct rooms = ${node.directRooms.length}");
//print("total direct rooms = ${node.directRooms.length}");
node.printDirectRoomInfo();
break;
case 4:
// in case the program was invoked with --pubkey, then user can't send messages
if( userPrivateKey == "") {
print("Since no private key has been supplied, messages and replies can't be sent. Invoke with --prikey \n");
break;
}
bool showChannelOption = true;
stdout.write("\nType user public key, or its 1st few letters; or type 'x' to go to menu: ");
String? $tempUserInput = stdin.readLineSync();
String directRoomId = $tempUserInput??"";
if( directRoomId == "x") {
showChannelOption = false;
}
int pageNum = 1;
while(showChannelOption) {
String fullChannelId = node.showDirectRoom(directRoomId, pageNum);
if( fullChannelId == "") {
print("Could not find the given direct room.");
showChannelOption = false;
break;
}
stdout.write("\nType message; or type 'x' to exit, or press <enter> to refresh: ");
$tempUserInput = stdin.readLineSync(encoding: utf8);
String messageToSend = $tempUserInput??"";
if( messageToSend != "") {
if( messageToSend == 'x') {
showChannelOption = false;
} else {
if( messageToSend.isChannelPageNumber(gMaxChannelPagesDisplayed) ) {
pageNum = (int.tryParse(messageToSend))??1;
} else {
// send message to the given room
await sendChatMessage(node, fullChannelId, messageToSend);
pageNum = 1; // reset it
}
}
} else {
print("Refreshing...");
}
await processNotifications(node);
}
break;
case 5:
continueChatMenu = false;
@ -513,7 +583,7 @@ Future<void> mainMenuUi(Store node) async {
// at the very beginning, show the tree as it is, and then show the options menu
bool hasRepliesAndLikes (Tree t) => t.hasRepliesAndLikes(userPublicKey);
//node.printTree(0, DateTime.now().subtract(Duration(days:gNumLastDays)), hasRepliesAndLikes);
node.printTree(0, DateTime.now().subtract(Duration(days:gNumLastDays)), hasRepliesAndLikes);
bool userContinue = true;
while(userContinue) {
@ -542,7 +612,7 @@ Future<void> mainMenuUi(Store node) async {
print('You picked: $option');
switch(option) {
case 1:
//node.printTree(0, DateTime.now().subtract(Duration(days:gNumLastDays)), selectAll);
node.printTree(0, DateTime.now().subtract(Duration(days:gNumLastDays)), selectAll);
break;
case 2:

View File

@ -5,7 +5,21 @@ import 'package:translator/translator.dart';
import 'package:crypto/crypto.dart';
import 'package:nostr_console/settings.dart';
int gDebug = 1;
import 'package:kepler/kepler.dart';
import "dart:typed_data";
import "dart:math";
import 'dart:convert' as convert;
import "package:pointycastle/export.dart";
import 'package:hex/hex.dart';
import 'package:base58check/base58.dart';
import 'package:pointycastle/src/impl/secure_random_base.dart';
import "package:pointycastle/src/registry/registry.dart";
import "package:pointycastle/src/ufixnum.dart";
import "package:pointycastle/export.dart";
int gDebug = 0;
// translate
final translator = GoogleTranslator();
@ -125,12 +139,9 @@ class EventData {
}
}
if(gDebug >= 2 ) {
print("----------------------------------------Creating EventData with content: ${json['content']}");
}
if( json['id'] == gCheckEventId) {
if(gDebug > 0) print("In Event fromJson: got message: $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,
@ -168,13 +179,19 @@ class EventData {
}
void translateAndExpandMentions() {
if (content == "") {
return;
}
if( evaluatedContent == "") {
evaluatedContent = expandMentions(content);
if( evaluatedContent != "") {
return;
}
switch(kind) {
case 1:
evaluatedContent = expandMentions(content);
if( gTranslate && !evaluatedContent.isEnglish()) {
if( gDebug > 0) print("found that this comment is non-English: $evaluatedContent");
//final input = "Здравствуйте. Ты в порядке?";
@ -187,14 +204,84 @@ class EventData {
.translate(content, to: 'en')
//.catchError( (error, stackTrace) => null )
.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:
break;
// not implemented yet. has issues.
if(!isUserDirectMessage(this)) {
break;
}
printEventData(0);
print("\n\n");
{ // sample from kepler
var alice = Kepler.generateKeyPair();
print(
"alice private key: " +
Kepler.strinifyPrivateKey(alice.privateKey as ECPrivateKey),
);
print(
"alice public key: " +
Kepler.strinifyPublicKey(alice.publicKey as ECPublicKey),
);
// Create Bob's keypair
var bob = Kepler.generateKeyPair();
// This is what alice wants to say to bob
var rawStr = 'Encrypt and decrypt data using secp256k1';
// use alic's privatekey and bob's publickey means alice says to bob
var encMap = Kepler.pubkeyEncrypt(
Kepler.strinifyPrivateKey(alice.privateKey as ECPrivateKey),
Kepler.strinifyPublicKey(bob.publicKey as ECPublicKey),
rawStr,
);
// Get encrypted base64 string
var encStr = encMap['enc'];
print("encrypted text: " + encStr!);
// Get random IV
var iv = encMap['iv'];
print("iv: " + iv!);
// Now, you can send enc_str and IV to Bob
// Use bob's privatekey and alice's publickey to decrypt alices message, for Bob to read.
var decryptd = Kepler.privateDecrypt(
Kepler.strinifyPrivateKey(bob.privateKey as ECPrivateKey),
Kepler.strinifyPublicKey(alice.publicKey as ECPublicKey),
encStr,
iv,
);
print('decrypted text: $decryptd');
}
}
print("in translateAndExpandMentions() for a dm pubkey = $pubkey content = $content");
print("tags = $tags");
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");
var decryptd = myPrivateDecrypt( userPrivateKey, "03" + pubkey, enc_str, iv); // use bob's privatekey and alic's publickey means bob can read message from alic
print('they say:${decryptd}');
break;
} // end switch
} // end translateAndExpandMentions
// only applicable for kind 42 event
String getChatRoomId() {
@ -394,14 +481,18 @@ Set<Event> readEventsFromFile(String filename) {
try {
List<String> lines = file.readAsLinesSync();
for( int i = 0; i < lines.length; i++ ) {
Event e = Event.fromJson(lines[i], "");
events.add(e);
Event e = Event.fromJson(lines[i], "");
if( e.eventData.id == gCheckEventId) {
print("read $gCheckEventId from file");
}
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;
}
@ -532,7 +623,7 @@ String getAuthorName(String pubkey) {
Set<String> getPublicKeyFromName(String userName) {
Set<String> pubkeys = {};
if(gDebug > 0) print("In getPublicKeyFromName: doing lookup for $userName len of gKindONames= ${gKindONames.length}");
if(gDebug >= 0) print("In getPublicKeyFromName: doing lookup for $userName len of gKindONames= ${gKindONames.length}");
gKindONames.forEach((pk, value) {
// check both the user name, and the pubkey to search for the user
@ -710,3 +801,57 @@ String getPrintableDate(int createdAt) {
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 encdData,
[String b64IV = ""]) {
final secretIV = Kepler.byteSecret(privateString, publicString);
final secret = Uint8List.fromList(secretIV[0]);
final iv = b64IV.length > 6
? convert.base64.decode(b64IV)
: Uint8List.fromList(secretIV[1]);
ChaCha20Engine _cipher = ChaCha20Engine();
_cipher.reset();
_cipher.init(false, buildParams(secret, iv));
return _cipher.process(encdData);
}
ParametersWithIV<KeyParameter> buildParams(
Uint8List key, Uint8List iv) {
return ParametersWithIV<KeyParameter>(KeyParameter(key), iv);
}

View File

@ -3,7 +3,7 @@ import 'package:logging/logging.dart';
final log = Logger('ExampleLogger');
// for debugging
String gCheckEventId = "15d86a36a620fc1f735f2322f31366b2adde786361f568faf6a0dc8368f7e534";
String gCheckEventId = ""; //"1763016774ceaa8c135dce01e77923994c5afad4cd3e126704a1292ebb1a577e"; //"15d86a36a620fc1f735f2322f31366b2adde786361f568faf6a0dc8368f7e534";
const int gDefaultNumWaitSeconds = 3000; // is used in main()

View File

@ -1,6 +1,5 @@
import 'dart:io';
import 'dart:convert';
import 'dart:math';
import 'package:nostr_console/event_ds.dart';
import 'package:nostr_console/settings.dart';
@ -32,7 +31,7 @@ class ScrollableMessages {
page = 1;
}
print(topHeader);
//print(topHeader);
String displayName = topHeader;
@ -79,7 +78,7 @@ class ChatRoom extends ScrollableMessages {
String about;
String picture;
ChatRoom(this.chatRoomId, this.internalChatRoomName, this.about, this.picture, messageIds) :
ChatRoom(this.chatRoomId, this.internalChatRoomName, this.about, this.picture, List<String> messageIds) :
super ( "${internalChatRoomName} ( ${chatRoomId.substring(0, 6)}", messageIds);
String get chatRoomName {
@ -374,21 +373,21 @@ class Store {
switch(eKind) {
case 42:
{
//numKind42Events++;
if( gCheckEventId == ce.eventData.id) print("In handleChannelEvents: processing $gCheckEventId ");
String chatRoomId = ce.eventData.getChatRoomId();
if( chatRoomId != "") {
if( chatRoomId != "") { // sometimes people may forget to give e tags or give wrong tags like #e
if( rooms.containsKey(chatRoomId)) {
//if( gDebug > 0) print("Adding new message $key to a chat room $chatRoomId. ");
if( gDebug > 0) print("chat room already exists = $chatRoomId adding event to it" );
if( gCheckEventId == ce.eventData.id) print("Adding new message $eId to a chat room $chatRoomId. ");
addMessageToChannel(chatRoomId, eId, tempChildEventsMap, rooms);
} else {
List<String> temp = [];
temp.add(eId);
ChatRoom room = ChatRoom(chatRoomId, "", "", "", temp);
rooms[chatRoomId] = room;
//if( gDebug > 0) print("Added new chat room object $chatRoomId and added message to it. ");
if( gCheckEventId == ce.eventData.id) print("Adding new message $eId to NEW chat room $chatRoomId. ");
rooms[chatRoomId] = ChatRoom(chatRoomId, "", "", "", []);
addMessageToChannel(chatRoomId, eId, tempChildEventsMap, rooms);
}
} else {
//if( gDebug > 0) print("Could not get chat room id for event $eId, its original json: ");
}
}
break;
@ -427,28 +426,11 @@ class Store {
} // end switch
}
static bool isUserDirectMessage(Event directMessage) {
if( directMessage.eventData.pubkey == userPublicKey) {
return true;
}
bool sentToUser = false;
directMessage.eventData.tags.forEach((tag) {
if( tag.length < 2 )
return;
if( tag[0] == "p" && tag[1] == userPublicKey) {
sentToUser = true;
}
});
return sentToUser;
}
static void handleDirectMessages( Map<String, DirectMessageRoom> directRooms, Map<String, Tree> tempChildEventsMap, Event ce) {
String eId = ce.eventData.id;
int eKind = ce.eventData.kind;
if( !isUserDirectMessage(ce)) {
if( !isUserDirectMessage(ce.eventData)) {
return;
}
@ -458,17 +440,17 @@ class Store {
String directRoomId = getDirectRoomId(ce.eventData);
if( directRoomId != "") {
if( directRooms.containsKey(directRoomId)) {
if( gDebug >= 0) print("Adding new message ${ce.eventData.id} to a direct room $directRoomId. ");
if( gDebug > 0) print("Adding new message ${ce.eventData.id} to a direct room $directRoomId sender pubkey = ${ce.eventData.pubkey}. ");
addMessageToDirectRoom(directRoomId, eId, tempChildEventsMap, directRooms);
} else {
List<String> temp = [];
temp.add(eId);
DirectMessageRoom newDirectRoom= DirectMessageRoom(directRoomId, temp);
directRooms[directRoomId] = newDirectRoom;
if( gDebug >= 0) print("Adding new message ${ce.eventData.id} to NEW direct room $directRoomId. ");
if( gDebug > 0) print("Adding new message ${ce.eventData.id} to NEW direct room $directRoomId. sender pubkey = ${ce.eventData.pubkey}.");
}
} else {
if( gDebug > 0) print("Could not get chat room id for event ${ce.eventData.id}, its original json: ");
if( gDebug > 0) print("Could not get chat room id for event ${ce.eventData.id} sender pubkey = ${ce.eventData.pubkey}.");
}
}
break;
@ -528,7 +510,7 @@ class Store {
// is not a parent, find its parent and then add this element to that parent Tree
String parentId = tree.event.eventData.getParent();
if( tree.event.eventData.id == gCheckEventId) {
if(gDebug > 0) print("In Tree FromEvents: got id: $gCheckEventId");
if(gDebug >= 0) print("In Tree FromEvents: got id: $gCheckEventId");
}
if(tempChildEventsMap.containsKey( parentId)) {
@ -600,7 +582,7 @@ class Store {
return;
}
if( !isUserDirectMessage(newEvent)) { // direct message not relevant to user are ignored
if( !isUserDirectMessage(newEvent.eventData)) { // direct message not relevant to user are ignored
return;
}
@ -611,7 +593,8 @@ class Store {
}
// expand mentions ( and translate if flag is set) and then add event to main event map
newEvent.eventData.translateAndExpandMentions();
newEvent.eventData.translateAndExpandMentions(); // this also handles dm decryption for kind 4 messages, for kind 1 will do translation/expansion;
eventsNotReadFromFile.add(newEvent.eventData.id); // used later so that only these events are appended to the file
// add them to the main store of the Tree object
@ -651,31 +634,37 @@ class Store {
case 4:
// add kind 4 direct chat message event to its direct massage room
String directRoomId = getDirectRoomId(newTree.event.eventData);
print("in insert events: got directRoomId = ${directRoomId}");
//print("in insert events: got directRoomId = ${directRoomId}");
if( directRoomId != "") {
if( directRooms.containsKey(directRoomId)) {
if( gDebug > 0) print("added event to chat room in insert event");
if( gDebug > 0) print("added event to direct room in insert event");
addMessageToDirectRoom(directRoomId, newTree.event.eventData.id, allChildEventsMap, directRooms);
newTree.event.eventData.isNotification = true; // highlight it too in next printing
print(" in from event: added it to a direct room");
//print(" in from event: added it to a direct room");
break;
}
} else {
print(" in insert events, could not find parent/channel id");
}
List<String> temp = [];
temp.add(newTree.event.eventData.id);
directRooms[directRoomId] = DirectMessageRoom(directRoomId, temp); // TODO sort it
break;
case 42:
newTree.event.eventData.isNotification = true; // highlight it too in next printing
// add 42 chat message event id to its chat room
String channelId = newTree.event.eventData.getParent();
if( channelId != "") {
if( chatRooms.containsKey(channelId)) {
if( gDebug > 0) print("added event to chat room in insert event");
addMessageToChannel(channelId, newTree.event.eventData.id, allChildEventsMap, chatRooms); // adds in order
break;
} else {
chatRooms[channelId] = ChatRoom(channelId, "", "", "", []);
addMessageToChannel(channelId, newTree.event.eventData.id, allChildEventsMap, chatRooms);
newTree.event.eventData.isNotification = true; // highlight it too in next printing
}
} else {
print("info: in insert events, could not find parent/channel id");
}
}
break;
default:
break;
@ -852,13 +841,8 @@ class Store {
print("\n\nDirect messages inbox:");
printUnderlined(" From Num of Messages Latest Message ");
directRooms.forEach((key, value) {
String name = "direct room name";
/* if( value.chatRoomName == "") {
name = value.chatRoomId.substring(0, 6);
} else {
name = "${value.chatRoomName} ( ${value.chatRoomId.substring(0, 6)})";
}
*/
String name = getAuthorName(key);
int numMessages = value.messageIds.length;
stdout.write("${name} ${getNumSpaces(32-name.length)} $numMessages${getNumSpaces(12- numMessages.toString().length)}");
List<String> messageIds = value.messageIds;
@ -875,6 +859,28 @@ class Store {
});
}
// shows the given directRoomId, where directRoomId is prefix-id or pubkey of the other user. returns full id of other user.
String showDirectRoom(String directRoomId, [int page = 1]) {
if( !directRooms.containsKey(directRoomId)) {
return "";
}
if( directRoomId.length != 64) {
return "";
}
for( String key in directRooms.keys) {
if( key == directRoomId ) {
DirectMessageRoom? room = directRooms[key];
if( room != null) {
printDirectMessageRoom(room, page);
}
return key;
}
}
return "";
}
void printChannel(ChatRoom room, [int page = 1]) {
if( page < 1) {
@ -897,6 +903,9 @@ class Store {
// shows the given channelId, where channelId is prefix-id or channel name as mentioned in room.name. returns full id of channel.
String showChannel(String channelId, [int page = 1]) {
if( channelId.length > 64 ) {
return "";
}
for( String key in chatRooms.keys) {
if( key.substring(0, channelId.length) == channelId ) {
@ -1173,28 +1182,33 @@ class Store {
void addMessageToChannel(String channelId, String messageId, Map<String, Tree> tempChildEventsMap, var chatRooms) {
int newEventTime = (tempChildEventsMap[messageId]?.event.eventData.createdAt??0);
if( gCheckEventId == messageId) {
print("In addMessageToChannel: newEventTime= $newEventTime");
//gDebug = 1;
}
if( chatRooms.containsKey(channelId)) {
ChatRoom? room = chatRooms[channelId];
if( room != null ) {
if( room.messageIds.isEmpty) {
//if(gDebug> 0) print("room is empty. adding new message and returning. ");
if(gDebug> 0 || gCheckEventId == messageId) print("room is empty. adding new message and returning. ");
room.messageIds.add(messageId);
return;
}
if(gDebug> 0) print("room has ${room.messageIds.length} messages already. adding new one to it. ");
if(gDebug> 0 || gCheckEventId == messageId) print("room has ${room.messageIds.length} messages already. adding new one to it. ");
for(int i = 0; i < room.messageIds.length; i++) {
int eventTime = (tempChildEventsMap[room.messageIds[i]]?.event.eventData.createdAt??0);
if( newEventTime < eventTime) {
// shift current i and rest one to the right, and put event Time here
if(gDebug> 0) print("In addMessageToChannel: inserted in middle to channel ${room.chatRoomId} ");
if(gDebug> 0 || gCheckEventId == messageId )
print("In addMessageToChannel: inserted event $messageId at position $i to channel ${room.chatRoomId} ");
room.messageIds.insert(i, messageId);
return;
}
}
if(gDebug> 0) print("In addMessageToChannel: added to channel ${room.chatRoomId} ");
if(gDebug> 0 || gCheckEventId == messageId) print("In addMessageToChannel: added to channel ${room.chatRoomId} at end");
// insert at end
room.messageIds.add(messageId);
@ -1377,8 +1391,8 @@ Store getTree(Set<Event> events) {
// sort all participants by id; then create a large string with them together, thats the unique id for now
String getDirectRoomId(EventData eventData) {
List<String> participantIds = [];
String roomId = "";
eventData.tags.forEach((tag) {
if( tag.length < 2)
return;
@ -1391,5 +1405,11 @@ String getDirectRoomId(EventData eventData) {
participantIds.sort();
String uniqueId = "";
participantIds.forEach((element) {uniqueId += element;});
return uniqueId;
if( eventData.pubkey == userPublicKey) {
return uniqueId;
} else {
return eventData.pubkey;
}
}

View File

@ -21,3 +21,5 @@ dependencies:
translator: ^0.1.7
web_socket_channel: ^2.2.0
logging: ^1.0.2
kepler: ^1.0.3
# secp256k1cipher: ^0.2.7