Sideband/main.py
2022-07-06 12:19:05 +02:00

627 lines
29 KiB
Python

import RNS
import LXMF
import time
from kivy.logger import Logger, LOG_LEVELS
Logger.setLevel(LOG_LEVELS["error"])
from sideband.core import SidebandCore
from kivymd.app import MDApp
from kivy.core.window import Window
from kivy.clock import Clock
from kivy.lang.builder import Builder
from ui.layouts import root_layout
from ui.conversations import Conversations, MsgSync, NewConv
from ui.announces import Announces
from ui.messages import Messages, ts_format
from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem
from kivy.metrics import dp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
__version__ = "0.1.4"
__variant__ = "alpha"
class SidebandApp(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.title = "Sideband"
self.sideband = SidebandCore(self)
self.conversations_view = None
self.flag_new_conversations = False
self.flag_unread_conversations = False
self.flag_new_announces = False
self.lxmf_sync_dialog_open = False
self.sync_dialog = None
Window.softinput_mode = "below_target"
self.icon = self.sideband.asset_dir+"/images/icon.png"
#################################################
# General helpers #
#################################################
def build(self):
FONT_PATH = self.sideband.asset_dir+"/fonts"
# self.theme_cls.primary_palette = "Green"
self.theme_cls.theme_style = "Dark"
# self.theme_cls.theme_style = "Light"
screen = Builder.load_string(root_layout)
return screen
def jobs(self, delta_time):
if self.root.ids.screen_manager.current == "messages_screen":
self.messages_view.update()
if not self.root.ids.messages_scrollview.dest_known:
self.message_area_detect()
elif self.root.ids.screen_manager.current == "conversations_screen":
if self.flag_new_conversations:
RNS.log("Updating because of new conversations flag")
if self.conversations_view != None:
self.conversations_view.update()
if self.flag_unread_conversations:
RNS.log("Updating because of unread messages flag")
if self.conversations_view != None:
self.conversations_view.update()
if self.lxmf_sync_dialog_open and self.sync_dialog != None:
self.sync_dialog.ids.sync_progress.value = self.sideband.get_sync_progress()*100
self.sync_dialog.ids.sync_status.text = self.sideband.get_sync_status()
elif self.root.ids.screen_manager.current == "announces_screen":
if self.flag_new_announces:
RNS.log("Updating because of new announces flag")
if self.announces_view != None:
self.announces_view.update()
def on_start(self):
self.root.ids.screen_manager.app = self
self.root.ids.app_version_info.text = "Sideband v"+__version__+" "+__variant__
self.open_conversations()
Clock.schedule_interval(self.jobs, 1)
def widget_hide(self, w, hide=True):
if hasattr(w, "saved_attrs"):
if not hide:
w.height, w.size_hint_y, w.opacity, w.disabled = w.saved_attrs
del w.saved_attrs
elif hide:
w.saved_attrs = w.height, w.size_hint_y, w.opacity, w.disabled
w.height, w.size_hint_y, w.opacity, w.disabled = 0, None, 0, True
def quit_action(self, sender):
RNS.exit()
RNS.log("RNS shutdown complete")
MDApp.get_running_app().stop()
Window.close()
def announce_now_action(self, sender=None):
self.sideband.lxmf_announce()
yes_button = MDFlatButton(
text="OK",
)
dialog = MDDialog(
text="An announce for your LXMF destination was sent on all available interfaces",
buttons=[ yes_button ],
)
def dl_yes(s):
dialog.dismiss()
yes_button.bind(on_release=dl_yes)
dialog.open()
def conversation_update(self, context_dest):
pass
# if self.root.ids.messages_scrollview.active_conversation == context_dest:
# self.messages_view.update_widget()
# else:
# RNS.log("Not updating since context_dest does not match active")
#################################################
# Screens #
#################################################
### Messages (conversation) screen
######################################
def conversation_from_announce_action(self, context_dest):
if self.sideband.has_conversation(context_dest):
pass
else:
self.sideband.create_conversation(context_dest)
self.open_conversation(context_dest)
def conversation_action(self, sender):
self.open_conversation(sender.sb_uid)
def open_conversation(self, context_dest):
if self.sideband.config["propagation_by_default"]:
self.outbound_mode_propagation = True
else:
self.outbound_mode_propagation = False
self.root.ids.screen_manager.transition.direction = "left"
self.messages_view = Messages(self, context_dest)
for child in self.root.ids.messages_scrollview.children:
self.root.ids.messages_scrollview.remove_widget(child)
list_widget = self.messages_view.get_widget()
self.root.ids.messages_scrollview.add_widget(list_widget)
self.root.ids.messages_scrollview.scroll_y = 0
self.root.ids.messages_toolbar.title = self.sideband.peer_display_name(context_dest)
self.root.ids.messages_scrollview.active_conversation = context_dest
self.root.ids.nokeys_text.text = ""
self.message_area_detect()
self.update_message_widgets()
self.root.ids.screen_manager.current = "messages_screen"
self.sideband.read_conversation(context_dest)
def close_messages_action(self, sender=None):
self.open_conversations(direction="right")
def message_send_action(self, sender=None):
if self.root.ids.screen_manager.current == "messages_screen":
if self.outbound_mode_propagation and self.sideband.message_router.get_outbound_propagation_node() == None:
self.messages_view.send_error_dialog = MDDialog(
text="Error: Propagated delivery was requested, but no active LXMF propagation nodes were found. Cannot send message. Wait for a Propagation Node to announce on the network, or manually specify one in the settings.",
buttons=[
MDFlatButton(
text="OK",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
on_release=self.messages_view.close_send_error_dialog
)
],
)
self.messages_view.send_error_dialog.open()
else:
msg_content = self.root.ids.message_text.text
context_dest = self.root.ids.messages_scrollview.active_conversation
if self.sideband.send_message(msg_content, context_dest, self.outbound_mode_propagation):
self.root.ids.message_text.text = ""
self.jobs(0)
else:
self.messages_view.send_error_dialog = MDDialog(
text="Error: Could not send the message",
buttons=[
MDFlatButton(
text="OK",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
on_release=self.messages_view.close_send_error_dialog
)
],
)
self.messages_view.send_error_dialog.open()
def message_propagation_action(self, sender):
if self.outbound_mode_propagation:
self.outbound_mode_propagation = False
else:
self.outbound_mode_propagation = True
self.update_message_widgets()
def update_message_widgets(self):
toolbar_items = self.root.ids.messages_toolbar.ids.right_actions.children
mode_item = toolbar_items[1]
if not self.outbound_mode_propagation:
mode_item.icon = "lan-connect"
self.root.ids.message_text.hint_text = "Write message for direct delivery"
else:
mode_item.icon = "upload-network"
self.root.ids.message_text.hint_text = "Write message for propagation"
# self.root.ids.message_text.hint_text = "Write message for delivery via propagation nodes"
def key_query_action(self, sender):
context_dest = self.root.ids.messages_scrollview.active_conversation
if self.sideband.request_key(context_dest):
keys_str = "Public key information for "+RNS.prettyhexrep(context_dest)+" was requested from the network. Waiting for request to be answered."
self.root.ids.nokeys_text.text = keys_str
else:
keys_str = "Could not send request. Check your connectivity and addresses."
self.root.ids.nokeys_text.text = keys_str
def message_area_detect(self):
context_dest = self.root.ids.messages_scrollview.active_conversation
if self.sideband.is_known(context_dest):
self.root.ids.messages_scrollview.dest_known = True
self.widget_hide(self.root.ids.message_input_part, False)
self.widget_hide(self.root.ids.no_keys_part, True)
else:
self.root.ids.messages_scrollview.dest_known = False
if self.root.ids.nokeys_text.text == "":
keys_str = "The crytographic keys for the destination address are unknown at this time. You can wait for an announce to arrive, or query the network for the necessary keys."
self.root.ids.nokeys_text.text = keys_str
self.widget_hide(self.root.ids.message_input_part, True)
self.widget_hide(self.root.ids.no_keys_part, False)
### Conversations screen
######################################
def conversations_action(self, sender=None):
self.open_conversations()
def open_conversations(self, direction="left"):
self.root.ids.screen_manager.transition.direction = direction
self.root.ids.nav_drawer.set_state("closed")
self.conversations_view = Conversations(self)
for child in self.root.ids.conversations_scrollview.children:
self.root.ids.conversations_scrollview.remove_widget(child)
self.root.ids.conversations_scrollview.add_widget(self.conversations_view.get_widget())
self.root.ids.screen_manager.current = "conversations_screen"
self.root.ids.messages_scrollview.active_conversation = None
def lxmf_sync_action(self, sender):
if self.sideband.message_router.get_outbound_propagation_node() == None:
yes_button = MDFlatButton(
text="OK",
)
dialog = MDDialog(
text="No active LXMF propagation nodes were found. Cannot fetch messages. Wait for a Propagation Node to announce on the network, or manually specify one in the settings.",
buttons=[ yes_button ],
)
def dl_yes(s):
dialog.dismiss()
yes_button.bind(on_release=dl_yes)
dialog.open()
else:
if self.sideband.config["lxmf_sync_limit"]:
sl = self.sideband.config["lxmf_sync_max"]
else:
sl = None
self.sideband.request_lxmf_sync(limit=sl)
close_button = MDFlatButton(text="Close", font_size=dp(20))
# stop_button = MDFlatButton(text="Stop", font_size=dp(20))
dialog_content = MsgSync()
dialog = MDDialog(
title="LXMF Sync via "+RNS.prettyhexrep(self.sideband.message_router.get_outbound_propagation_node()),
type="custom",
content_cls=dialog_content,
buttons=[ close_button ],
)
dialog.d_content = dialog_content
def dl_close(s):
self.lxmf_sync_dialog_open = False
dialog.dismiss()
self.sideband.cancel_lxmf_sync()
# def dl_stop(s):
# self.lxmf_sync_dialog_open = False
# dialog.dismiss()
# self.sideband.cancel_lxmf_sync()
close_button.bind(on_release=dl_close)
# stop_button.bind(on_release=dl_stop)
self.lxmf_sync_dialog_open = True
self.sync_dialog = dialog_content
dialog.open()
dialog_content.ids.sync_progress.value = self.sideband.get_sync_progress()*100
dialog_content.ids.sync_status.text = self.sideband.get_sync_status()
def new_conversation_action(self, sender=None):
try:
yes_button = MDFlatButton(
text="OK",
font_size=dp(20),
)
no_button = MDFlatButton(
text="Cancel",
font_size=dp(20),
)
dialog_content = NewConv()
dialog = MDDialog(
title="New Conversation",
type="custom",
content_cls=dialog_content,
buttons=[ yes_button, no_button ],
)
dialog.d_content = dialog_content
def dl_yes(s):
new_result = False
try:
n_address = dialog.d_content.ids["n_address_field"].text
n_name = dialog.d_content.ids["n_name_field"].text
n_trusted = dialog.d_content.ids["n_trusted"].active
RNS.log("Create conversation "+str(n_address)+"/"+str(n_name)+"/"+str(n_trusted))
new_result = self.sideband.new_conversation(n_address, n_name, n_trusted)
except Exception as e:
RNS.log("Error while creating conversation: "+str(e), RNS.LOG_ERROR)
if new_result:
dialog.d_content.ids["n_address_field"].error = False
dialog.dismiss()
self.open_conversations()
else:
dialog.d_content.ids["n_address_field"].error = True
# dialog.d_content.ids["n_error_field"].text = "Could not create conversation. Check your input."
def dl_no(s):
dialog.dismiss()
yes_button.bind(on_release=dl_yes)
no_button.bind(on_release=dl_no)
dialog.open()
except Exception as e:
RNS.log("Error while creating new conversation dialog: "+str(e), RNS.LOG_ERROR)
### Information/version screen
######################################
def information_action(self, sender=None):
def link_exec(sender=None, event=None):
RNS.log("Click")
import webbrowser
webbrowser.open("https://unsigned.io/sideband")
info = "This is Sideband v"+__version__+" "+__variant__+".\n\nHumbly build using the following open components:\n\n - [b]Reticulum[/b] (MIT License)\n - [b]LXMF[/b] (MIT License)\n - [b]KivyMD[/b] (MIT License)\n - [b]Kivy[/b] (MIT License)\n - [b]Python[/b] (PSF License)"+"\n\nGo to [u][ref=link]https://unsigned.io/sideband[/ref][/u] to support the project.\n\nThe Sideband app is Copyright (c) 2022 Mark Qvist / unsigned.io\n\nPermission is granted to freely share and distribute binary copies of Sideband v"+__version__+" "+__variant__+", so long as no payment or compensation is charged for said distribution or sharing.\n\nIf you were charged or paid anything for this copy of Sideband, please report it to [b]license@unsigned.io[/b].\n\nTHIS IS EXPERIMENTAL SOFTWARE - USE AT YOUR OWN RISK AND RESPONSIBILITY"
self.root.ids.information_info.text = info
self.root.ids.information_info.bind(on_ref_press=link_exec)
self.root.ids.screen_manager.transition.direction = "left"
self.root.ids.screen_manager.current = "information_screen"
self.root.ids.nav_drawer.set_state("closed")
### Prepare Settings screen
def settings_action(self, sender=None):
self.root.ids.screen_manager.transition.direction = "left"
def save_disp_name(sender=None, event=None):
in_name = self.root.ids.settings_display_name.text
if in_name == "":
new_name = "Anonymous Peer"
else:
new_name = in_name
self.sideband.config["display_name"] = new_name
self.sideband.save_configuration()
def save_prop_addr(sender=None, event=None):
in_addr = self.root.ids.settings_propagation_node_address.text
new_addr = None
if in_addr == "":
new_addr = None
self.root.ids.settings_propagation_node_address.error = False
else:
if len(in_addr) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8*2:
new_addr = None
else:
try:
new_addr = bytes.fromhex(in_addr)
except Exception as e:
new_addr = None
if new_addr == None:
self.root.ids.settings_propagation_node_address.error = True
else:
self.root.ids.settings_propagation_node_address.error = False
self.sideband.config["lxmf_propagation_node"] = new_addr
self.sideband.set_active_propagation_node(self.sideband.config["lxmf_propagation_node"])
def save_start_announce(sender=None, event=None):
RNS.log("Save announce")
self.sideband.config["start_announce"] = self.root.ids.settings_start_announce.active
self.sideband.save_configuration()
def save_lxmf_delivery_by_default(sender=None, event=None):
RNS.log("Save propagation")
self.sideband.config["propagation_by_default"] = self.root.ids.settings_lxmf_delivery_by_default.active
self.sideband.save_configuration()
def save_lxmf_sync_limit(sender=None, event=None):
RNS.log("Save propagation")
self.sideband.config["lxmf_sync_limit"] = self.root.ids.settings_lxmf_sync_limit.active
self.sideband.save_configuration()
self.root.ids.settings_lxmf_address.text = RNS.hexrep(self.sideband.lxmf_destination.hash, delimit=False)
self.root.ids.settings_display_name.text = self.sideband.config["display_name"]
self.root.ids.settings_display_name.bind(on_text_validate=save_disp_name)
self.root.ids.settings_display_name.bind(focus=save_disp_name)
if self.sideband.config["lxmf_propagation_node"] == None:
prop_node_addr = ""
else:
prop_node_addr = RNS.hexrep(self.sideband.config["lxmf_propagation_node"], delimit=False)
self.root.ids.settings_propagation_node_address.text = prop_node_addr
self.root.ids.settings_propagation_node_address.bind(on_text_validate=save_prop_addr)
self.root.ids.settings_propagation_node_address.bind(focus=save_prop_addr)
self.root.ids.settings_start_announce.active = self.sideband.config["start_announce"]
self.root.ids.settings_start_announce.bind(active=save_start_announce)
self.root.ids.settings_lxmf_delivery_by_default.active = self.sideband.config["propagation_by_default"]
self.root.ids.settings_lxmf_delivery_by_default.bind(active=save_lxmf_delivery_by_default)
if self.sideband.config["lxmf_sync_limit"] == None or self.sideband.config["lxmf_sync_limit"] == False:
sync_limit = False
else:
sync_limit = True
self.root.ids.settings_lxmf_sync_limit.active = sync_limit
self.root.ids.settings_lxmf_sync_limit.bind(active=save_lxmf_sync_limit)
self.root.ids.screen_manager.current = "settings_screen"
self.root.ids.nav_drawer.set_state("closed")
def close_settings_action(self, sender=None):
self.open_conversations(direction="right")
### Connectivity screen
######################################
def connectivity_action(self, sender=None):
def save_connectivity(sender=None, event=None):
RNS.log("Save connectivity")
self.sideband.config["connect_local"] = self.root.ids.connectivity_use_local.active
self.sideband.config["connect_local_groupid"] = self.root.ids.connectivity_local_groupid.text
self.sideband.config["connect_local_ifac_netname"] = self.root.ids.connectivity_local_ifac_netname.text
self.sideband.config["connect_local_ifac_passphrase"] = self.root.ids.connectivity_local_ifac_passphrase.text
self.sideband.config["connect_tcp"] = self.root.ids.connectivity_use_tcp.active
self.sideband.config["connect_tcp_host"] = self.root.ids.connectivity_tcp_host.text
self.sideband.config["connect_tcp_port"] = self.root.ids.connectivity_tcp_port.text
self.sideband.config["connect_tcp_ifac_netname"] = self.root.ids.connectivity_tcp_ifac_netname.text
self.sideband.config["connect_tcp_ifac_passphrase"] = self.root.ids.connectivity_tcp_ifac_passphrase.text
self.sideband.config["connect_i2p"] = self.root.ids.connectivity_use_i2p.active
self.sideband.config["connect_i2p_b32"] = self.root.ids.connectivity_i2p_b32.text
self.sideband.config["connect_i2p_ifac_netname"] = self.root.ids.connectivity_i2p_ifac_netname.text
self.sideband.config["connect_i2p_ifac_passphrase"] = self.root.ids.connectivity_i2p_ifac_passphrase.text
self.sideband.save_configuration()
info = "By default, sideband will try to discover and connect to any available Reticulum network via active WiFi and/or Ethernet interfaces. If any Reticulum Transport Instances are found, Sideband will use these to connect to wider Reticulum networks. You can disable this behaviour if you don't want it.\n\n"
info += "You can connect also connect to a network via a remote or local Reticulum instance using TCP or I2P. [b]Please Note![/b] Connecting via I2P requires that you already have I2P running on your device, and the SAM API enabled.\n\n"
info += "For changes to connectivity to take effect, you must shut down and restart Sideband."
self.root.ids.connectivity_info.text = info
self.root.ids.connectivity_use_local.active = self.sideband.config["connect_local"]
self.root.ids.connectivity_local_groupid.text = self.sideband.config["connect_local_groupid"]
self.root.ids.connectivity_local_ifac_netname.text = self.sideband.config["connect_local_ifac_netname"]
self.root.ids.connectivity_local_ifac_passphrase.text = self.sideband.config["connect_local_ifac_passphrase"]
self.root.ids.connectivity_use_tcp.active = self.sideband.config["connect_tcp"]
self.root.ids.connectivity_tcp_host.text = self.sideband.config["connect_tcp_host"]
self.root.ids.connectivity_tcp_port.text = self.sideband.config["connect_tcp_port"]
self.root.ids.connectivity_tcp_ifac_netname.text = self.sideband.config["connect_tcp_ifac_netname"]
self.root.ids.connectivity_tcp_ifac_passphrase.text = self.sideband.config["connect_tcp_ifac_passphrase"]
self.root.ids.connectivity_use_i2p.active = self.sideband.config["connect_i2p"]
self.root.ids.connectivity_i2p_b32.text = self.sideband.config["connect_i2p_b32"]
self.root.ids.connectivity_i2p_ifac_netname.text = self.sideband.config["connect_i2p_ifac_netname"]
self.root.ids.connectivity_i2p_ifac_passphrase.text = self.sideband.config["connect_i2p_ifac_passphrase"]
self.root.ids.connectivity_use_local.bind(active=save_connectivity)
self.root.ids.connectivity_local_groupid.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_local_ifac_netname.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_local_ifac_passphrase.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_use_tcp.bind(active=save_connectivity)
self.root.ids.connectivity_tcp_host.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_tcp_port.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_tcp_ifac_netname.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_tcp_ifac_passphrase.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_use_i2p.bind(active=save_connectivity)
self.root.ids.connectivity_i2p_b32.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_i2p_ifac_netname.bind(on_text_validate=save_connectivity)
self.root.ids.connectivity_i2p_ifac_passphrase.bind(on_text_validate=save_connectivity)
self.root.ids.screen_manager.transition.direction = "left"
self.root.ids.screen_manager.current = "connectivity_screen"
self.root.ids.nav_drawer.set_state("closed")
def close_connectivity_action(self, sender=None):
self.open_conversations(direction="right")
### Announce Stream screen
######################################
def announces_action(self, sender=None):
self.root.ids.screen_manager.transition.direction = "left"
self.root.ids.nav_drawer.set_state("closed")
self.announces_view = Announces(self)
# info = "The [b]Announce Stream[/b] feature is not yet implemented in Sideband.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/sideband[/ref][/u] to support the project."
# self.root.ids.announces_info.text = info
# self.root.ids.announces_info.bind(on_ref_press=link_exec)
for child in self.root.ids.announces_scrollview.children:
self.root.ids.announces_scrollview.remove_widget(child)
self.root.ids.announces_scrollview.add_widget(self.announces_view.get_widget())
self.root.ids.screen_manager.current = "announces_screen"
def announce_filter_action(self, sender=None):
pass
#################################################
# Unimplemented Screens #
#################################################
def keys_action(self, sender=None):
def link_exec(sender=None, event=None):
RNS.log("Click")
import webbrowser
webbrowser.open("https://unsigned.io/sideband")
info = "The [b]Encryption Keys[/b] import and export feature is not yet implemented in Sideband.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/sideband[/ref][/u] to support the project."
self.root.ids.keys_info.text = info
self.root.ids.keys_info.bind(on_ref_press=link_exec)
self.root.ids.screen_manager.transition.direction = "left"
self.root.ids.screen_manager.current = "keys_screen"
self.root.ids.nav_drawer.set_state("closed")
def map_action(self, sender=None):
def link_exec(sender=None, event=None):
RNS.log("Click")
import webbrowser
webbrowser.open("https://unsigned.io/sideband")
info = "The [b]Local Area[/b] feature is not yet implemented in Sideband.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/sideband[/ref][/u] to support the project."
self.root.ids.map_info.text = info
self.root.ids.map_info.bind(on_ref_press=link_exec)
self.root.ids.screen_manager.transition.direction = "left"
self.root.ids.screen_manager.current = "map_screen"
self.root.ids.nav_drawer.set_state("closed")
def broadcasts_action(self, sender=None):
def link_exec(sender=None, event=None):
RNS.log("Click")
import webbrowser
webbrowser.open("https://unsigned.io/sideband")
info = "The [b]Local Broadcasts[/b] feature will allow you to send and listen for local broadcast transmissions on connected radio, LoRa and WiFi interfaces.\n\n[b]Local Broadcasts[/b] makes it easy to establish public information exchange with anyone in direct radio range, or even with large areas far away using the [i]Remote Broadcast Repeater[/i] feature.\n\nThese features are not yet implemented in Sideband.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/sideband[/ref][/u] to support the project."
self.root.ids.broadcasts_info.text = info
self.root.ids.broadcasts_info.bind(on_ref_press=link_exec)
self.root.ids.screen_manager.transition.direction = "left"
self.root.ids.screen_manager.current = "broadcasts_screen"
self.root.ids.nav_drawer.set_state("closed")
def guide_action(self, sender=None):
def link_exec(sender=None, event=None):
RNS.log("Click")
import webbrowser
webbrowser.open("https://unsigned.io/sideband")
info = "The [b]Guide[/b] section is not yet implemented in Sideband.\n\nWant it faster? Go to [u][ref=link]https://unsigned.io/sideband[/ref][/u] to support the project."
self.root.ids.guide_info.text = info
self.root.ids.guide_info.bind(on_ref_press=link_exec)
self.root.ids.screen_manager.transition.direction = "left"
self.root.ids.screen_manager.current = "guide_screen"
self.root.ids.nav_drawer.set_state("closed")
SidebandApp().run()