diff --git a/nomadnet/Conversation.py b/nomadnet/Conversation.py index fa250f6..52479ab 100644 --- a/nomadnet/Conversation.py +++ b/nomadnet/Conversation.py @@ -2,6 +2,7 @@ import os import RNS import LXMF import shutil +import nomadnet from nomadnet.Directory import DirectoryEntry class Conversation: @@ -77,7 +78,7 @@ class Conversation: except Exception as e: RNS.log("Could not remove conversation at "+str(conversation_path)+". The contained exception was: "+str(e), RNS.LOG_ERROR) - def __init__(self, source_hash, app): + def __init__(self, source_hash, app, initiator=False): self.app = app self.source_hash = source_hash self.send_destination = None @@ -96,6 +97,12 @@ class Conversation: self.source_known = True self.send_destination = RNS.Destination(self.source_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery") + if initiator: + if not os.path.isdir(self.messages_path): + os.makedirs(self.messages_path) + if Conversation.created_callback != None: + Conversation.created_callback() + self.scan_storage() self.trust_level = app.directory.trust_level(bytes.fromhex(self.source_hash)) @@ -113,6 +120,38 @@ class Conversation: if self.__changed_callback != None: self.__changed_callback(self) + def purge_failed(self): + purged_messages = [] + for conversation_message in self.messages: + if conversation_message.get_state() == LXMF.LXMessage.FAILED: + purged_messages.append(conversation_message) + conversation_message.purge() + + for purged_message in purged_messages: + self.messages.remove(purged_message) + + def register_changed_callback(self, callback): + self.__changed_callback = callback + + def send(self, content="", title=""): + if self.send_destination: + dest = self.send_destination + source = self.app.lxmf_destination + lxm = LXMF.LXMessage(dest, source, content, desired_method=LXMF.LXMessage.DIRECT) + lxm.register_delivery_callback(self.message_notification) + lxm.register_failed_callback(self.message_notification) + self.app.message_router.handle_outbound(lxm) + + message_path = Conversation.ingest(lxm, self.app, originator=True) + self.messages.append(ConversationMessage(message_path)) + + return True + else: + RNS.log("Path to destination is not known, cannot create LXMF Message.", RNS.LOG_VERBOSE) + return False + + def message_notification(self, message): + message_path = Conversation.ingest(message, self.app, originator=True) def __str__(self): string = self.source_hash @@ -124,28 +163,6 @@ class Conversation: return string - def register_changed_callback(self, callback): - self.__changed_callback = callback - - def send(self, content="", title=""): - if self.send_destination: - dest = self.send_destination - source = self.app.lxmf_destination - lxm = LXMF.LXMessage(dest, source, content, desired_method=LXMF.LXMessage.DIRECT) - lxm.register_delivery_callback(self.message_delivered) - self.app.message_router.handle_outbound(lxm) - - message_path = Conversation.ingest(lxm, self.app, originator=True) - self.messages.append(ConversationMessage(message_path)) - - else: - # TODO: Implement - # Alter UI so message cannot be sent until there is a path, or LXMF propagation is implemented - RNS.log("Destination unknown") - - def message_delivered(self, message): - message_path = Conversation.ingest(message, self.app, originator=True) - class ConversationMessage: @@ -160,6 +177,16 @@ class ConversationMessage: self.lxm = LXMF.LXMessage.unpack_from_file(open(self.file_path, "rb")) self.loaded = True self.timestamp = self.lxm.timestamp + + if self.lxm.state > LXMF.LXMessage.DRAFT and self.lxm.state < LXMF.LXMessage.SENT: + found = False + for pending in nomadnet.NomadNetworkApp.get_shared_instance().message_router.pending_outbound: + if pending.hash == self.lxm.hash: + found = True + + if not found: + self.lxm.state = LXMF.LXMessage.FAILED + except Exception as e: RNS.log("Error while loading LXMF message "+str(self.file_path)+" from disk. The contained exception was: "+str(e), RNS.LOG_ERROR) @@ -167,6 +194,11 @@ class ConversationMessage: self.loaded = False self.lxm = None + def purge(self): + self.unload() + if os.path.isfile(self.file_path): + os.unlink(self.file_path) + def get_timestamp(self): if not self.loaded: self.load() @@ -191,6 +223,12 @@ class ConversationMessage: return self.lxm.hash + def get_state(self): + if not self.loaded: + self.load() + + return self.lxm.state + def signature_validated(self): if not self.loaded: self.load() diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py index 1d13d1e..bc64709 100644 --- a/nomadnet/ui/TextUI.py +++ b/nomadnet/ui/TextUI.py @@ -28,12 +28,15 @@ THEMES = { ("msg_header_caution", 'black', 'yellow', 'standout', '#111', '#fd3'), ("msg_header_sent", 'black', 'light gray', 'standout', '#111', '#ddd'), ("msg_header_delivered", 'black', 'light blue', 'standout', '#111', '#28b'), - ("msg_warning_untrusted", 'black', 'dark red', 'standout', 'black', 'dark red'), + ("msg_header_failed", 'black', 'dark gray', 'standout', 'black', 'dark gray'), + ("msg_warning_untrusted", 'black', 'dark red', 'standout', '#111', 'dark red'), ("list_focus", "black", "light gray", "standout", "#111", "#bbb"), ("list_off_focus", "black", "dark gray", "standout", "#111", "dark gray"), ("list_trusted", "light green", "default", "default", "#6b2", "default"), + ("list_focus_trusted", "black", "light gray", "standout", "#180", "#bbb"), ("list_unknown", "dark gray", "default", "default", "light gray", "default"), ("list_untrusted", "dark red", "default", "default", "dark red", "default"), + ("list_focus_untrusted", "black", "light gray", "standout", "#810", "#bbb"), ] } diff --git a/nomadnet/ui/textui/Conversations.py b/nomadnet/ui/textui/Conversations.py index 2587980..cd301a6 100644 --- a/nomadnet/ui/textui/Conversations.py +++ b/nomadnet/ui/textui/Conversations.py @@ -12,18 +12,18 @@ class ConversationListDisplayShortcuts(): def __init__(self, app): self.app = app - self.widget = urwid.AttrMap(urwid.Text("[Enter] Open [C-a] Add to directory [C-x] Delete [C-n] New"), "shortcutbar") + self.widget = urwid.AttrMap(urwid.Text("[Enter] Open [C-e] Directory Entry [C-x] Delete [C-n] New"), "shortcutbar") class ConversationDisplayShortcuts(): def __init__(self, app): self.app = app - self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-k] Clear [C-t] Add Title [C-w] Close Conversation"), "shortcutbar") + self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-k] Clear [C-t] Add Title [C-w] Close Conversation [C-p] Purge Failed"), "shortcutbar") class ConversationsArea(urwid.LineBox): def keypress(self, size, key): - if key == "ctrl a": - self.delegate.add_selected_to_directory() + if key == "ctrl e": + self.delegate.edit_selected_in_directory() elif key == "ctrl x": self.delegate.delete_selected_conversation() elif key == "ctrl n": @@ -109,18 +109,43 @@ class ConversationsDisplay(): options = self.columns_widget.options("weight", ConversationsDisplay.list_width) self.columns_widget.contents[0] = (overlay, options) - def add_selected_to_directory(self): + def edit_selected_in_directory(self): self.dialog_open = True - source_hash = self.ilb.get_selected_item().source_hash + source_hash_text = self.ilb.get_selected_item().source_hash display_name = self.ilb.get_selected_item().display_name - e_id = urwid.Edit(caption="ID : ",edit_text=source_hash) + e_id = urwid.Edit(caption="ID : ",edit_text=source_hash_text) + t_id = urwid.Text("ID : "+source_hash_text) e_name = urwid.Edit(caption="Name : ",edit_text=display_name) + selected_id_widget = t_id + + untrusted_selected = False + unknown_selected = True + trusted_selected = False + + try: + if self.app.directory.find(bytes.fromhex(source_hash_text)): + trust_level = self.app.directory.trust_level(bytes.fromhex(source_hash_text)) + if trust_level == DirectoryEntry.UNTRUSTED: + untrusted_selected = True + unknown_selected = False + trusted_selected = False + elif trust_level == DirectoryEntry.UNKNOWN: + untrusted_selected = False + unknown_selected = True + trusted_selected = False + elif trust_level == DirectoryEntry.TRUSTED: + untrusted_selected = False + unknown_selected = False + trusted_selected = True + except Exception as e: + RNS.log("EXC: "+str(e)) + trust_button_group = [] - r_untrusted = urwid.RadioButton(trust_button_group, "Untrusted") - r_unknown = urwid.RadioButton(trust_button_group, "Unknown", state=True) - r_trusted = urwid.RadioButton(trust_button_group, "Trusted") + r_untrusted = urwid.RadioButton(trust_button_group, "Untrusted", state=untrusted_selected) + r_unknown = urwid.RadioButton(trust_button_group, "Unknown", state=unknown_selected) + r_trusted = urwid.RadioButton(trust_button_group, "Trusted", state=trusted_selected) def dismiss_dialog(sender): self.update_conversation_list() @@ -149,18 +174,18 @@ class ConversationsDisplay(): dialog_pile.contents.append((urwid.Text(("error_text", "Could not save entry. Check your input."), align="center"), options)) dialog_pile = urwid.Pile([ - e_id, + selected_id_widget, e_name, urwid.Text(""), r_untrusted, r_unknown, r_trusted, urwid.Text(""), - urwid.Columns([("weight", 0.45, urwid.Button("Add", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Cancel", on_press=dismiss_dialog))]) + urwid.Columns([("weight", 0.45, urwid.Button("Save", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Cancel", on_press=dismiss_dialog))]) ]) dialog_pile.error_display = False - dialog = urwid.LineBox(dialog_pile, title="Add to Directory") + dialog = urwid.LineBox(dialog_pile, title="Edit Directory Entry") bottom = self.listbox overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) @@ -169,7 +194,72 @@ class ConversationsDisplay(): self.columns_widget.contents[0] = (overlay, options) def new_conversation(self): - pass + self.dialog_open = True + source_hash = "" + display_name = "" + + e_id = urwid.Edit(caption="ID : ",edit_text=source_hash) + e_name = urwid.Edit(caption="Name : ",edit_text=display_name) + + trust_button_group = [] + r_untrusted = urwid.RadioButton(trust_button_group, "Untrusted") + r_unknown = urwid.RadioButton(trust_button_group, "Unknown", state=True) + r_trusted = urwid.RadioButton(trust_button_group, "Trusted") + + def dismiss_dialog(sender): + self.update_conversation_list() + self.dialog_open = False + + def confirmed(sender): + try: + existing_conversations = nomadnet.Conversation.conversation_list(self.app) + + display_name = e_name.get_edit_text() + source_hash_text = e_id.get_edit_text() + source_hash = bytes.fromhex(source_hash_text) + trust_level = DirectoryEntry.UNTRUSTED + if r_unknown.state == True: + trust_level = DirectoryEntry.UNKNOWN + elif r_trusted.state == True: + trust_level = DirectoryEntry.TRUSTED + + if not source_hash in [c[0] for c in existing_conversations]: + entry = DirectoryEntry(source_hash, display_name, trust_level) + self.app.directory.remember(entry) + + new_conversation = nomadnet.Conversation(source_hash_text, nomadnet.NomadNetworkApp.get_shared_instance(), initiator=True) + self.update_conversation_list() + + self.display_conversation(source_hash_text) + self.dialog_open = False + + except Exception as e: + RNS.log("Could not start conversation. The contained exception was: "+str(e), RNS.LOG_VERBOSE) + if not dialog_pile.error_display: + dialog_pile.error_display = True + options = dialog_pile.options(height_type="pack") + dialog_pile.contents.append((urwid.Text(""), options)) + dialog_pile.contents.append((urwid.Text(("error_text", "Could not start conversation. Check your input."), align="center"), options)) + + dialog_pile = urwid.Pile([ + e_id, + e_name, + urwid.Text(""), + r_untrusted, + r_unknown, + r_trusted, + urwid.Text(""), + urwid.Columns([("weight", 0.45, urwid.Button("Start", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Cancel", on_press=dismiss_dialog))]) + ]) + dialog_pile.error_display = False + + dialog = urwid.LineBox(dialog_pile, title="New Conversation") + bottom = self.listbox + + overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2) + + options = self.columns_widget.options("weight", ConversationsDisplay.list_width) + self.columns_widget.contents[0] = (overlay, options) def delete_conversation(self, source_hash): if source_hash in ConversationsDisplay.cached_conversation_widgets: @@ -220,20 +310,25 @@ class ConversationsDisplay(): source_hash = conversation[0] if trust_level == DirectoryEntry.UNTRUSTED: - symbol = "x" - style = "list_untrusted" + symbol = "\u2715" + style = "list_untrusted" + focus_style = "list_focus_untrusted" elif trust_level == DirectoryEntry.UNKNOWN: - symbol = "?" - style = "list_unknown" + symbol = "?" + style = "list_unknown" + focus_style = "list_focus" elif trust_level == DirectoryEntry.TRUSTED: - symbol = "\u2713" - style = "list_trusted" + symbol = "\u2713" + style = "list_trusted" + focus_style = "list_focus_trusted" elif trust_level == DirectoryEntry.WARNING: - symbol = "\u26A0" - style = "list_warning" + symbol = "\u26A0" + style = "list_warning" + focus_style = "list_focus" else: - symbol = "\u26A0" - style = "list_warning" + symbol = "\u26A0" + style = "list_untrusted" + focus_style = "list_focus_untrusted" display_text = symbol if display_name != None: @@ -244,7 +339,7 @@ class ConversationsDisplay(): widget = ListEntry(display_text) urwid.connect_signal(widget, "click", self.display_conversation, conversation[0]) - display_widget = urwid.AttrMap(widget, style, "list_focus") + display_widget = urwid.AttrMap(widget, style, focus_style) display_widget.source_hash = source_hash display_widget.display_name = display_name @@ -290,8 +385,6 @@ class MessageEdit(urwid.Edit): self.delegate.send_message() elif key == "ctrl k": self.delegate.clear_editor() - elif key == "ctrl w": - self.delegate.close() else: return super(MessageEdit, self).keypress(size, key) @@ -335,6 +428,15 @@ class ConversationWidget(urwid.WidgetWrap): urwid.WidgetWrap.__init__(self, self.display_widget) + def keypress(self, size, key): + if key == "ctrl w": + self.close() + elif key == "ctrl p": + self.conversation.purge_failed() + self.conversation_changed(None) + else: + return super(ConversationWidget, self).keypress(size, key) + def conversation_changed(self, conversation): self.update_message_widgets(replace = True) @@ -389,6 +491,9 @@ class LXMessageWidget(urwid.WidgetWrap): if message.lxm.state == LXMF.LXMessage.DELIVERED: header_style = "msg_header_delivered" title_string = "\u2713 " + title_string + elif message.lxm.state == LXMF.LXMessage.FAILED: + header_style = "msg_header_failed" + title_string = "\u2715 " + title_string else: header_style = "msg_header_sent" title_string = "\u2192 " + title_string diff --git a/nomadnet/ui/textui/Main.py b/nomadnet/ui/textui/Main.py index 6e483f8..969ddcd 100644 --- a/nomadnet/ui/textui/Main.py +++ b/nomadnet/ui/textui/Main.py @@ -15,7 +15,7 @@ class SubDisplays(): self.directory_display = DirectoryDisplay(self.app) self.map_display = MapDisplay(self.app) - self.active_display = self.network_display + self.active_display = self.conversations_display def active(self): return self.active_display @@ -119,7 +119,8 @@ class MenuDisplay(): button_directory = (13, MenuButton("Directory", on_press=handler.show_directory)) button_map = (7, MenuButton("Map", on_press=handler.show_map)) - buttons = [menu_text, button_network, button_conversations, button_directory, button_map] + # buttons = [menu_text, button_conversations, button_node, button_directory, button_map] + buttons = [menu_text, button_conversations, button_network] columns = urwid.Columns(buttons, dividechars=1) self.widget = urwid.AttrMap(columns, "menubar")