From 70e49c8e6fa7a7fe540a7356533a0a53953a0ed9 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Thu, 7 Apr 2022 21:03:53 +0200 Subject: [PATCH] Initial commit --- Makefile | 29 ++ README.md | 13 + assets/icon.png | Bin 0 -> 44010 bytes assets/presplash.png | Bin 0 -> 40632 bytes main.py | 574 ++++++++++++++++++++++++++++ setup.py | 30 ++ sideband/_version.py | 2 + sideband/core.py | 888 +++++++++++++++++++++++++++++++++++++++++++ ui/announces.py | 151 ++++++++ ui/conversations.py | 228 +++++++++++ ui/helpers.py | 30 ++ ui/layouts.py | 675 ++++++++++++++++++++++++++++++++ ui/messages.py | 207 ++++++++++ 13 files changed, 2827 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/icon.png create mode 100644 assets/presplash.png create mode 100644 main.py create mode 100644 setup.py create mode 100644 sideband/_version.py create mode 100644 sideband/core.py create mode 100644 ui/announces.py create mode 100644 ui/conversations.py create mode 100644 ui/helpers.py create mode 100644 ui/layouts.py create mode 100644 ui/messages.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ab730fb --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +all: prepare debug + +prepare: activate + +clean: + buildozer android clean + +activate: + (. venv/bin/activate) + (mv setup.py setup.disabled) + +debug: + buildozer android debug + +release: + buildozer android release + +apk: prepare release + +devapk: prepare debug + +install: + adb install bin/sideband-0.0.1-arm64-v8a-debug.apk + +console: + (adb logcat | grep python) + +getrns: + (rm ./RNS -r;cp -rv ../Reticulum/RNS ./;rm ./RNS/Utilities/RNS;rm ./RNS/__pycache__ -r) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6455ece --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Sideband + +Sideband is an LXMF client for Android, Linux and macOS. It allows you to communicate with other people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, or anything else Reticulum supports. + +Sideband is completely free, anonymous and infrastructure-less. Sideband uses the completely peer-to-peer and distributed messaging system [LXMF](https://github.com/markqvist/lxmf "LXMF"). There is no service providers, no "end-user license agreements", no data theft and no surveillance. You own the system. + +Sideband currently includes basic functionality for secure and independent communication, and many useful features are planned for implementation. To get a feel for the idea behind Sideband, you can download it and try it out now. Please help make all the functionality a reality by supporting the development with donations. + +Sideband is currently provided as an early alpha-quality preview release. No user support is provided other than the published articles, tutorials and documentation resources. You are welcome to try it out and use it in any way you please. Please report any bugs or unexpected behaviours. Please expect this program to implode the known universe. + +If you want to help develop this program, get in touch. + +Please check back shortly for downloadable APK files for your device. diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f935d61790af6e6a2af9417c04898106890ebdbb GIT binary patch literal 44010 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 n;5J6n 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 literal 0 HcmV?d00001 diff --git a/assets/presplash.png b/assets/presplash.png new file mode 100644 index 0000000000000000000000000000000000000000..bf018653161a08206062eecc0b6d05d0cc211a39 GIT binary patch literal 40632 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

XB)xvkc;vjrR zxQ{2ThmvN>2fV9*#YCY89ge zJ89Q&GGXbf2%D)YV>V%}rU-JHkF9XL62 z))K_v9UgHXvyqG1>obl?H6)%ySSIt=6PWi>!JssM&J=}m^;{?jMgawCzXIHVjE&iy zHM|CB%f`T#88W(7JB)%Uj@8FYnt1!F8i6l!1j!?X>be(#5r>bLlq+A z2%%DrlG4)+>;`3hKw63?!Qb?=!Q_nBJ6pgFgMTvp^9CL_TG5*?I$Yp?UoKj&D;(In zxf-l$2wiRU>NDA<#JxdDWLH#EIQO)`t6zj0)(r*GyDSAW+xg+_9){z4Y6$P2FTe-n zpz`P>sx=ZdQKoGsQ7mH=(`y|5*6?BC;A7VJbk7--5WQSR=icdLR?=HNeELKd-WZSH z(NW#@P>=#QFxxnOhfCDUFb4~?7)SaH?$1)!D7uY^bGd}75GYU?H%rFtNTn<#%M~KZ z=db$QLYPXROxNy@uI-2EUwb2jVrle0VU`6XxWcUN^}pKEKVS0Wwg0?-qo9RM584CY z5aHhhWnv*Yh4-*>1u6s6lV!ZZ2e8`1MQKJ8M~xr!R_?oqRYi+q$PAe3`fE$eof#SP ie);04vjgjkOvtEyC|Lkg3vy-vfSHk%VX1*@ -1*PropagationNodeDetector.EMITTED_DELTA_IGNORE: + # age = 0 + pass + + RNS.log("Detected active propagation node "+RNS.prettyhexrep(destination_hash)+" emission "+str(age)+" seconds ago, "+str(hops)+" hops away") + + if self.owner.config["lxmf_propagation_node"] == None: + if self.owner.active_propagation_node == None: + self.owner.set_active_propagation_node(destination_hash) + else: + prev_hops = RNS.Transport.hops_to(self.owner.active_propagation_node) + if hops <= prev_hops: + self.owner.set_active_propagation_node(destination_hash) + else: + pass + else: + pass + + except Exception as e: + RNS.log("Error while processing received propagation node announce: "+str(e)) + + def __init__(self, owner): + self.owner = owner + self.owner_app = owner.owner_app + +class SidebandCore(): + CONV_P2P = 0x01 + CONV_GROUP = 0x02 + CONV_BROADCAST = 0x03 + + MAX_ANNOUNCES = 64 + + aspect_filter = "lxmf.delivery" + def received_announce(self, destination_hash, announced_identity, app_data): + # Add the announce to the directory announce + # stream logger + self.log_announce(destination_hash, app_data) + + def __init__(self, owner_app): + self.owner_app = owner_app + self.reticulum = None + + self.app_dir = plyer.storagepath.get_application_dir() + + self.rns_configdir = None + if RNS.vendor.platformutils.get_platform() == "android": + self.app_dir = self.app_dir+"/io.unsigned.sideband/files/" + self.rns_configdir = self.app_dir+"/app_storage/reticulum" + + if not os.path.isdir(self.app_dir+"/app_storage"): + os.makedirs(self.app_dir+"/app_storage") + + self.asset_dir = self.app_dir+"/assets" + self.kv_dir = self.app_dir+"/views/kv" + self.config_path = self.app_dir+"/app_storage/sideband_config" + self.identity_path = self.app_dir+"/app_storage/primary_identity" + self.db_path = self.app_dir+"/app_storage/sideband.db" + self.lxmf_storage = self.app_dir+"/app_storage/" + + try: + if not os.path.isfile(self.config_path): + self.__init_config() + else: + self.__load_config() + + except Exception as e: + RNS.log("Error while configuring Sideband: "+str(e), RNS.LOG_ERROR) + + + # Initialise Reticulum configuration + if RNS.vendor.platformutils.get_platform() == "android": + try: + self.rns_configdir = self.app_dir+"/app_storage/reticulum" + if not os.path.isdir(self.rns_configdir): + os.makedirs(self.rns_configdir) + + RNS.log("Configuring Reticulum instance...") + config_file = open(self.rns_configdir+"/config", "wb") + config_file.write(rns_config) + config_file.close() + + except Exception as e: + RNS.log("Error while configuring Reticulum instance: "+str(e), RNS.LOG_ERROR) + + else: + pass + + self.active_propagation_node = None + self.propagation_detector = PropagationNodeDetector(self) + + RNS.Transport.register_announce_handler(self) + RNS.Transport.register_announce_handler(self.propagation_detector) + + self.start() + + + def __init_config(self): + RNS.log("Creating new Sideband configuration...") + if os.path.isfile(self.identity_path): + self.identity = RNS.Identity.from_file(self.identity_path) + else: + self.identity = RNS.Identity() + self.identity.to_file(self.identity_path) + + self.config = {} + self.config["display_name"] = "Anonymous Peer" + self.config["start_announce"] = False + self.config["propagation_by_default"] = False + self.config["home_node_as_broadcast_repeater"] = False + self.config["send_telemetry_to_home_node"] = False + self.config["lxmf_propagation_node"] = None + self.config["lxmf_sync_limit"] = None + self.config["lxmf_sync_max"] = 3 + self.config["last_lxmf_propagation_node"] = None + self.config["nn_home_node"] = None + self.__save_config() + + if not os.path.isfile(self.db_path): + self.__db_init() + + + def __load_config(self): + RNS.log("Loading Sideband identity...") + self.identity = RNS.Identity.from_file(self.identity_path) + + RNS.log("Loading Sideband configuration...") + config_file = open(self.config_path, "rb") + self.config = msgpack.unpackb(config_file.read()) + config_file.close() + + if not os.path.isfile(self.db_path): + self.__db_init() + + + def __save_config(self): + RNS.log("Saving Sideband configuration...") + config_file = open(self.config_path, "wb") + config_file.write(msgpack.packb(self.config)) + config_file.close() + + + def save_configuration(self): + RNS.log("Saving configuration") + self.__save_config() + + def set_active_propagation_node(self, dest): + if dest == None: + RNS.log("No active propagation node configured") + else: + try: + self.active_propagation_node = dest + self.config["last_lxmf_propagation_node"] = dest + self.message_router.set_outbound_propagation_node(dest) + RNS.log("Active propagation node set to: "+RNS.prettyhexrep(dest)) + self.__save_config() + except Exception as e: + RNS.log("Error while setting LXMF propagation node: "+str(e), RNS.LOG_ERROR) + + + def log_announce(self, dest, app_data): + try: + RNS.log("Received LXMF destination announce for "+RNS.prettyhexrep(dest)+" with data: "+app_data.decode("utf-8")) + self._db_save_announce(dest, app_data) + self.owner_app.flag_new_announces = True + + except Exception as e: + RNS.log("Exception while decoding LXMF destination announce data:"+str(e)) + + def list_conversations(self): + result = self._db_conversations() + if result != None: + return result + else: + return [] + + def list_announces(self): + result = self._db_announces() + if result != None: + return result + else: + return [] + + def has_conversation(self, context_dest): + existing_conv = self._db_conversation(context_dest) + if existing_conv != None: + return True + else: + return False + + def is_trusted(self, context_dest): + try: + existing_conv = self._db_conversation(context_dest) + if existing_conv != None: + if existing_conv["trust"] == 1: + return True + else: + return False + else: + return False + + except Exception as e: + RNS.log("Error while checking trust for "+RNS.prettyhexrep(context_dest)+": "+str(e), RNS.LOG_ERROR) + return False + + def raw_display_name(self, context_dest): + try: + existing_conv = self._db_conversation(context_dest) + if existing_conv != None: + if existing_conv["name"] != None and existing_conv["name"] != "": + return existing_conv["name"] + else: + return "" + else: + return "" + + except Exception as e: + RNS.log("Error while getting peer name: "+str(e), RNS.LOG_ERROR) + return "" + + def peer_display_name(self, context_dest): + try: + existing_conv = self._db_conversation(context_dest) + if existing_conv != None: + if existing_conv["name"] != None and existing_conv["name"] != "": + if existing_conv["trust"] == 1: + return existing_conv["name"] + else: + return existing_conv["name"]+" "+RNS.prettyhexrep(context_dest) + + else: + app_data = RNS.Identity.recall_app_data(context_dest) + if app_data != None: + if existing_conv["trust"] == 1: + return app_data.decode("utf-8") + else: + return app_data.decode("utf-8")+" "+RNS.prettyhexrep(context_dest) + else: + return RNS.prettyhexrep(context_dest) + else: + app_data = RNS.Identity.recall_app_data(context_dest) + if app_data != None: + return app_data.decode("utf-8")+" "+RNS.prettyhexrep(context_dest) + else: + return RNS.prettyhexrep(context_dest) + + + except Exception as e: + RNS.log("Error while getting peer name: "+str(e), RNS.LOG_ERROR) + return RNS.prettyhexrep(context_dest) + + def clear_conversation(self, context_dest): + self._db_clear_conversation(context_dest) + + def delete_conversation(self, context_dest): + self._db_clear_conversation(context_dest) + self._db_delete_conversation(context_dest) + + def delete_message(self, message_hash): + self._db_delete_message(message_hash) + + def read_conversation(self, context_dest): + self._db_conversation_set_unread(context_dest, False) + + def unread_conversation(self, context_dest): + self._db_conversation_set_unread(context_dest, True) + + def trusted_conversation(self, context_dest): + self._db_conversation_set_trusted(context_dest, True) + + def untrusted_conversation(self, context_dest): + self._db_conversation_set_trusted(context_dest, False) + + def named_conversation(self, name, context_dest): + self._db_conversation_set_name(context_dest, name) + + def list_messages(self, context_dest, after = None): + result = self._db_messages(context_dest, after) + if result != None: + return result + else: + return [] + + def __event_conversations_changed(self): + pass + + def __event_conversation_changed(self, context_dest): + pass + + def __db_init(self): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + dbc.execute("DROP TABLE IF EXISTS lxm") + dbc.execute("CREATE TABLE lxm (lxm_hash BLOB PRIMARY KEY, dest BLOB, source BLOB, title BLOB, tx_ts INTEGER, rx_ts INTEGER, state INTEGER, method INTEGER, t_encrypted INTEGER, t_encryption INTEGER, data BLOB)") + + dbc.execute("DROP TABLE IF EXISTS conv") + dbc.execute("CREATE TABLE conv (dest_context BLOB PRIMARY KEY, last_tx INTEGER, last_rx INTEGER, unread INTEGER, type INTEGER, trust INTEGER, name BLOB, data BLOB)") + + dbc.execute("DROP TABLE IF EXISTS announce") + dbc.execute("CREATE TABLE announce (id PRIMARY KEY, received INTEGER, source BLOB, data BLOB)") + + db.commit() + db.close() + + def _db_conversation_set_unread(self, context_dest, unread): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "UPDATE conv set unread = ? where dest_context = ?" + data = (unread, context_dest) + dbc.execute(query, data) + result = dbc.fetchall() + db.commit() + + db.close() + + def _db_conversation_set_trusted(self, context_dest, trusted): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "UPDATE conv set trust = ? where dest_context = ?" + data = (trusted, context_dest) + dbc.execute(query, data) + result = dbc.fetchall() + db.commit() + + db.close() + + def _db_conversation_set_name(self, context_dest, name): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "UPDATE conv set name=:name_data where dest_context=:ctx;" + dbc.execute(query, {"ctx": context_dest, "name_data": name.encode("utf-8")}) + result = dbc.fetchall() + db.commit() + + db.close() + + def _db_conversations(self): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + dbc.execute("select * from conv") + result = dbc.fetchall() + + db.close() + + if len(result) < 1: + return None + else: + convs = [] + for entry in result: + conv = { + "dest": entry[0], + "unread": entry[3], + } + convs.append(conv) + + return convs + + + def _db_announces(self): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + dbc.execute("select * from announce order by received desc") + result = dbc.fetchall() + + db.close() + + if len(result) < 1: + return None + else: + announces = [] + for entry in result: + try: + announce = { + "dest": entry[2], + "data": entry[3].decode("utf-8"), + "time": entry[1], + } + announces.append(announce) + except Exception as e: + RNS.log("Exception while fetching announce from DB: "+str(e), RNS.LOG_ERROR) + + return announces + + + def _db_conversation(self, context_dest): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "select * from conv where dest_context=:ctx" + dbc.execute(query, {"ctx": context_dest}) + result = dbc.fetchall() + + db.close() + + if len(result) < 1: + return None + else: + c = result[0] + conv = {} + conv["dest"] = c[0] + conv["last_tx"] = c[1] + conv["last_rx"] = c[2] + conv["unread"] = c[3] + conv["type"] = c[4] + conv["trust"] = c[5] + conv["name"] = c[6].decode("utf-8") + conv["data"] = msgpack.unpackb(c[7]) + return conv + + + def _db_clear_conversation(self, context_dest): + RNS.log("Clearing conversation with "+RNS.prettyhexrep(context_dest)) + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "delete from lxm where (dest=:ctx_dst or source=:ctx_dst);" + dbc.execute(query, {"ctx_dst": context_dest}) + db.commit() + + db.close() + + def _db_delete_conversation(self, context_dest): + RNS.log("Deleting conversation with "+RNS.prettyhexrep(context_dest)) + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "delete from conv where (dest_context=:ctx_dst);" + dbc.execute(query, {"ctx_dst": context_dest}) + db.commit() + + db.close() + + def _db_create_conversation(self, context_dest, name = None, trust = False): + RNS.log("Creating conversation for "+RNS.prettyhexrep(context_dest)) + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + def_name = "".encode("utf-8") + query = "INSERT INTO conv (dest_context, last_tx, last_rx, unread, type, trust, name, data) values (?, ?, ?, ?, ?, ?, ?, ?)" + data = (context_dest, 0, 0, 0, SidebandCore.CONV_P2P, 0, def_name, msgpack.packb(None)) + + dbc.execute(query, data) + + db.commit() + db.close() + + if trust: + self._db_conversation_set_trusted(context_dest, True) + + if name != None and name != "": + self._db_conversation_set_name(context_dest, name) + + self.__event_conversations_changed() + + def _db_delete_message(self, msg_hash): + RNS.log("Deleting message "+RNS.prettyhexrep(msg_hash)) + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "delete from lxm where (lxm_hash=:mhash);" + dbc.execute(query, {"mhash": msg_hash}) + db.commit() + + db.close() + + def _db_clean_messages(self): + RNS.log("Purging stale messages... "+str(self.db_path)) + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "delete from lxm where (state=:outbound_state or state=:sending_state);" + dbc.execute(query, {"outbound_state": LXMF.LXMessage.OUTBOUND, "sending_state": LXMF.LXMessage.SENDING}) + db.commit() + + db.close() + + def _db_message_set_state(self, lxm_hash, state): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "UPDATE lxm set state = ? where lxm_hash = ?" + data = (state, lxm_hash) + dbc.execute(query, data) + db.commit() + result = dbc.fetchall() + + db.close() + + def _db_message_set_method(self, lxm_hash, method): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "UPDATE lxm set method = ? where lxm_hash = ?" + data = (method, lxm_hash) + dbc.execute(query, data) + db.commit() + result = dbc.fetchall() + + db.close() + + def message(self, msg_hash): + return self._db_message(msg_hash) + + def _db_message(self, msg_hash): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "select * from lxm where lxm_hash=:mhash" + dbc.execute(query, {"mhash": msg_hash}) + result = dbc.fetchall() + + db.close() + + if len(result) < 1: + return None + else: + entry = result[0] + lxm = LXMF.LXMessage.unpack_from_bytes(entry[10]) + message = { + "hash": lxm.hash, + "dest": lxm.destination_hash, + "source": lxm.source_hash, + "title": lxm.title, + "content": lxm.content, + "received": entry[5], + "sent": lxm.timestamp, + "state": entry[6], + "method": entry[7], + "lxm": lxm + } + return message + + def _db_messages(self, context_dest, after = None): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + if after == None: + query = "select * from lxm where dest=:context_dest or source=:context_dest" + dbc.execute(query, {"context_dest": context_dest}) + else: + query = "select * from lxm where (dest=:context_dest or source=:context_dest) and rx_ts>:after_ts" + dbc.execute(query, {"context_dest": context_dest, "after_ts": after}) + + result = dbc.fetchall() + + db.close() + + if len(result) < 1: + return None + else: + messages = [] + for entry in result: + lxm = LXMF.LXMessage.unpack_from_bytes(entry[10]) + message = { + "hash": lxm.hash, + "dest": lxm.destination_hash, + "source": lxm.source_hash, + "title": lxm.title, + "content": lxm.content, + "received": entry[5], + "sent": lxm.timestamp, + "state": entry[6], + "method": entry[7], + "lxm": lxm + } + messages.append(message) + + return messages + + + def _db_save_lxm(self, lxm, context_dest): + state = lxm.state + + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + if not lxm.packed: + lxm.pack() + + query = "INSERT INTO lxm (lxm_hash, dest, source, title, tx_ts, rx_ts, state, method, t_encrypted, t_encryption, data) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + data = ( + lxm.hash, + lxm.destination_hash, + lxm.source_hash, + lxm.title, + lxm.timestamp, + time.time(), + state, + lxm.method, + lxm.transport_encrypted, + lxm.transport_encryption, + lxm.packed + ) + + dbc.execute(query, data) + + db.commit() + db.close() + + self.__event_conversation_changed(context_dest) + + def _db_save_announce(self, destination_hash, app_data): + db = sqlite3.connect(self.db_path) + dbc = db.cursor() + + query = "INSERT INTO announce (received, source, data) values (?, ?, ?)" + data = ( + time.time(), + destination_hash, + app_data, + ) + + dbc.execute(query, data) + + query = "delete from announce where id not in (select id from announce order by received desc limit "+str(self.MAX_ANNOUNCES)+")" + dbc.execute(query) + + db.commit() + db.close() + + def lxmf_announce(self): + self.lxmf_destination.announce() + + def is_known(self, dest_hash): + try: + source_identity = RNS.Identity.recall(dest_hash) + + if source_identity: + return True + else: + return False + + except Exception as e: + return False + + def request_key(self, dest_hash): + try: + RNS.Transport.request_path(dest_hash) + return True + + except Exception as e: + RNS.log("Error while querying for key: "+str(e), RNS.LOG_ERROR) + return False + + def __start_jobs_deferred(self): + if self.config["start_announce"]: + self.lxmf_destination.announce() + + def __start_jobs_immediate(self): + self.reticulum = RNS.Reticulum(configdir=self.rns_configdir) + RNS.log("Reticulum started, activating LXMF...") + + self.message_router = LXMF.LXMRouter(identity = self.identity, storagepath = self.lxmf_storage, autopeer = True) + self.message_router.register_delivery_callback(self.lxmf_delivery) + + self.lxmf_destination = self.message_router.register_delivery_identity(self.identity, display_name=self.config["display_name"]) + self.lxmf_destination.set_default_app_data(self.get_display_name_bytes) + + self.rns_dir = RNS.Reticulum.configdir + + def message_notification(self, message): + if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail: + RNS.log("Direct delivery of "+str(message)+" failed. Retrying as propagated message.", RNS.LOG_VERBOSE) + message.try_propagation_on_fail = None + message.delivery_attempts = 0 + del message.next_delivery_attempt + message.packed = None + message.desired_method = LXMF.LXMessage.PROPAGATED + self._db_message_set_method(message.hash, LXMF.LXMessage.PROPAGATED) + self.message_router.handle_outbound(message) + else: + self.lxm_ingest(message, originator=True) + + + def send_message(self, content, destination_hash, propagation): + try: + if content == "": + raise ValueError("Message content cannot be empty") + + dest_identity = RNS.Identity.recall(destination_hash) + dest = RNS.Destination(dest_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery") + source = self.lxmf_destination + + # TODO: Add setting + if propagation: + desired_method = LXMF.LXMessage.PROPAGATED + else: + desired_method = LXMF.LXMessage.DIRECT + + lxm = LXMF.LXMessage(dest, source, content, title="", desired_method=desired_method) + lxm.register_delivery_callback(self.message_notification) + lxm.register_failed_callback(self.message_notification) + + if self.message_router.get_outbound_propagation_node() != None: + lxm.try_propagation_on_fail = True + + self.message_router.handle_outbound(lxm) + self.lxm_ingest(lxm, originator=True) + + return True + + except Exception as e: + RNS.log("Error while sending message: "+str(e), RNS.LOG_ERROR) + return False + + def new_conversation(self, dest_str, name = "", trusted = False): + if len(dest_str) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2: + return False + + try: + addr_b = bytes.fromhex(dest_str) + self._db_create_conversation(addr_b, name, trusted) + + except Exception as e: + RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR) + return False + + return True + + def create_conversation(self, context_dest, name = None, trusted = False): + try: + self._db_create_conversation(context_dest, name, trusted) + + except Exception as e: + RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR) + return False + + return True + + def lxm_ingest(self, message, originator = False): + if originator: + context_dest = message.destination_hash + else: + context_dest = message.source_hash + + if self._db_message(message.hash): + RNS.log("Message exists, setting state to: "+str(message.state)) + self._db_message_set_state(message.hash, message.state) + else: + RNS.log("Message does not exist, saving") + self._db_save_lxm(message, context_dest) + + if self._db_conversation(context_dest) == None: + self._db_create_conversation(context_dest) + self.owner_app.flag_new_conversations = True + + if self.owner_app.root.ids.screen_manager.current == "messages_screen": + if self.owner_app.root.ids.messages_scrollview.active_conversation != context_dest: + self.unread_conversation(context_dest) + self.owner_app.flag_unread_conversations = True + else: + self.unread_conversation(context_dest) + self.owner_app.flag_unread_conversations = True + + try: + self.owner_app.conversation_update(context_dest) + except Exception as e: + RNS.log("Error in conversation update callback: "+str(e)) + + + def start(self): + self._db_clean_messages() + self.__start_jobs_immediate() + + if self.config["lxmf_propagation_node"] != None and self.config["lxmf_propagation_node"] != "": + self.set_active_propagation_node(self.config["lxmf_propagation_node"]) + else: + if self.config["last_lxmf_propagation_node"] != None and self.config["last_lxmf_propagation_node"] != "": + self.set_active_propagation_node(self.config["last_lxmf_propagation_node"]) + else: + self.set_active_propagation_node(None) + + thread = threading.Thread(target=self.__start_jobs_deferred) + thread.setDaemon(True) + thread.start() + RNS.log("Sideband Core "+str(self)+" started") + + def request_lxmf_sync(self, limit = None): + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit) + RNS.log("LXMF message sync requested from propagation node "+RNS.prettyhexrep(self.message_router.get_outbound_propagation_node())+" for "+str(self.identity)) + return True + else: + return False + + def cancel_lxmf_sync(self): + if self.message_router.propagation_transfer_state != LXMF.LXMRouter.PR_IDLE: + self.message_router.cancel_propagation_node_requests() + + def get_sync_progress(self): + return self.message_router.propagation_transfer_progress + + + def lxmf_delivery(self, message): + time_string = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)) + signature_string = "Signature is invalid, reason undetermined" + if message.signature_validated: + signature_string = "Validated" + else: + if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID: + signature_string = "Invalid signature" + if message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN: + signature_string = "Cannot verify, source is unknown" + + RNS.log("LXMF delivery "+str(time_string)+". "+str(signature_string)+".") + + try: + self.lxm_ingest(message) + except Exception as e: + RNS.log("Error while ingesting LXMF message "+RNS.prettyhexrep(message.hash)+" to database: "+str(e)) + + def get_display_name_bytes(self): + return self.config["display_name"].encode("utf-8") + + def get_sync_status(self): + if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE: + return "Idle" + elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_PATH_REQUESTED: + return "Path requested" + elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_ESTABLISHING: + return "Establishing link" + elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_ESTABLISHED: + return "Link established" + elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_REQUEST_SENT: + return "Sync request sent" + elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RECEIVING: + return "Receiving messages" + elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RESPONSE_RECEIVED: + return "Messages received" + elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE: + new_msgs = self.message_router.propagation_transfer_last_result + if new_msgs == 0: + return "Done, no new messages" + else: + return "Downloaded "+str(new_msgs)+" new messages" + else: + return "Unknown" + +rns_config = """ +[reticulum] +enable_transport = False +share_instance = Yes +shared_instance_port = 37428 +instance_control_port = 37429 +panic_on_interface_error = No + +[logging] +loglevel = 7 + +[interfaces] + [[Default Interface]] + type = AutoInterface + interface_enabled = True +""".encode("utf-8") diff --git a/ui/announces.py b/ui/announces.py new file mode 100644 index 0000000..ce7a362 --- /dev/null +++ b/ui/announces.py @@ -0,0 +1,151 @@ +import time +import RNS + +from kivy.metrics import dp +from kivy.uix.boxlayout import BoxLayout +from kivy.properties import StringProperty, BooleanProperty +from kivymd.uix.list import MDList, IconLeftWidget, IconRightWidget, OneLineAvatarIconListItem +from kivymd.uix.menu import MDDropdownMenu +from kivy.uix.gridlayout import GridLayout +from kivy.uix.boxlayout import BoxLayout + +from kivymd.uix.button import MDFlatButton +from kivymd.uix.dialog import MDDialog + +from ui.helpers import ts_format + +class Announces(): + def __init__(self, app): + self.app = app + self.context_dests = [] + self.added_item_dests = [] + self.list = None + self.update() + + def reload(self): + self.clear_list() + self.update() + + def clear_list(self): + if self.list != None: + self.list.clear_widgets() + + self.context_dests = [] + self.added_item_dests = [] + + def update(self): + self.clear_list() + self.announces = self.app.sideband.list_announces() + self.update_widget() + + self.app.flag_new_announces = False + + def update_widget(self): + if self.list == None: + self.list = MDList() + + for announce in self.announces: + context_dest = announce["dest"] + ts = announce["time"] + a_data = announce["data"] + + if not context_dest in self.added_item_dests: + if self.app.sideband.is_trusted(context_dest): + trust_icon = "account-check" + else: + trust_icon = "account-question" + + def gen_info(ts, dest, name): + def x(sender): + yes_button = MDFlatButton( + text="OK", + ) + + dialog = MDDialog( + text="Announce Received: "+ts+"\nAnnounced Name: "+name+"\nLXMF Address: "+RNS.prettyhexrep(dest), + buttons=[ yes_button ], + ) + def dl_yes(s): + dialog.dismiss() + + yes_button.bind(on_release=dl_yes) + item.dmenu.dismiss() + dialog.open() + return x + + time_string = time.strftime(ts_format, time.localtime(ts)) + disp_name = self.app.sideband.peer_display_name(context_dest) + iconl = IconLeftWidget(icon=trust_icon) + item = OneLineAvatarIconListItem(text=time_string+": "+disp_name, on_release=gen_info(time_string, context_dest, a_data)) + item.add_widget(iconl) + item.sb_uid = context_dest + + def gen_del(dest, item): + def x(): + yes_button = MDFlatButton( + text="Yes", + ) + no_button = MDFlatButton( + text="No", + ) + dialog = MDDialog( + text="Delete announce?", + buttons=[ yes_button, no_button ], + ) + def dl_yes(s): + dialog.dismiss() + self.app.sideband.delete_announce(dest) + self.reload() + def dl_no(s): + dialog.dismiss() + + yes_button.bind(on_release=dl_yes) + no_button.bind(on_release=dl_no) + item.dmenu.dismiss() + dialog.open() + return x + + def gen_conv(dest, item): + def x(): + item.dmenu.dismiss() + self.app.conversation_from_announce_action(dest) + return x + + dm_items = [ + { + "viewclass": "OneLineListItem", + "text": "Converse", + "height": dp(64), + "on_release": gen_conv(context_dest, item) + }, + # { + # "text": "Delete Announce", + # "viewclass": "OneLineListItem", + # "height": dp(64), + # "on_release": gen_del(context_dest, item) + # } + ] + + item.iconr = IconRightWidget(icon="dots-vertical"); + + item.dmenu = MDDropdownMenu( + caller=item.iconr, + items=dm_items, + position="center", + width_mult=4, + ) + + def callback_factory(ref): + def x(sender): + ref.dmenu.open() + return x + + item.iconr.bind(on_release=callback_factory(item)) + + item.add_widget(item.iconr) + + self.added_item_dests.append(context_dest) + self.list.add_widget(item) + + def get_widget(self): + return self.list \ No newline at end of file diff --git a/ui/conversations.py b/ui/conversations.py new file mode 100644 index 0000000..ae39451 --- /dev/null +++ b/ui/conversations.py @@ -0,0 +1,228 @@ +import RNS + +from kivy.metrics import dp +from kivy.uix.boxlayout import BoxLayout +from kivy.properties import StringProperty, BooleanProperty +from kivymd.uix.list import MDList, IconLeftWidget, IconRightWidget, OneLineAvatarIconListItem +from kivymd.uix.menu import MDDropdownMenu +from kivy.uix.gridlayout import GridLayout +from kivy.uix.boxlayout import BoxLayout + +from kivymd.uix.button import MDFlatButton +from kivymd.uix.dialog import MDDialog + + +class NewConv(BoxLayout): + pass + + +class MsgSync(BoxLayout): + pass + + +class ConvSettings(BoxLayout): + disp_name = StringProperty() + trusted = BooleanProperty() + + +class Conversations(): + def __init__(self, app): + self.app = app + self.context_dests = [] + self.added_item_dests = [] + self.list = None + self.update() + + def reload(self): + self.clear_list() + self.update() + + def clear_list(self): + if self.list != None: + self.list.clear_widgets() + + self.context_dests = [] + self.added_item_dests = [] + + def update(self): + if self.app.flag_unread_conversations: + self.clear_list() + + self.context_dests = self.app.sideband.list_conversations() + self.update_widget() + + self.app.flag_new_conversations = False + self.app.flag_unread_conversations = False + + def update_widget(self): + if self.list == None: + self.list = MDList() + + for conv in self.context_dests: + context_dest = conv["dest"] + unread = conv["unread"] + + if not context_dest in self.added_item_dests: + if self.app.sideband.is_trusted(context_dest): + if unread: + trust_icon = "email-seal" + else: + trust_icon = "account-check" + else: + if unread: + trust_icon = "email" + else: + trust_icon = "account-question" + + iconl = IconLeftWidget(icon=trust_icon) + item = OneLineAvatarIconListItem(text=self.app.sideband.peer_display_name(context_dest), on_release=self.app.conversation_action) + item.add_widget(iconl) + item.sb_uid = context_dest + + def gen_edit(dest, item): + def x(): + try: + disp_name = self.app.sideband.raw_display_name(dest) + is_trusted = self.app.sideband.is_trusted(dest) + + yes_button = MDFlatButton( + text="Save", + font_size=dp(20), + ) + no_button = MDFlatButton( + text="Cancel", + font_size=dp(20), + ) + dialog_content = ConvSettings(disp_name=disp_name, trusted=is_trusted) + dialog = MDDialog( + title="Conversation with "+RNS.prettyhexrep(dest), + type="custom", + content_cls=dialog_content, + buttons=[ yes_button, no_button ], + ) + dialog.d_content = dialog_content + def dl_yes(s): + try: + name = dialog.d_content.ids["name_field"].text + trusted = dialog.d_content.ids["trusted_switch"].active + if trusted: + RNS.log("Setting Trusted "+str(trusted)) + self.app.sideband.trusted_conversation(dest) + else: + RNS.log("Setting Untrusted "+str(trusted)) + self.app.sideband.untrusted_conversation(dest) + + RNS.log("Name="+name) + self.app.sideband.named_conversation(name, dest) + + except Exception as e: + RNS.log("Error while saving conversation settings: "+str(e), RNS.LOG_ERROR) + + dialog.dismiss() + self.reload() + + def dl_no(s): + dialog.dismiss() + + yes_button.bind(on_release=dl_yes) + no_button.bind(on_release=dl_no) + item.dmenu.dismiss() + dialog.open() + except Exception as e: + RNS.log("Error while creating conversation settings: "+str(e), RNS.LOG_ERROR) + + return x + + def gen_clear(dest, item): + def x(): + yes_button = MDFlatButton( + text="Yes", + ) + no_button = MDFlatButton( + text="No", + ) + dialog = MDDialog( + text="Clear all messages in conversation?", + buttons=[ yes_button, no_button ], + ) + def dl_yes(s): + dialog.dismiss() + self.app.sideband.clear_conversation(dest) + def dl_no(s): + dialog.dismiss() + + yes_button.bind(on_release=dl_yes) + no_button.bind(on_release=dl_no) + item.dmenu.dismiss() + dialog.open() + return x + + def gen_del(dest, item): + def x(): + yes_button = MDFlatButton( + text="Yes", + ) + no_button = MDFlatButton( + text="No", + ) + dialog = MDDialog( + text="Delete conversation?", + buttons=[ yes_button, no_button ], + ) + def dl_yes(s): + dialog.dismiss() + self.app.sideband.delete_conversation(dest) + self.reload() + def dl_no(s): + dialog.dismiss() + + yes_button.bind(on_release=dl_yes) + no_button.bind(on_release=dl_no) + item.dmenu.dismiss() + dialog.open() + return x + + dm_items = [ + { + "viewclass": "OneLineListItem", + "text": "Edit", + "height": dp(64), + "on_release": gen_edit(context_dest, item) + }, + { + "text": "Clear Messages", + "viewclass": "OneLineListItem", + "height": dp(64), + "on_release": gen_clear(context_dest, item) + }, + { + "text": "Delete Conversation", + "viewclass": "OneLineListItem", + "height": dp(64), + "on_release": gen_del(context_dest, item) + } + ] + + item.iconr = IconRightWidget(icon="dots-vertical"); + + item.dmenu = MDDropdownMenu( + caller=item.iconr, + items=dm_items, + position="center", + width_mult=4, + ) + + def callback_factory(ref): + def x(sender): + ref.dmenu.open() + return x + + item.iconr.bind(on_release=callback_factory(item)) + + item.add_widget(item.iconr) + + self.added_item_dests.append(context_dest) + self.list.add_widget(item) + + def get_widget(self): + return self.list \ No newline at end of file diff --git a/ui/helpers.py b/ui/helpers.py new file mode 100644 index 0000000..8422d7a --- /dev/null +++ b/ui/helpers.py @@ -0,0 +1,30 @@ +from kivy.utils import get_color_from_hex +from kivymd.color_definitions import colors +from kivy.uix.screenmanager import ScreenManager, Screen +from kivymd.theming import ThemableBehavior +from kivymd.uix.list import OneLineIconListItem, MDList, IconLeftWidget, IconRightWidget +from kivy.properties import StringProperty + +ts_format = "%Y-%m-%d %H:%M:%S" + +def mdc(color, hue=None): + if hue == None: + hue = "400" + return get_color_from_hex(colors[color][hue]) + +color_received = "Green" +color_delivered = "Indigo" +color_propagated = "Blue" +color_failed = "Red" +color_unknown = "Gray" +intensity_msgs = "600" + +class ContentNavigationDrawer(Screen): + pass + +class DrawerList(ThemableBehavior, MDList): + pass + +class IconListItem(OneLineIconListItem): + icon = StringProperty() + diff --git a/ui/layouts.py b/ui/layouts.py new file mode 100644 index 0000000..5be6b81 --- /dev/null +++ b/ui/layouts.py @@ -0,0 +1,675 @@ +root_layout = """ +MDNavigationLayout: + + ScreenManager: + id: screen_manager + + MDScreen: + name: "conversations_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Conversations" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + right_action_items: + [ + ['access-point', lambda x: root.ids.screen_manager.app.announce_now_action(self)], + ['email-sync', lambda x: root.ids.screen_manager.app.lxmf_sync_action(self)], + ['account-plus', lambda x: root.ids.screen_manager.app.new_conversation_action(self)], + ] + + ScrollView: + id: conversations_scrollview + + + MDScreen: + name: "messages_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + id: messages_toolbar + title: "Messages" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + right_action_items: + [ + ['lan-connect', lambda x: root.ids.screen_manager.app.message_propagation_action(self)], + ['close', lambda x: root.ids.screen_manager.app.close_messages_action(self)], + ] + + ScrollView: + id: messages_scrollview + do_scroll_x: False + do_scroll_y: True + + BoxLayout: + orientation: "vertical" + id: no_keys_part + padding: [dp(28), dp(16), dp(16), dp(16)] + spacing: dp(24) + size_hint_y: None + height: self.minimum_height + dp(64) + + MDLabel: + id: nokeys_text + text: "" + + MDRectangleFlatIconButton: + icon: "key-wireless" + text: "Query Network For Keys" + # padding: [dp(16), dp(16), dp(16), dp(16)] + on_release: root.ids.screen_manager.app.key_query_action(self) + + + BoxLayout: + id: message_input_part + # orientation: "vertical" + padding: [dp(28), dp(16), dp(16), dp(16)] + spacing: dp(24) + size_hint_y: None + height: self.minimum_height + + MDTextField: + id: message_text + multiline: True + hint_text: "Write message" + mode: "rectangle" + max_height: dp(100) + + MDRectangleFlatIconButton: + icon: "transfer-up" + text: "Send" + # padding: [dp(16), dp(16), dp(16), dp(16)] + on_release: root.ids.screen_manager.app.message_send_action(self) + + + MDScreen: + name: "broadcasts_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Local Broadcasts" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + + ScrollView: + id: broadcasts_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(64) + + MDLabel: + id: broadcasts_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDScreen: + name: "connectivity_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Connectivity" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + + ScrollView: + id:connectivity_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(64) + + MDLabel: + id: connectivity_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDScreen: + name: "guide_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Guide" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + + ScrollView: + id:guide_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(64) + + MDLabel: + id: guide_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDScreen: + name: "information_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "App & Version Information" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + + ScrollView: + id:information_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(64) + + MDLabel: + id: information_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDScreen: + name: "map_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Local Area Map" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + + ScrollView: + id:information_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(64) + + MDLabel: + id: map_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDScreen: + name: "keys_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Encryption Keys" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + + ScrollView: + id:keys_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(64) + + MDLabel: + id: keys_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDScreen: + name: "announces_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Announce Stream" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + right_action_items: + [ + ['close', lambda x: root.ids.screen_manager.app.close_settings_action(self)], + ] + # [['eye-off', lambda x: root.ids.screen_manager.app.announce_filter_action(self)]] + + ScrollView: + id: announces_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(64) + + MDLabel: + id: announces_info + markup: True + text: "" + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + MDScreen: + name: "settings_screen" + + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Settings" + elevation: 10 + pos_hint: {"top": 1} + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + right_action_items: + [ + ['close', lambda x: root.ids.screen_manager.app.close_settings_action(self)], + ] + + ScrollView: + id: settings_scrollview + + MDBoxLayout: + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height + padding: dp(16) + + MDLabel: + text: "" + font_style: "H6" + + MDLabel: + text: "User Options" + font_style: "H6" + + MDTextField: + id: settings_display_name + hint_text: "Display Name" + text: "" + max_text_length: 128 + font_size: dp(24) + + MDTextField: + id: settings_lxmf_address + hint_text: "Your LXMF Address" + text: "" + disabled: False + max_text_length: 20 + font_size: dp(24) + + MDTextField: + id: settings_propagation_node_address + hint_text: "LXMF Propagation Node" + disabled: False + text: "" + max_text_length: 20 + font_size: dp(24) + + MDTextField: + id: settings_home_node_address + hint_text: "Nomad Network Home Node" + disabled: False + text: "" + max_text_length: 20 + font_size: dp(24) + + MDBoxLayout: + orientation: "horizontal" + # spacing: "24dp" + size_hint_y: None + height: dp(48) + + MDLabel: + text: "Announce At App Startup" + font_style: "H6" + + MDSwitch: + id: settings_start_announce + active: False + + MDBoxLayout: + orientation: "horizontal" + # spacing: "24dp" + size_hint_y: None + height: dp(48) + + MDLabel: + text: "Deliver via LXMF Propagation Node by default" + font_style: "H6" + + MDSwitch: + id: settings_lxmf_delivery_by_default + disabled: False + active: False + + MDBoxLayout: + orientation: "horizontal" + # spacing: "24dp" + size_hint_y: None + height: dp(48) + + MDLabel: + text: "Limit each sync to 3 messages" + font_style: "H6" + + MDSwitch: + id: settings_lxmf_sync_limit + disabled: False + active: False + + MDBoxLayout: + orientation: "horizontal" + # spacing: "24dp" + size_hint_y: None + height: dp(48) + + MDLabel: + text: "Use Home Node as Broadcast Repeater" + font_style: "H6" + + MDSwitch: + id: settings_home_node_as_broadcast_repeater + active: False + disabled: True + + MDBoxLayout: + orientation: "horizontal" + # spacing: "24dp" + size_hint_y: None + height: dp(48) + + MDLabel: + text: "Send Telemetry to Home Node" + font_style: "H6" + + MDSwitch: + id: settings_telemetry_to_home_node + disabled: True + active: False + + + MDNavigationDrawer: + id: nav_drawer + + ContentNavigationDrawer: + ScrollView: + DrawerList: + id: md_list + + MDList: + OneLineIconListItem: + text: "Conversations" + on_release: root.ids.screen_manager.app.conversations_action(self) + + IconLeftWidget: + icon: "email" + on_release: root.ids.screen_manager.app.conversations_action(self) + + + OneLineIconListItem: + text: "Announce Stream" + on_release: root.ids.screen_manager.app.announces_action(self) + + IconLeftWidget: + icon: "account-voice" + on_release: root.ids.screen_manager.app.announces_action(self) + + + OneLineIconListItem: + text: "Local Broadcasts" + on_release: root.ids.screen_manager.app.broadcasts_action(self) + + IconLeftWidget: + icon: "radio-tower" + on_release: root.ids.screen_manager.app.broadcasts_action(self) + + + OneLineIconListItem: + text: "Local Area Map" + on_release: root.ids.screen_manager.app.map_action(self) + + IconLeftWidget: + icon: "map" + on_release: root.ids.screen_manager.app.map_action(self) + + + OneLineIconListItem: + text: "Connectivity" + on_release: root.ids.screen_manager.app.connectivity_action(self) + _no_ripple_effect: True + + IconLeftWidget: + icon: "wifi" + on_release: root.ids.screen_manager.app.connectivity_action(self) + + + OneLineIconListItem: + text: "Settings" + on_release: root.ids.screen_manager.app.settings_action(self) + _no_ripple_effect: True + + IconLeftWidget: + icon: "cog" + on_release: root.ids.screen_manager.app.settings_action(self) + + + OneLineIconListItem: + text: "Encryption Keys" + on_release: root.ids.screen_manager.app.keys_action(self) + _no_ripple_effect: True + + IconLeftWidget: + icon: "key-chain" + on_release: root.ids.screen_manager.app.keys_action(self) + + + OneLineIconListItem: + text: "Guide" + on_release: root.ids.screen_manager.app.guide_action(self) + _no_ripple_effect: True + + IconLeftWidget: + icon: "book-open" + on_release: root.ids.screen_manager.app.guide_action(self) + + + OneLineIconListItem: + id: app_version_info + text: "" + on_release: root.ids.screen_manager.app.information_action(self) + _no_ripple_effect: True + + IconLeftWidget: + icon: "information" + on_release: root.ids.screen_manager.app.information_action(self) + + + OneLineIconListItem: + text: "Shutdown" + on_release: root.ids.screen_manager.app.quit_action(self) + _no_ripple_effect: True + + IconLeftWidget: + icon: "power" + on_release: root.ids.screen_manager.app.quit_action(self) + +: + padding: dp(8) + size_hint: 1.0, None + height: content_text.height + heading_text.height + dp(32) + pos_hint: {"center_x": .5, "center_y": .5} + + MDRelativeLayout: + size_hint: 1.0, None + # size: root.size + + MDIconButton: + id: msg_submenu + icon: "dots-vertical" + pos: + root.width - (self.width + root.padding[0] + dp(4)), root.height - (self.height + root.padding[0] + dp(4)) + + MDLabel: + id: heading_text + markup: True + text: root.heading + adaptive_size: True + pos: 0, root.height - (self.height + root.padding[0] + dp(8)) + + MDLabel: + id: content_text + text: root.text + # adaptive_size: True + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + + + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: self.minimum_height+dp(24) + + MDLabel: + id: sync_status + hint_text: "Name" + text: "Initiating sync..." + + MDProgressBar: + id: sync_progress + value: 0 + + + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: dp(148) + + MDTextField: + id: name_field + hint_text: "Name" + text: root.disp_name + font_size: dp(24) + + MDBoxLayout: + orientation: "horizontal" + # spacing: "24dp" + size_hint_y: None + height: dp(48) + MDLabel: + id: trusted_switch_label + text: "Trusted" + font_style: "H6" + + MDSwitch: + id: trusted_switch + active: root.trusted + + + orientation: "vertical" + spacing: "24dp" + size_hint_y: None + height: dp(250) + + MDTextField: + id: n_address_field + max_text_length: 20 + hint_text: "Address" + helper_text: "Error, check your input" + helper_text_mode: "on_error" + text: "" + font_size: dp(24) + + MDTextField: + id: n_name_field + hint_text: "Name" + text: "" + font_size: dp(24) + + MDBoxLayout: + orientation: "horizontal" + size_hint_y: None + height: dp(48) + MDLabel: + id: "trusted_switch_label" + text: "Trusted" + font_style: "H6" + + MDSwitch: + id: n_trusted + active: False +""" \ No newline at end of file diff --git a/ui/messages.py b/ui/messages.py new file mode 100644 index 0000000..bc47e2e --- /dev/null +++ b/ui/messages.py @@ -0,0 +1,207 @@ +import time +import RNS +import LXMF + +from kivy.metrics import dp +from kivy.core.clipboard import Clipboard +from kivymd.uix.card import MDCard +from kivymd.uix.menu import MDDropdownMenu +from kivymd.uix.behaviors import RoundedRectangularElevationBehavior +from kivy.properties import StringProperty, BooleanProperty +from kivy.uix.gridlayout import GridLayout +from kivy.uix.boxlayout import BoxLayout + +from kivymd.uix.button import MDFlatButton +from kivymd.uix.dialog import MDDialog + +from ui.helpers import ts_format, mdc +from ui.helpers import color_received, color_delivered, color_propagated, color_failed, color_unknown, intensity_msgs + +class ListLXMessageCard(MDCard, RoundedRectangularElevationBehavior): + text = StringProperty() + heading = StringProperty() + +class Messages(): + def __init__(self, app, context_dest): + self.app = app + self.context_dest = context_dest + self.messages = [] + self.added_item_hashes = [] + self.latest_message_timestamp = None + self.list = None + self.widgets = [] + self.send_error_dialog = None + self.update() + + def reload(self): + if self.list != None: + self.list.clear_widgets() + + self.messages = [] + self.added_item_hashes = [] + self.latest_message_timestamp = None + self.widgets = [] + + self.update() + + def update(self): + self.messages = self.app.sideband.list_messages(self.context_dest, self.latest_message_timestamp) + if self.list == None: + layout = GridLayout(cols=1, spacing=16, padding=16, size_hint_y=None) + layout.bind(minimum_height=layout.setter('height')) + self.list = layout + + if len(self.messages) > 0: + self.update_widget() + + for w in self.widgets: + m = w.m + if m["state"] == LXMF.LXMessage.SENDING or m["state"] == LXMF.LXMessage.OUTBOUND: + msg = self.app.sideband.message(m["hash"]) + if msg["state"] == LXMF.LXMessage.DELIVERED: + w.md_bg_color = msg_color = mdc(color_delivered, intensity_msgs) + txstr = time.strftime(ts_format, time.localtime(msg["sent"])) + titlestr = "" + if msg["title"]: + titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" + w.heading = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] Delivered" + m["state"] = msg["state"] + + if msg["method"] == LXMF.LXMessage.PROPAGATED and msg["state"] == LXMF.LXMessage.SENT: + w.md_bg_color = msg_color = mdc(color_propagated, intensity_msgs) + txstr = time.strftime(ts_format, time.localtime(msg["sent"])) + titlestr = "" + if msg["title"]: + titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" + w.heading = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] On Propagation Net" + m["state"] = msg["state"] + + if msg["state"] == LXMF.LXMessage.FAILED: + w.md_bg_color = msg_color = mdc(color_failed, intensity_msgs) + txstr = time.strftime(ts_format, time.localtime(msg["sent"])) + titlestr = "" + if msg["title"]: + titlestr = "[b]Title[/b] "+msg["title"].decode("utf-8")+"\n" + w.heading = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] Failed" + m["state"] = msg["state"] + + + def update_widget(self): + for m in self.messages: + if not m["hash"] in self.added_item_hashes: + txstr = time.strftime(ts_format, time.localtime(m["sent"])) + rxstr = time.strftime(ts_format, time.localtime(m["received"])) + titlestr = "" + + if m["title"]: + titlestr = "[b]Title[/b] "+m["title"].decode("utf-8")+"\n" + + if m["source"] == self.app.sideband.lxmf_destination.hash: + if m["state"] == LXMF.LXMessage.DELIVERED: + msg_color = mdc(color_delivered, intensity_msgs) + heading_str = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] Delivered" + + elif m["method"] == LXMF.LXMessage.PROPAGATED and m["state"] == LXMF.LXMessage.SENT: + msg_color = mdc(color_propagated, intensity_msgs) + heading_str = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] On Propagation Net" + + elif m["state"] == LXMF.LXMessage.FAILED: + msg_color = mdc(color_failed, intensity_msgs) + heading_str = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] Failed" + + elif m["state"] == LXMF.LXMessage.OUTBOUND or m["state"] == LXMF.LXMessage.SENDING: + msg_color = mdc(color_unknown, intensity_msgs) + heading_str = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] Sending " + + else: + msg_color = mdc(color_unknown, intensity_msgs) + heading_str = titlestr+"[b]Sent[/b] "+txstr+" [b]State[/b] Unknown" + + else: + msg_color = mdc("Green", intensity_msgs) + heading_str = titlestr+"[b]Sent[/b] "+txstr+"\n[b]Received[/b] "+rxstr + + item = ListLXMessageCard( + text=m["content"].decode("utf-8"), + heading=heading_str, + md_bg_color=msg_color, + ) + item.sb_uid = m["hash"] + item.m = m + + def gen_del(mhash, item): + def x(): + yes_button = MDFlatButton( + text="Yes", + ) + no_button = MDFlatButton( + text="No", + ) + dialog = MDDialog( + text="Delete message?", + buttons=[ yes_button, no_button ], + ) + def dl_yes(s): + dialog.dismiss() + self.app.sideband.delete_message(mhash) + self.reload() + def dl_no(s): + dialog.dismiss() + + yes_button.bind(on_release=dl_yes) + no_button.bind(on_release=dl_no) + item.dmenu.dismiss() + dialog.open() + return x + + def gen_copy(msg, item): + def x(): + Clipboard.copy(msg) + RNS.log(str(item)) + item.dmenu.dismiss() + + return x + + dm_items = [ + { + "viewclass": "OneLineListItem", + "text": "Copy", + "height": dp(64), + "on_release": gen_copy(m["content"].decode("utf-8"), item) + }, + { + "text": "Delete", + "viewclass": "OneLineListItem", + "height": dp(64), + "on_release": gen_del(m["hash"], item) + } + ] + + item.dmenu = MDDropdownMenu( + caller=item.ids.msg_submenu, + items=dm_items, + position="center", + width_mult=4, + ) + + def callback_factory(ref): + def x(sender): + ref.dmenu.open() + return x + + # Bind menu open + item.ids.msg_submenu.bind(on_release=callback_factory(item)) + + self.added_item_hashes.append(m["hash"]) + self.widgets.append(item) + self.list.add_widget(item) + + if self.latest_message_timestamp == None or m["received"] > self.latest_message_timestamp: + self.latest_message_timestamp = m["received"] + + def get_widget(self): + return self.list + + def close_send_error_dialog(self, sender=None): + if self.send_error_dialog: + self.send_error_dialog.dismiss() \ No newline at end of file