view.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. # This file is part of DarkFi (https://dark.fi)
  2. #
  3. # Copyright (C) 2020-2025 Dyne.org foundation
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. import urwid
  18. import logging
  19. import asyncio
  20. import datetime as dt
  21. from enum import Enum
  22. from src.model import Model
  23. class DnetWidget(urwid.WidgetWrap):
  24. def __init__(self, name, kind):
  25. self.name = name
  26. self.kind = kind
  27. def selectable(self):
  28. return True
  29. def keypress(self, size, key):
  30. return key
  31. def update(self, txt):
  32. super().__init__(txt)
  33. self._w = urwid.AttrWrap(self._w, None)
  34. self._w.focus_attr = 'line'
  35. class NodeState(Enum):
  36. ON = 0
  37. OFF = 1
  38. class Node(DnetWidget):
  39. def __init__(self, name, kind, state):
  40. self.state = state
  41. super().__init__(name, kind)
  42. def set_txt(self):
  43. if self.state == NodeState.OFF:
  44. txt = urwid.Text(f"{self.name} (offline)")
  45. super().update(txt)
  46. else:
  47. txt = urwid.Text(f"{self.name}")
  48. super().update(txt)
  49. class Session(DnetWidget):
  50. def set_txt(self):
  51. txt = urwid.Text(f" {self.kind}")
  52. super().update(txt)
  53. class Slot(DnetWidget):
  54. def set_txt(self, i, addr):
  55. self.i = i
  56. match self.kind:
  57. case "outbound-slot":
  58. self.addr = addr[0]
  59. self.id = addr[1]
  60. txt = urwid.Text(f" {self.i}: {self.addr}")
  61. super().update(txt)
  62. case "spawn-slot":
  63. self.id = addr
  64. txt = urwid.Text(f" {addr}")
  65. super().update(txt)
  66. case "manual-slot" | "seed-slot" | "inbound-slot":
  67. self.addr = addr
  68. txt = urwid.Text(f" {self.addr}")
  69. super().update(txt)
  70. case "direct-slot":
  71. self.addr = addr[0] if addr else addr
  72. txt = urwid.Text(f" {self.addr}")
  73. super().update(txt)
  74. class View():
  75. palette = [
  76. ('body','default','default','standout'),
  77. ('line','dark cyan','black','standout'),
  78. ]
  79. def __init__(self, model):
  80. self.model = model
  81. self.pile = urwid.Pile([urwid.Text("")])
  82. scroll = urwid.ScrollBar(urwid.Scrollable(self.pile))
  83. rightbox = urwid.LineBox(scroll)
  84. leftbox = urwid.LineBox(scroll)
  85. self.listwalker = urwid.SimpleListWalker([])
  86. self.list = urwid.ListBox(self.listwalker)
  87. leftbox = urwid.LineBox(self.list)
  88. columns = urwid.Columns([leftbox, rightbox], focus_column=0)
  89. self.ui = urwid.Frame(urwid.AttrWrap(columns, 'body'))
  90. self.sessions = set()
  91. self.nodes = set()
  92. self.refresh_needed = False
  93. def add_node(self, name, info, state):
  94. logging.debug(f"Adding node: {name} {info} {state}")
  95. node = Node(name, "node", state)
  96. node.set_txt()
  97. self.nodes.add(name)
  98. self.listwalker.append(node)
  99. self.add_sessions(name, info)
  100. def add_sessions(self, name, info):
  101. for session in ['outbound', 'inbound', 'manual', 'seed', 'direct']:
  102. if session in info and info[session]:
  103. session_widget = Session(name, session)
  104. session_widget.set_txt()
  105. self.listwalker.append(session_widget)
  106. self.add_slots(name, session, info[session])
  107. # If the direct session's peer discovery is active, show the
  108. # session even if there is currently no direct channel
  109. elif self.model.nodes[name] and self.model.nodes[name]['direct_peer_discovery'] is not None and session == 'direct':
  110. session_widget = Session(name, session)
  111. session_widget.set_txt()
  112. self.listwalker.append(session_widget)
  113. def add_slots(self, name, session, slots):
  114. for i, addr in slots.items():
  115. slot = Slot(name, f"{session}-slot")
  116. slot.set_txt(i, addr)
  117. self.listwalker.append(slot)
  118. match session:
  119. case "outbound":
  120. if addr[1] > 0:
  121. self.sessions.add(addr[1])
  122. case "inbound" | "manual " | "seed" | "direct":
  123. self.sessions.add(i)
  124. def add_lilith(self, name, info, state):
  125. logging.debug(f"Adding lilith: {name} {info} {state}")
  126. node = Node(name, "lilith-node", state)
  127. node.set_txt()
  128. self.nodes.add(name)
  129. self.listwalker.append(node)
  130. if state == NodeState.OFF:
  131. return
  132. else:
  133. for (i, key) in enumerate(info['spawns'].keys()):
  134. slot = Slot(name, "spawn-slot")
  135. slot.set_txt(i, key)
  136. self.listwalker.append(slot)
  137. def update_lilith(self, name, info):
  138. for index, widget in enumerate(self.listwalker):
  139. if isinstance(widget, Node) and widget.name == name:
  140. # Offline node has come online
  141. if widget.state == NodeState.OFF and info:
  142. self.refresh_needed = True
  143. # Online node has gone offline
  144. elif widget.state == NodeState.ON and not info:
  145. self.refresh_needed = True
  146. def update_node(self, name, info):
  147. for index, widget in enumerate(self.listwalker):
  148. if isinstance(widget, Node) and widget.name == name:
  149. # Offline node has come online
  150. if widget.state == NodeState.OFF and info:
  151. self.refresh_needed = True
  152. # Online node has gone offline
  153. elif widget.state == NodeState.ON and not info:
  154. self.refresh_needed = True
  155. else:
  156. widget.set_txt()
  157. return index + 1
  158. return None
  159. def update_slots(self, name, info):
  160. if info is None:
  161. return
  162. for session in ['outbound', 'inbound', 'manual', 'seed', 'direct']:
  163. if session in info and info[session]:
  164. for i, addr in info[session].items():
  165. self.update_slot(name, session, i, addr)
  166. def update_slot(self, name, session, i, addr):
  167. for index, widget in enumerate(self.listwalker):
  168. if isinstance(widget, Slot) and \
  169. widget.name == name and \
  170. widget.kind == f"{session}-slot" and \
  171. widget.i == i:
  172. key = (f"{widget.name}", f"{widget.i}")
  173. if key in self.model.nodes[widget.name]['event']:
  174. info = self.model.nodes[widget.name]['event'].get(key)
  175. widget.set_txt(i, info)
  176. self.listwalker[index] = widget
  177. break
  178. #-----------------------------------------------------------------
  179. # Render dnet.subscribe_events() RPC call
  180. # Right hand menu only
  181. #-----------------------------------------------------------------
  182. def update_right_box(self):
  183. self.pile.contents.clear()
  184. focus_w = self.list.get_focus()
  185. if focus_w[0] is None:
  186. return
  187. kind = focus_w[0].kind
  188. match kind:
  189. case "outbound" | "direct":
  190. key = (focus_w[0].name, kind)
  191. info = self.model.nodes.get(focus_w[0].name)
  192. if info and key in info['event']:
  193. ev = info['event'].get(key)
  194. self.pile.contents.append((
  195. urwid.Text(f" {ev}"),
  196. self.pile.options()))
  197. case "outbound-slot" | "inbound-slot" | \
  198. "manual-slot" | "seed-slot" | "direct-slot":
  199. addr = focus_w[0].addr
  200. name = focus_w[0].name
  201. info = self.model.nodes.get(name)
  202. if info and addr in info['msgs']:
  203. msg = info['msgs'].get(addr)
  204. for m in msg:
  205. time = m[0]
  206. event = m[1]
  207. msg = m[2]
  208. self.pile.contents.append((urwid.Text(
  209. f"{time}: {event}: {msg}"),
  210. self.pile.options()))
  211. case "spawn-slot":
  212. name = focus_w[0].name
  213. spawn_name = focus_w[0].id
  214. lilith = self.model.liliths.get(name)
  215. spawns = lilith.get('spawns')
  216. if spawns is None:
  217. return
  218. info = spawns.get(spawn_name)
  219. if info['urls']:
  220. urls = info['urls']
  221. self.pile.contents.append((urwid.Text(
  222. f"Accept addrs:"),
  223. self.pile.options()))
  224. for url in urls:
  225. self.pile.contents.append((urwid.Text(
  226. f" {url}"),
  227. self.pile.options()))
  228. if info['whitelist']:
  229. whitelist = info['whitelist']
  230. self.pile.contents.append((urwid.Text(
  231. f"Whitelist:"),
  232. self.pile.options()))
  233. for host in whitelist:
  234. self.pile.contents.append((urwid.Text(
  235. f" {host}"),
  236. self.pile.options()))
  237. if info['greylist']:
  238. greylist = info['greylist']
  239. self.pile.contents.append((urwid.Text(
  240. f"Greylist:"),
  241. self.pile.options()))
  242. for host in greylist:
  243. self.pile.contents.append((urwid.Text(
  244. f" {host}"),
  245. self.pile.options()))
  246. if info['goldlist']:
  247. goldlist = info['goldlist']
  248. self.pile.contents.append((urwid.Text(
  249. f"Goldlist:"),
  250. self.pile.options()))
  251. for host in goldlist:
  252. self.pile.contents.append((urwid.Text(
  253. f" {host}"),
  254. self.pile.options()))
  255. def update_node_state(self, info):
  256. if info:
  257. logging.debug(f"update_node_state(): Returning {NodeState.ON}")
  258. return NodeState.ON
  259. else:
  260. logging.debug(f"update_node_state(): Returning {NodeState.OFF}")
  261. return NodeState.OFF
  262. def refresh(self):
  263. logging.debug("Refresh initiated.")
  264. self.listwalker.clear()
  265. self.sessions.clear()
  266. self.nodes.clear()
  267. # Repopulate
  268. for name, info in self.model.nodes.items():
  269. logging.debug(f"refresh nodes(): (nodes) updating node state")
  270. state = self.update_node_state(info)
  271. logging.debug(f"refresh nodes(): {name}, {info}, {state}")
  272. self.add_node(name, info, state)
  273. for name, info in self.model.liliths.items():
  274. logging.debug(f"refresh nodes(): (lilith) updating node state")
  275. state = self.update_node_state(info)
  276. logging.debug(f"refresh liliths(): {name}, {info}, {state}")
  277. self.add_lilith(name, info, state)
  278. logging.debug("Refresh complete.")
  279. async def update_view(self, evloop: asyncio.AbstractEventLoop,
  280. loop: urwid.MainLoop):
  281. while True:
  282. await asyncio.sleep(0.1)
  283. if self.refresh_needed:
  284. self.refresh()
  285. self.refresh_needed = False
  286. else:
  287. for name, info in self.model.nodes.items():
  288. #logging.debug(f"update_view(): found {name}, {info}")
  289. # Check for new nodes or update existing slots.
  290. if name not in self.nodes:
  291. logging.debug(f"update_view(): (node) updating node state")
  292. state = self.update_node_state(info)
  293. self.add_node(name, info, state)
  294. else:
  295. start_index = self.update_node(name, info)
  296. if start_index is not None:
  297. self.update_slots(name, info)
  298. # Check for outbound, inbound, or direct connections coming
  299. # online or going offline, which requires a redraw.
  300. if 'outbound' in info:
  301. for i, (addr, id) in info['outbound'].items():
  302. if id > 0 and id not in self.sessions:
  303. logging.debug(f"Outbound {id}, {addr} came online.")
  304. self.refresh_needed = True
  305. break
  306. if 'inbound' in info:
  307. for key, addr in info['inbound'].items():
  308. if key in self.sessions and not addr:
  309. logging.debug(f"Inbound {key} went offline.")
  310. # Delete this key from the model.
  311. del(info['inbound'][f'{key}'])
  312. self.refresh_needed = True
  313. break
  314. if key not in self.sessions:
  315. logging.debug(f"Inbound {key}, {addr} came online.")
  316. self.refresh_needed = True
  317. break
  318. if 'direct' in info:
  319. for key, val in info['direct'].items():
  320. if key in self.sessions and not val:
  321. logging.debug(f"Direct outbound {key} went offline.")
  322. # Delete this key from the model.
  323. del(info['direct'][f'{key}'])
  324. self.refresh_needed = True
  325. break
  326. if key not in self.sessions:
  327. logging.debug(f"Direct outbound {key} came online.")
  328. self.refresh_needed = True
  329. break
  330. if self.model.nodes[name] and self.model.nodes[name]['direct_peer_discovery'] == 0:
  331. logging.debug(f"Direct peer discovery detected.")
  332. self.model.nodes[name]['direct_peer_discovery'] = 1
  333. self.refresh_needed = True
  334. break
  335. # Check for new lilith nodes.
  336. for name, info in self.model.liliths.items():
  337. if name not in self.nodes:
  338. logging.debug(f"update_view(): (lilith) updating node state")
  339. state = self.update_node_state(info)
  340. self.add_lilith(name, info, state)
  341. else:
  342. self.update_lilith(name, info)
  343. self.update_right_box()
  344. evloop.call_soon(loop.draw_screen)