From f201fab2c3696a74033d0d7cc46095443115e79a Mon Sep 17 00:00:00 2001 From: freqnik Date: Mon, 15 Apr 2024 15:48:15 -0400 Subject: [PATCH] Add filter plan nodes. Fix PlanAccordion height. Merge @Tkd-Alex/2.0/search branch into 2.0. Amend search() to include list of values. #66, #36, #93, #89 --- src/cli/sentinel.py | 174 +++++++++++++++++++++++++++++++++++++++++--- src/kv/meile.kv | 78 +++++++++++++++----- src/ui/screens.py | 81 ++++++++++----------- src/ui/widgets.py | 18 ++++- 4 files changed, 276 insertions(+), 75 deletions(-) diff --git a/src/cli/sentinel.py b/src/cli/sentinel.py index a8538e6a..aab2caae 100644 --- a/src/cli/sentinel.py +++ b/src/cli/sentinel.py @@ -8,6 +8,7 @@ from datetime import datetime,timedelta import time from urllib.parse import urlparse +import copy from treelib import Tree from treelib.exceptions import DuplicatedNodeIdError @@ -27,12 +28,13 @@ v2ray_tun2routes_connect_bash = MeileConfig.resource_path("../bin/routes.sh") class NodeTreeData(): - NodeTree = None - NodeScores = {} - NodeLocations = {} - NodeTypes = {} - NodeHealth = {} - NodeFormula = {} + BackupNodeTree = None + NodeTree = None + NodeScores = {} + NodeLocations = {} + NodeTypes = {} + NodeHealth = {} + NodeFormula = {} def __init__(self, node_tree): if not node_tree: @@ -93,9 +95,9 @@ def get_nodes(self, latency, *kwargs): '''Parse out old node versions < 0.7.0''' d[NodeKeys.NodesInfoKeys[14]] = d[NodeKeys.NodesInfoKeys[14]].split('-')[0] - version = d[NodeKeys.NodesInfoKeys[14]].replace('.','') - if version not in NodeKeys.NodeVersions: - continue + #version = d[NodeKeys.NodesInfoKeys[14]].replace('.','') + #if version not in NodeKeys.NodeVersions: + # continue # Gigabyte Prices d[NodeKeys.NodesInfoKeys[2]] = self.return_denom(d[NodeKeys.NodesInfoKeys[2]]) @@ -111,7 +113,10 @@ def get_nodes(self, latency, *kwargs): except Exception as e: print(str(e)) # print the exception in this early build to identify any issues building the nodetree pass - + + # Used for Search and Plans + self.BackupNodeTree = copy.deepcopy(self.NodeTree) + # For pretty output. Unicode is used in treelib > 1.6.1 self.NodeTree.show() # User-submitted Ratings @@ -125,7 +130,154 @@ def get_nodes(self, latency, *kwargs): # Get MathNodes NodeFormula self.GetNodeFormula() - + + + + # Filter nodetree. + # key; what we want to filter, for example: Moniker, Type, Health + # value; query value + # between; value must be between[0], between[1], for example: + # key = "Hourly Price", between = ("5.3426dvpn", "15.3426dvpn") + # key = "Scores", between = (8, 10) + # from_backup; if true it will be used the backupped data, else will be used the 'renderized' one (maybe already filtered) + # perfect_match; if true, value must be equal for example: + # perfect_match = True, key = "Moniker", value = "Pinco" will match only Moniker === Pinco + # perfect_match = False, key = "Moniker", value = "Pinco" will match only Moniker like Pincopallo, Pizzapinco10, Pincopallino, Pinco1 + + def search(self, key: str, value = None, between: tuple = (), from_backup: bool = True, perfect_match: bool = False, is_list: bool = False): + if value is None and len(between) == 0: + # at least one of value or between must be setted + return + + amount_rx = r'^(\.\d|\d+\.\d+|\d+)' + key = key.title() + + # Prepare the "between" values out of iteration + if value is None and len(between) == 2: + a, b = between[0], between[1] + if key in ['Hourly Price', 'Price']: + a = amount_denon_dict(a) + b = amount_denon_dict(b) + # print(f"[DEBUG] a: {a}, b: {b}") + + if a is None or b is None: + # unable to continue + return + + if a["denom"] != b["denom"]: + # unable to use different denom in between + return + + # It the same of b["denom"], just a variable rename + def_denom = a["denom"] + if def_denom == "udvpn": + # btw, probably no one will use udvpn as search field + a["denom"] = b["denom"] = "dvpn" + a["amount"] = a["amount"] // 1000000 + b["amount"] = b["amount"] // 1000000 + else: + a = float(re.search(amount_rx, a).group(0)) + b = float(re.search(amount_rx, b).group(0)) + + # Create a copy of Tree please ... + # Under the iteration of keys I will delete all the nodes that doesn't match our query + filtered = copy.deepcopy(self.BackupNodeTree if from_backup is True else self.NodeTree) + # Iteration via the original data, in order to prevent "RuntimeError: dictionary changed size during iteration" + for identifier, content in (self.BackupNodeTree if from_backup is True else self.NodeTree).nodes.items(): + if identifier.startswith("sentnode"): + if key in content.data: + # use in... / wherlike / contains + if value is not None: + if is_list: + for v in value: + if perfect_match is True: + if v.lower().strip() != content.data[key].lower(): + filtered.remove_node(identifier) + elif v.lower().strip() not in content.data[key].lower(): + # use in... / wherlike / contains + filtered.remove_node(identifier) + else: + if perfect_match is True: + if value.lower().strip() != content.data[key].lower(): + filtered.remove_node(identifier) + elif value.lower().strip() not in content.data[key].lower(): + # use in... / wherlike / contains + filtered.remove_node(identifier) + elif len(between) == 2: + + # ups, following this: https://github.com/MathNodes/meile-gui/commit/622e501d332f0a34009b77548c4672e0ae32577b#diff-3729b5451a4398b2a4fd75a4bf0062d9bd5040677dd766ade023084aa9c03379R87 + # NodesInfoKeys = ["Moniker","Address","Price","Hourly Price", "Country","Speed","Latency","Peers","Handshake","Type","Version","Status"] + # NodesInfoKeys = ["Moniker","Address","Price","Hourly Price", "Country","City","Latitude","Longitude","Download","Upload","Peers","Max Peers","Handshake","Type","Version"] + + # I'm so crazy and I like it + if key in ['Hourly Price', 'Price']: + # 'Hourly Price': '0.0185scrt,0.0008atom,1.8719dec,0.0189osmo,4.16dvpn', + # 'Price': '0.0526scrt,0.0092atom,1.1809dec,0.1227osmo,15.3426dvpn', + prices = content.data[key].split(",") + # Now we have an array: ['0.0185scrt', '0.0008atom', '1.8719dec', '0.0189osmo', '4.16dvpn'] + # Convert array with denom as key and amount as value: + prices = { + amount_denon_dict(p)["denom"]: amount_denon_dict(p)["amount"] for p in prices + } + # print(f"[DEBUG] {identifier} | prices: {prices}") + if def_denom not in prices: + # uhm, unable to continue, probably the node doesn't support this denom (?) + # remove anyway from the tree + filtered.remove_node(identifier) + else: + # Make sure a is min and b is max + _min = min(a["amount"], b["amount"]) + _max = max(a["amount"], b["amount"]) + if prices[def_denom] > _max or prices[def_denom] < _min: + # print(f"[DEBUG] {identifier} | remove basecause > {_max} (max) or < {_min} (min)") + filtered.remove_node(identifier) + else: + # 'Latency': '1.762s', + # 'Peers': '0', + # --> not managed: 'Speed': '123.93MB+520.20MB', + + #Extract only number + node_value = float(re.search(amount_rx, content.data[key]).group(0)) + # Make sure a is min and b is max + _min = min(a, b) + _max = max(a, b) + if node_value > _max or node_value < _min: + filtered.remove_node(identifier) + + + # Type: wireguard / v2ray + # Type: residential / hosting .... (uhmm) - ConnectionType + elif key == "ConnectionType": + if identifier not in self.NodeTypes: + filtered.remove_node(identifier) + else: + if self.NodeTypes[identifier] != value: + filtered.remove_node(identifier) + elif key == "Health": + if identifier not in self.NodeHealth: + filtered.remove_node(identifier) + else: + as_bool = value if isinstance(value, bool) else (value.lower() == "true") + if self.NodeHealth[identifier] != as_bool: + filtered.remove_node(identifier) + elif key == "Scores": + if identifier not in self.NodeScores: + filtered.remove_node(identifier) + else: + rating = float(self.NodeScores[identifier][0]) + if value is not None and float(value) != rating: + filtered.remove_node(identifier) + elif len(between) == 2: + # Make sure a is min and b is max + _min = min(a, b) + _max = max(a, b) + if rating > _max or rating < _min: + filtered.remove_node(identifier) + + # Always override the 'renderized' one (maybe already filtered) + self.NodeTree = filtered + + def GetHealthCheckData(self): Request = HTTPRequests.MakeRequest(TIMEOUT=4) http = Request.hadapter() diff --git a/src/kv/meile.kv b/src/kv/meile.kv index 43b5eb4b..d62be592 100644 --- a/src/kv/meile.kv +++ b/src/kv/meile.kv @@ -250,7 +250,8 @@ WindowManager: icon: "animation-outline" theme_text_color: "Custom" text_color: get_color_from_hex("#fcb711") - + on_release: root.switch_to_plan_window() + ToolTipMDIconButton: tooltip_text: "Settings" icon: "cog-outline" @@ -831,13 +832,13 @@ WindowManager: MDLabel: text: "Plan" bold: True - size_hint_x: 2.5 + size_hint_x: 2.9 MDLabel: text: "Nodes" bold: True - size_hint_x: 1.3 + size_hint_x: 1.1 MDLabel: text: "Countries" @@ -850,7 +851,7 @@ WindowManager: size_hint_x: 1.4 MDLabel: - text: "UUID" + text: bold: True size_hint_x: 1 @@ -968,14 +969,14 @@ WindowManager: rows: 2 cols: 6 AsyncImage: - size_hint_x: .3 + size_hint_x: .1 width: 100 source: root.logo_image MDLabel: padding: [10, 0, 0, 0] text: root.plan_name markup: True - size_hint_x: 2 + size_hint_x: 2.5 theme_text_color: "Custom" font_style: "H6" font_size: sp(16) @@ -985,28 +986,29 @@ WindowManager: MDLabel: text: root.num_of_nodes markup: True - size_hint_x: 1 + size_hint_x: 1.4 font_name: "../../src/fonts/arial-unicode-ms.ttf" MDLabel: text: root.num_of_countries markup: True - size_hint_x: 1 + size_hint_x: 1.4 font_name: "../../src/fonts/arial-unicode-ms.ttf" MDLabel: text: root.cost markup: True - size_hint_x: 1 + size_hint_x: 1.8 font_name: "../../src/fonts/arial-unicode-ms.ttf" - MDRaisedButton: - id: plan_sub_button - text: "SUBSCRIBE" - font_size: dp(9) - pos_hint: {"center_x": .1, "center_y": .5} - on_press: root.copy_seed_phrase() - size_hint: None, None - size: 15,10 + MDFlatButton: + #md_bg_color: get_color_from_hex("#121212") + pos_hint: {'x' : .67, 'y': .275} + size_hint_x: .5 + Image: + id: subscribe_button + size_hint: 1.5,1.5 + source: "../../src/imgs/SubscribeButton.png" + HSeparator: HSeparator: HSeparator: @@ -1057,35 +1059,71 @@ WindowManager: HSeparator: : - rows: 2 - cols: 4 - adaptive_height: True + rows: 3 + cols: 6 + #adaptive_height: True + height: 90 #orientation: 'horizontal' MDLabel text: "[b]UUID[/b]" markup: True + size_hint_x: 2 + MDLabel + text: "[b]ID[/b]" + markup: True + + MDLabel text: "[b]Expires[/b]" markup: True + size_hint_x: 1.3 + MDLabel text: "[b]Deposit[/b]" markup: True + MDLabel text: "[b]Coin[/b]" markup: True + + TooltipMDIconButton: + icon: "table-filter" + tooltip_text: "Filter Nodes" + size_hint_x: .25 + theme_text_color: "Custom" + text_color: app.theme_cls.primary_color + on_release: root.filter_nodes() + MDLabel text: root.uuid markup: True + font_size: sp(10) + size_hint_x: 2 + MDLabel + text: root.id + markup: True + font_size: sp(10) MDLabel text: root.expires markup: True + font_size: sp(11) + size_hint_x: 1.3 MDLabel text: root.deposit markup: True + font_size: sp(11) MDLabel text: root.coin markup: True + font_size: sp(11) + + MDLabel + MDLabel + MDLabel + MDLabel + MDLabel + MDLabel : rows: 2 diff --git a/src/ui/screens.py b/src/ui/screens.py index 3b9aaa68..c3b6bdf5 100644 --- a/src/ui/screens.py +++ b/src/ui/screens.py @@ -5,7 +5,7 @@ from typedef.konstants import NodeKeys, TextStrings, MeileColors, HTTParams, IBCTokens from cli.sentinel import disconnect as Disconnect import main.main as Meile -from ui.widgets import WalletInfoContent, MDMapCountryButton, RatingContent, NodeRV, NodeRV2, NodeAccordion, NodeRow, NodeDetails, PlanAccordionm, PlanRow, PlanDetails +from ui.widgets import WalletInfoContent, MDMapCountryButton, RatingContent, NodeRV, NodeRV2, NodeAccordion, NodeRow, NodeDetails, PlanAccordion, PlanRow, PlanDetails from utils.qr import QRCode from cli.wallet import HandleWalletFunctions from conf.meile_config import MeileGuiConfig @@ -377,25 +377,17 @@ def __init__(self, node_tree, **kwargs): #) #self.menu.bind() - def build(self, dt): - ''' - OurWorld.CONTINENTS.remove(OurWorld.CONTINENTS[1]) + def build(self, dt): + # Check to build Map + self.build_meile_map() + # Build alphabetical country recyclerview tree data + self.build_country_tree() - for name_tab in OurWorld.CONTINENTS: - tab = Tab(tab_label_text=name_tab) - self.ids.android_tabs.add_widget(tab) - - self.on_tab_switch( - None, - None, - None, - self.ids.android_tabs.ids.layout.children[-1].text - ) - ''' + # TODO: Would be good to process this in a background thread so as to not hang the UI + self.get_ip_address(None) - # Check to build Map - self.build_meile_map() + def build_country_tree(self): CountryTree = [] CountryTreeTags = [] @@ -410,9 +402,7 @@ def build(self, dt): for ctree in CountryTree: if tag == ctree.tag: self.add_country_rv_data(self.build_node_data(ctree)) - - # WOuld be good to process this in a background thread so as to not hang the UI - self.get_ip_address(None) + def build_node_data(self, ncountry): floc = "../imgs/" @@ -476,6 +466,11 @@ def add_country_rv_data(self, NodeCountries): }, ) + def refresh_country_recycler(self): + self.ids.rv.data = None + self.build_country_tree() + self.ids.rv.refresh_from_data() + def AddCountryNodePins(self, clear): Config = MeileGuiConfig() try: @@ -958,6 +953,15 @@ def switch_to_sub_window(self): self.carousel.add_widget(self.NodeWidget) self.carousel.load_slide(self.NodeWidget) + def switch_to_plan_window(self): + try: + self.carousel.remove_widget(self.NodeWidget) + except Exception as e: + print(str(e)) + self.NodeWidget = PlanScreen(name=WindowNames.PLAN) + self.carousel.add_widget(self.NodeWidget) + self.carousel.load_slide(self.NodeWidget) + def close_sub_window(self): self.carousel.remove_widget(self.NodeWidget) self.carousel.load_previous() @@ -1556,22 +1560,7 @@ def add_rv_data(self, node, flagloc): else: HealthButton = MeileColors.SICK_ICON HealthToolTip = TextStrings.FailedHealthCheck - '''will use for Meile plans - item = NodeAccordion( - node=NodeRow( - moniker=node[NodeKeys.NodesInfoKeys[0]], - location=node[NodeKeys.NodesInfoKeys[4]], - speed=speedText, - status="Status", - protocol=node[NodeKeys.NodesInfoKeys[13]], - node_type=ToolTipText, - ), - content=NodeDetails( - health_check=True if HealthToolTip == TextStrings.PassedHealthCheck else False, - price=node[NodeKeys.NodesInfoKeys[2]], - ) - ) - ''' + self.ids.rv.data.append( { "viewclass" : "RecycleViewRow", @@ -1610,6 +1599,7 @@ def set_previous_screen(self): class PlanScreen(MDBoxLayout): def __init__(self, **kwargs): + super(PlanScreen, self).__init__() self.mw = Meile.app.root.get_screen(WindowNames.MAIN_WINDOW) wallet = self.mw.address Request = HTTPRequests.MakeRequest() @@ -1617,11 +1607,11 @@ def __init__(self, **kwargs): req = http.get(HTTParams.PLAN_API + HTTParams.API_PLANS, auth=HTTPBasicAuth(scrtsxx.PLANUSERNAME, scrtsxx.PLANPASSWORD)) plan_data = req.json() - req2 = http.get(HTTParams.PLAN_API + HTTParams.API_PLANS_SUMBS % wallet, auth=HTTPBasicAuth(scrtsxx.PLANUSERNAME, scrtsxx.PLANPASSWORD)) + req2 = http.get(HTTParams.PLAN_API + HTTParams.API_PLANS_SUBS % wallet, auth=HTTPBasicAuth(scrtsxx.PLANUSERNAME, scrtsxx.PLANPASSWORD)) user_enrolled_plans = req2.json() for pd in plan_data: - self.build_plans(pd, user_enrolled_plans) + self.build_plans( pd, user_enrolled_plans) def build_plans(self, data, plans): @@ -1636,21 +1626,26 @@ def build_plans(self, data, plans): item = PlanAccordion( node=PlanRow( plan_name=data['plan_name'], - num_of_nodes=45, - num_of_countries=30, - cost=str(float(data['plan_price'] / IBCTokens.SATOSHI)) + data['plan_denom'], + num_of_nodes=str(45), + num_of_countries=str(30), + cost=str(round(float(data['plan_price'] / IBCTokens.SATOSHI),2)) + data['plan_denom'], logo_image=data['logo'], ), content=PlanDetails( uuid=data['uuid'], + id=str(plan['subscription_id']), expires=plan['expires'], - deposit=plan['amt_paid'], + deposit=str(round(float(plan['amt_paid']),2)), coin=plan['amt_denom'], ) ) self.ids.rv.add_widget(item) - self.ids.rv.add_widget(item) + + def set_previous_screen(self): + mw = Meile.app.root.get_screen(WindowNames.MAIN_WINDOW) + mw.carousel.remove_widget(mw.NodeWidget) + mw.carousel.load_previous() ''' This is the card class of the country cards on the left panel ''' diff --git a/src/ui/widgets.py b/src/ui/widgets.py index 67f3bf71..79bc3a44 100644 --- a/src/ui/widgets.py +++ b/src/ui/widgets.py @@ -35,7 +35,8 @@ import re import psutil import time - +from requests.auth import HTTPBasicAuth +import json from typedef.konstants import IBCTokens, HTTParams, MeileColors, NodeKeys from typedef.win import CoinsList, WindowNames @@ -507,10 +508,25 @@ class PlanRow(MDGridLayout): class PlanDetails(MDGridLayout): uuid = StringProperty() + id = StringProperty() expires = StringProperty() deposit = StringProperty() coin = StringProperty() + def filter_nodes(self): + from fiat.stripe_pay import scrtsxx + mw = Meile.app.root.get_screen(WindowNames.MAIN_WINDOW) + + Request = HTTPRequests.MakeRequest() + http = Request.hadapter() + req = http.get(HTTParams.PLAN_API + HTTParams.API_PLANS_NODES % self.uuid, auth=HTTPBasicAuth(scrtsxx.PLANUSERNAME, scrtsxx.PLANPASSWORD)) + + plan_nodes_data = json.loads(req.json()) + + mw.NodeTree.search(key=NodeKeys.NodesInfoKeys[1], value=plan_nodes_data, perfect_match=True, is_list=True) + + mw.refresh_country_recycler() + class PlanAccordion(ButtonBehavior, MDGridLayout): node = ObjectProperty() # Main node info