tamagonion 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.3
2
+ Name: tamagonion
3
+ Version: 0.0.0
4
+ Summary: This is Stinky, he's running your Tor relay. He mostly loafs around and browses the Web anonymously, but he's more entertaining to look at than a graph!
5
+ Author: Samuel De Grace
6
+ Author-email: Samuel De Grace <48831884+daxAKAhackerman@users.noreply.github.com>
7
+ Requires-Dist: psutil==7.2.2
8
+ Requires-Dist: stem==1.8.2
9
+ Requires-Python: ==3.14.*
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Tamagonion
13
+
14
+ ## Instructions
15
+
16
+ This is Stinky, it's a Tamagonion and it's running your Tor relay. Tamagonions are cyber creatures that have bravely stepped out of the Tor network to learn what life is like in the clearnet and to help you monitor your relay. It mostly loafs around and browses the Web anonymously, but it's more entertaining to look at than a graph!
17
+
18
+ ```
19
+ ▌▌
20
+ ▓▓▀
21
+ ▄▄▄▀▀▀▀▄▄▄
22
+ █ ─ ─ █
23
+ █ ▄ ▄ █
24
+ █ ▀ ▀ █
25
+ █ ── █
26
+ █ █
27
+ █▄▄▄▄▄▄▄▄▄▄▄▄█
28
+ ```
29
+
30
+ ## Activation
31
+
32
+ To activate the viewport, use the `tamagonion` command as such:
33
+
34
+ ```bash
35
+ # By default, we will try to connect to the control port at 127.0.0.1:9051
36
+ $ uv run tamagonion
37
+
38
+ # If you use password authentication, you can supply it with -P or with the TAMAGONION_PASSWORD environment variable
39
+ $ uv run tamagonion -P my_password
40
+ $ TAMAGONION_PASSWORD=my_password uv run tamagonion
41
+
42
+ # If the port is listening somewhere else, you can specify it with -i and -p
43
+ $ uv run tamagonion -i 192.168.0.10 -p 9999
44
+
45
+ # If you are connecting using a Unix socket, you can point to it with -s
46
+ $ uv run tamagonion -s /var/run/tor/control
47
+ ```
48
+
49
+ ## Status check
50
+
51
+ ```
52
+ ┌──────────────────────────────────────────────┐
53
+ │Relay nickname: exitafpwrwmm │
54
+ │Uptime: 0d 05h 53m 06s │
55
+ │Connections (N/L/Co/F/Cl): 0,0,7,0,0 │
56
+ │Download (Cur/Avg/Tot): 338B / 116B / 2MB │
57
+ │Upload (Cur/Avg/Tot): 1KB / 152B / 3MB │
58
+ │Version: 0.4.9.11 (none recommended) │
59
+ │Process info (CPU/Mem): 0.0%/867MB │
60
+ │ ░░░░░░░░ │
61
+ │ ░╔══════╗░ █▄ │
62
+ │ ▌▌ z ░║ EXIT ║░ █████ │
63
+ │ ▓▓▀ Z ░╚══════╝░ █▀ │
64
+ │ ▄▄▄▀▀▀▀▄▄▄ Z ░░░░░░░░ │
65
+ │ █ ─ ─ ▄▄▄▄▄ │
66
+ │ █ █ █ █ │
67
+ │ █ ── ── █ ▀█▀ █ ╓───┬───╖╤═══╕ │
68
+ │ ____ █ ∙ █ █ █ ║---│---║╧╤══╧╤ │
69
+ ││oV2o│█ ▀▀▀▀▀ ║---│---║╒╧══╤╧ │
70
+ │╘════╛ █▄▄▄▄▄▄▄▄▄▄▄▄█ ╙───┴───╜╘═══╧ │
71
+ └───────────────────────────Hit ctrl+c to exit─┘
72
+ ```
73
+
74
+ The top of the viewport contains various information about your relay. More specifically:
75
+
76
+ - The relay nickname as defined in your Tor configuration
77
+ - The uptime since the Tor daemon has started
78
+ - The number of connections
79
+ - NEW: We have received a new incoming OR connection, and are starting the server-side handshake.
80
+ - LAUNCHED: We have launched a new outgoing OR connection, and are starting the client-side handshake.
81
+ - CONNECTED: The OR connection has been connected and the handshake is done.
82
+ - FAILED: Our attempt to open the OR connection failed.
83
+ - CLOSED: The OR connection closed in an unremarkable way.
84
+ - The current, average and total amount of data that was downloaded and uploaded
85
+ - The version of the Tor binary, as well as the recommendation from the consensus (if any)
86
+ - If you are connected locally (localhost or Unix socket), the CPU and memory usage of the Tor daemon
87
+
88
+ ## Biology
89
+
90
+ Tamagonions are very atuned to a relay's operation. You can know how your relay is going by looking at how Stinky is feeling or behaving.
91
+
92
+ | Behavior | Meaning |
93
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------- |
94
+ | Asleep | The relay is "dormant", meaning it has gone idle due to lack of use or some similar reason. |
95
+ | Sad | The relay doesn't have the both the "Valid" and the "Running" flags, or it has the "NoEdConsensus" flag. |
96
+ | Confused | The relay discarded expired statuses and server descriptors to fall below the desired threshold of directory information. |
97
+ | Embarassed | A directory authority rejected our descriptor. |
98
+ | Looking away | The relay's ORPort is unreachable. |
99
+ | Bored | The relay has the "MiddleOnly" flag. |
100
+ | Glitchy | The relay doesn't have the "Stable" flag. |
101
+ | Old | The relay has the "StaleDesc" flag. |
102
+ | Egg | The relay is bootstrapping. |
103
+ | Slow | The relay doesn't have the "Fast" flag. |
104
+
105
+ ## Room status check
106
+
107
+ Stinky's room can get a bit crowded with things it collected along the way. But every thing it collects means something special to it.
108
+
109
+ | Object or room state | Meaning |
110
+ | -------------------- | ---------------------------------------------- |
111
+ | Light's out | The relay believes the network is unreachable. |
112
+ | Exit sign | The relay has the "Exit" flag. |
113
+ | Broken exit sign | The relay has the "BadExit" flag. |
114
+ | Books | The relay has the "HSDir" flag. |
115
+ | Cassette | The relay has the "V2Dir" flag. |
116
+ | Shield | The relay has the "Guard" flag. |
117
+
118
+ ## You may also like...
119
+
120
+ - [Testing Tor Network](https://github.com/daxAKAhackerman/testing-tor-network) - CLI tool to setup a testing TOR network with Docker
121
+ - [Tor HashedControlPassword Brute](https://github.com/daxAKAhackerman/tor-hashed-control-password-brute) - C program to execute a dictionary attack against a Tor HashedControlPassword value
122
+
123
+ ---
124
+
125
+ > This repository contains no AI-generated code and was made with :heart: by [@daxAKAhackerman](https://github.com/daxAKAhackerman/)
@@ -0,0 +1,114 @@
1
+ # Tamagonion
2
+
3
+ ## Instructions
4
+
5
+ This is Stinky, it's a Tamagonion and it's running your Tor relay. Tamagonions are cyber creatures that have bravely stepped out of the Tor network to learn what life is like in the clearnet and to help you monitor your relay. It mostly loafs around and browses the Web anonymously, but it's more entertaining to look at than a graph!
6
+
7
+ ```
8
+ ▌▌
9
+ ▓▓▀
10
+ ▄▄▄▀▀▀▀▄▄▄
11
+ █ ─ ─ █
12
+ █ ▄ ▄ █
13
+ █ ▀ ▀ █
14
+ █ ── █
15
+ █ █
16
+ █▄▄▄▄▄▄▄▄▄▄▄▄█
17
+ ```
18
+
19
+ ## Activation
20
+
21
+ To activate the viewport, use the `tamagonion` command as such:
22
+
23
+ ```bash
24
+ # By default, we will try to connect to the control port at 127.0.0.1:9051
25
+ $ uv run tamagonion
26
+
27
+ # If you use password authentication, you can supply it with -P or with the TAMAGONION_PASSWORD environment variable
28
+ $ uv run tamagonion -P my_password
29
+ $ TAMAGONION_PASSWORD=my_password uv run tamagonion
30
+
31
+ # If the port is listening somewhere else, you can specify it with -i and -p
32
+ $ uv run tamagonion -i 192.168.0.10 -p 9999
33
+
34
+ # If you are connecting using a Unix socket, you can point to it with -s
35
+ $ uv run tamagonion -s /var/run/tor/control
36
+ ```
37
+
38
+ ## Status check
39
+
40
+ ```
41
+ ┌──────────────────────────────────────────────┐
42
+ │Relay nickname: exitafpwrwmm │
43
+ │Uptime: 0d 05h 53m 06s │
44
+ │Connections (N/L/Co/F/Cl): 0,0,7,0,0 │
45
+ │Download (Cur/Avg/Tot): 338B / 116B / 2MB │
46
+ │Upload (Cur/Avg/Tot): 1KB / 152B / 3MB │
47
+ │Version: 0.4.9.11 (none recommended) │
48
+ │Process info (CPU/Mem): 0.0%/867MB │
49
+ │ ░░░░░░░░ │
50
+ │ ░╔══════╗░ █▄ │
51
+ │ ▌▌ z ░║ EXIT ║░ █████ │
52
+ │ ▓▓▀ Z ░╚══════╝░ █▀ │
53
+ │ ▄▄▄▀▀▀▀▄▄▄ Z ░░░░░░░░ │
54
+ │ █ ─ ─ ▄▄▄▄▄ │
55
+ │ █ █ █ █ │
56
+ │ █ ── ── █ ▀█▀ █ ╓───┬───╖╤═══╕ │
57
+ │ ____ █ ∙ █ █ █ ║---│---║╧╤══╧╤ │
58
+ ││oV2o│█ ▀▀▀▀▀ ║---│---║╒╧══╤╧ │
59
+ │╘════╛ █▄▄▄▄▄▄▄▄▄▄▄▄█ ╙───┴───╜╘═══╧ │
60
+ └───────────────────────────Hit ctrl+c to exit─┘
61
+ ```
62
+
63
+ The top of the viewport contains various information about your relay. More specifically:
64
+
65
+ - The relay nickname as defined in your Tor configuration
66
+ - The uptime since the Tor daemon has started
67
+ - The number of connections
68
+ - NEW: We have received a new incoming OR connection, and are starting the server-side handshake.
69
+ - LAUNCHED: We have launched a new outgoing OR connection, and are starting the client-side handshake.
70
+ - CONNECTED: The OR connection has been connected and the handshake is done.
71
+ - FAILED: Our attempt to open the OR connection failed.
72
+ - CLOSED: The OR connection closed in an unremarkable way.
73
+ - The current, average and total amount of data that was downloaded and uploaded
74
+ - The version of the Tor binary, as well as the recommendation from the consensus (if any)
75
+ - If you are connected locally (localhost or Unix socket), the CPU and memory usage of the Tor daemon
76
+
77
+ ## Biology
78
+
79
+ Tamagonions are very atuned to a relay's operation. You can know how your relay is going by looking at how Stinky is feeling or behaving.
80
+
81
+ | Behavior | Meaning |
82
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------- |
83
+ | Asleep | The relay is "dormant", meaning it has gone idle due to lack of use or some similar reason. |
84
+ | Sad | The relay doesn't have the both the "Valid" and the "Running" flags, or it has the "NoEdConsensus" flag. |
85
+ | Confused | The relay discarded expired statuses and server descriptors to fall below the desired threshold of directory information. |
86
+ | Embarassed | A directory authority rejected our descriptor. |
87
+ | Looking away | The relay's ORPort is unreachable. |
88
+ | Bored | The relay has the "MiddleOnly" flag. |
89
+ | Glitchy | The relay doesn't have the "Stable" flag. |
90
+ | Old | The relay has the "StaleDesc" flag. |
91
+ | Egg | The relay is bootstrapping. |
92
+ | Slow | The relay doesn't have the "Fast" flag. |
93
+
94
+ ## Room status check
95
+
96
+ Stinky's room can get a bit crowded with things it collected along the way. But every thing it collects means something special to it.
97
+
98
+ | Object or room state | Meaning |
99
+ | -------------------- | ---------------------------------------------- |
100
+ | Light's out | The relay believes the network is unreachable. |
101
+ | Exit sign | The relay has the "Exit" flag. |
102
+ | Broken exit sign | The relay has the "BadExit" flag. |
103
+ | Books | The relay has the "HSDir" flag. |
104
+ | Cassette | The relay has the "V2Dir" flag. |
105
+ | Shield | The relay has the "Guard" flag. |
106
+
107
+ ## You may also like...
108
+
109
+ - [Testing Tor Network](https://github.com/daxAKAhackerman/testing-tor-network) - CLI tool to setup a testing TOR network with Docker
110
+ - [Tor HashedControlPassword Brute](https://github.com/daxAKAhackerman/tor-hashed-control-password-brute) - C program to execute a dictionary attack against a Tor HashedControlPassword value
111
+
112
+ ---
113
+
114
+ > This repository contains no AI-generated code and was made with :heart: by [@daxAKAhackerman](https://github.com/daxAKAhackerman/)
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "tamagonion"
3
+ version = "0.0.0"
4
+ description = "This is Stinky, he's running your Tor relay. He mostly loafs around and browses the Web anonymously, but he's more entertaining to look at than a graph!"
5
+ readme = "README.md"
6
+ requires-python = "==3.14.*"
7
+ dependencies = [
8
+ "psutil==7.2.2",
9
+ "stem==1.8.2",
10
+ ]
11
+
12
+ [[project.authors]]
13
+ name = "Samuel De Grace"
14
+ email = "48831884+daxAKAhackerman@users.noreply.github.com"
15
+
16
+ [project.scripts]
17
+ tamagonion = "tamagonion:main"
18
+
19
+ [build-system]
20
+ requires = ["uv_build==0.12.3"]
21
+ build-backend = "uv_build"
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "tamagonion"
3
+ version = "0.0.0"
4
+ description = "This is Stinky, he's running your Tor relay. He mostly loafs around and browses the Web anonymously, but he's more entertaining to look at than a graph!"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Samuel De Grace", email = "48831884+daxAKAhackerman@users.noreply.github.com" }
8
+ ]
9
+ requires-python = "==3.14.*"
10
+ dependencies = [
11
+ "psutil==7.2.2",
12
+ "stem==1.8.2",
13
+ ]
14
+
15
+ [project.scripts]
16
+ tamagonion = "tamagonion:main"
17
+
18
+ [build-system]
19
+ requires = ["uv_build==0.12.3"]
20
+ build-backend = "uv_build"
@@ -0,0 +1,48 @@
1
+ import argparse
2
+ import sys
3
+ from time import sleep
4
+
5
+ from tamagonion.app_data import AppData
6
+ from tamagonion.draw import Paper
7
+ from tamagonion.relay_manager import RelayManager
8
+
9
+
10
+ def main() -> None:
11
+ args = get_args()
12
+
13
+ print("\033[2J\033[H", end="") # Clear the screen and reset cursor
14
+ relay_manager = RelayManager(ip_addr=args.ip, port=args.port, password=args.password, socket_file=args.socket_file)
15
+ app_data = AppData(relay_manager)
16
+ paper = Paper(app_data)
17
+
18
+ try:
19
+ while True:
20
+ app_data.update()
21
+ paper.draw()
22
+ sleep(1)
23
+ except KeyboardInterrupt:
24
+ pass
25
+
26
+ paper.erase(include_frame=True)
27
+ sys.exit(0)
28
+
29
+
30
+ def get_args() -> argparse.Namespace:
31
+ parser = argparse.ArgumentParser(
32
+ prog="Tamagonion",
33
+ description="This is Stinky, he's running your Tor relay. He mostly loafs around and browses the Web anonymously, but he's more entertaining to look at than a graph!",
34
+ epilog="Hack the planet!",
35
+ )
36
+
37
+ parser.add_argument(
38
+ "-i", "--ip", help="IP of the Tor relay where the control protocol listens. Can also be a hostname. Defaults to 127.0.0.1.", default="127.0.0.1"
39
+ )
40
+ parser.add_argument("-p", "--port", help="Control port. Defaults to 9051.", default="9051")
41
+ parser.add_argument("-P", "--password", help="Control port password, if any", required=False)
42
+ parser.add_argument("-s", "--socket-file", help="Path to the socket file to use instead of an IP", required=False)
43
+
44
+ return parser.parse_args()
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main()
@@ -0,0 +1,152 @@
1
+ from collections import defaultdict
2
+ from enum import StrEnum
3
+ from typing import Any, Self
4
+
5
+ import psutil
6
+ from stem import DescriptorUnavailable
7
+ from stem.version import Version
8
+
9
+ from tamagonion.relay_manager import RelayManager
10
+
11
+ SECONDS_IN_MINUTE = 60
12
+ SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60
13
+ SECONDS_IN_DAY = SECONDS_IN_HOUR * 24
14
+
15
+
16
+ class Flags(StrEnum):
17
+ BAD_EXIT = "BadExit"
18
+ EXIT = "Exit"
19
+ FAST = "Fast"
20
+ GUARD = "Guard"
21
+ HS_DIR = "HSDir"
22
+ MIDDLE_ONLY = "MiddleOnly"
23
+ STABLE = "Stable"
24
+ STALE_DESC = "StaleDesc"
25
+ V2_DIR = "V2Dir"
26
+ NO_ED_CONSENSUS = "NoEdConsensus"
27
+ RUNNING = "Running"
28
+ VALID = "Valid"
29
+
30
+
31
+ class AppData:
32
+ relay_manager: RelayManager
33
+ flags: list[str]
34
+ version: Version
35
+ info: dict[str, Any]
36
+ process_info: dict[str, Any]
37
+ uptime: float = 0.0
38
+ connection_status_map: defaultdict[str, int]
39
+ pid: int = 0
40
+ version_status: str = ""
41
+ relay_name: str = ""
42
+ frame: int = 0
43
+ frame_skip: bool = False
44
+ instance: Self | None = None
45
+
46
+ def __init(self, relay_manager: RelayManager) -> None:
47
+ self.process_info = {}
48
+ self.info = {}
49
+ self.version = Version("0.0.0.0")
50
+ self.flags = []
51
+ self.connection_status_map = defaultdict(lambda: 0)
52
+
53
+ self.relay_manager = relay_manager
54
+ self._get_version()
55
+ self._get_version_status()
56
+ self._get_pid()
57
+ self._get_relay_name()
58
+
59
+ def __new__(cls, *args, **kwargs) -> Self:
60
+ if cls.instance is None:
61
+ cls.instance = super().__new__(cls)
62
+ cls.instance.__init(*args, **kwargs)
63
+ return cls.instance
64
+
65
+ def update(self) -> None:
66
+ self._get_flags()
67
+ self._get_uptime()
68
+ self._get_info()
69
+ self._get_orconn_status()
70
+
71
+ if self.relay_manager.is_local:
72
+ self._get_process_info()
73
+
74
+ def _get_flags(self) -> None:
75
+ try:
76
+ self.flags = self.relay_manager.controller.get_network_status().flags
77
+ except DescriptorUnavailable:
78
+ self.flags = []
79
+
80
+ def _get_uptime(self) -> None:
81
+ self.uptime = self.relay_manager.controller.get_uptime()
82
+
83
+ @property
84
+ def formated_uptime(self) -> str:
85
+ uptime_as_int = int(self.uptime)
86
+ days = uptime_as_int // SECONDS_IN_DAY
87
+ hours = (uptime_as_int % SECONDS_IN_DAY) // SECONDS_IN_HOUR
88
+ minutes = (uptime_as_int % SECONDS_IN_HOUR) // SECONDS_IN_MINUTE
89
+ seconds = uptime_as_int % SECONDS_IN_MINUTE
90
+
91
+ return f"{days}d {hours:02}h {minutes:02}m {seconds:02}s"
92
+
93
+ def _get_pid(self) -> None:
94
+ self.pid = self.relay_manager.controller.get_pid()
95
+
96
+ def _get_version(self) -> None:
97
+ self.version = self.relay_manager.controller.get_version()
98
+
99
+ def _get_version_status(self) -> None:
100
+ self.version_status = self.relay_manager.controller.get_info("status/version/current")
101
+
102
+ def _get_relay_name(self) -> None:
103
+ nickname = self.relay_manager.controller.get_conf("Nickname")
104
+ self.relay_name = nickname or "Unknown"
105
+
106
+ def _get_info(self) -> None:
107
+ bw_event_cache = self.relay_manager.controller.get_info("bw-event-cache")
108
+ bw_event_latest = bw_event_cache.split(" ")[-1].split(",")
109
+ traffic_read = self.relay_manager.controller.get_info("traffic/read")
110
+ traffic_written = self.relay_manager.controller.get_info("traffic/written")
111
+
112
+ self.info = {
113
+ "dormant": self.relay_manager.controller.get_info("dormant") != "0",
114
+ "traffic_read": int(traffic_read),
115
+ "traffic_written": int(traffic_written),
116
+ "bw_event_cache_down": int(bw_event_latest[0]),
117
+ "bw_event_cache_up": int(bw_event_latest[1]),
118
+ "bw_avg_down": int(traffic_read) / self.uptime,
119
+ "bw_avg_up": int(traffic_written) / self.uptime,
120
+ "network_liveness": self.relay_manager.controller.get_info("network-liveness") == "up",
121
+ "bootstrap_percent": int(self.relay_manager.controller.get_info("status/bootstrap-phase").split(" ")[2].split("=")[-1]),
122
+ "has_enough_dir_info": self.relay_manager.controller.get_info("status/enough-dir-info") == "1",
123
+ "good_server_descriptor": self.relay_manager.controller.get_info("status/good-server-descriptor") == "1",
124
+ "reachability": self.relay_manager.controller.get_info("status/reachability-succeeded/or") == "1",
125
+ }
126
+
127
+ def _get_orconn_status(self) -> None:
128
+ self.connection_status_map = defaultdict(lambda: 0)
129
+ orconn_status = self.relay_manager.controller.get_info("orconn-status")
130
+
131
+ for status in orconn_status.split("\n"):
132
+ _node, state = status.split(" ")
133
+ self.connection_status_map[state] += 1
134
+
135
+ def _get_process_info(self) -> None:
136
+ process = psutil.Process(self.pid)
137
+
138
+ self.process_info = {
139
+ "cpu": process.cpu_percent(),
140
+ "memory": process.memory_info().rss,
141
+ }
142
+
143
+ @staticmethod
144
+ def format_bytes(b: int) -> str:
145
+ if b < 10**3:
146
+ return f"{int(b)}B"
147
+ elif b < 10**6:
148
+ return f"{int(b / (10**3))}KB"
149
+ elif b < 10**9:
150
+ return f"{int(b / (10**6))}MB"
151
+ else:
152
+ return f"{int(b / (10**9))}GB"
@@ -0,0 +1,276 @@
1
+ frame = """┌──────────────────────────────────────────────┐
2
+ │ │
3
+ │ │
4
+ │ │
5
+ │ │
6
+ │ │
7
+ │ │
8
+ │ │
9
+ │ │
10
+ │ │
11
+ │ │
12
+ │ │
13
+ │ │
14
+ │ │
15
+ │ │
16
+ │ │
17
+ │ │
18
+ │ │
19
+ │ │
20
+ └───────────────────────────Hit ctrl+c to exit─┘"""
21
+
22
+ stinky_base = [
23
+ """│ ▌▌
24
+ │ ▓▓▀
25
+ │ ▄▄▄▀▀▀▀▄▄▄
26
+ │ █ ─ ─ █
27
+ │ █ ▄ ▄ █
28
+ │ █ ▀ ▀ █
29
+ │ █ ── █
30
+ │ █ █
31
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█ """,
32
+ """│
33
+ │ ▌▌
34
+ │ ▓▓▀
35
+ │ ▄▄▄▀▀▀▀▄▄▄
36
+ │ █ ─ ─ █
37
+ │ █ ▄ ▄ █
38
+ │ █ ▀ ▀ █
39
+ │ █ ── █
40
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄▄▄█""",
41
+ ]
42
+
43
+ stinky_asleep = [
44
+ """│ ▌▌ z
45
+ │ ▓▓▀ Z
46
+ │ ▄▄▄▀▀▀▀▄▄▄ Z
47
+ │ █ ─ ─ █
48
+ │ █ █
49
+ │ █ ── ── █
50
+ │ █ ∙ █
51
+ │ █ █
52
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█""",
53
+ """│
54
+ │ ▌▌ z
55
+ │ ▓▓▀ Z
56
+ │ ▄▄▄▀▀▀▀▄▄▄ Z
57
+ │ █ ─ ─ █
58
+ │ █ █
59
+ │ █ ── ── █
60
+ │ █ ∙ █
61
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄▄▄█""",
62
+ ]
63
+
64
+ stinky_blackout = [
65
+ """│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
66
+ │░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░
67
+ │░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
68
+ │░▒▓████████████████████████████████████████▓▒░
69
+ │░▒▓████████████████████████████████████████▓▒░
70
+ │░▒▓█████████▀██▀███████████████████████████▓▒░
71
+ │░▒▓█████████▄██▄███████████████████████████▓▒░
72
+ │░▒▓████████████████████████████████████████▓▒░
73
+ │░▒▓████████████████████████████████████████▓▒░
74
+ │░▒▓████████████████████████████████████████▓▒░""",
75
+ """│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
76
+ │░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░
77
+ │░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
78
+ │░▒▓████████████████████████████████████████▓▒░
79
+ │░▒▓████████████████████████████████████████▓▒░
80
+ │░▒▓████████████████████████████████████████▓▒░
81
+ │░▒▓█████████▀██▀███████████████████████████▓▒░
82
+ │░▒▓█████████▄██▄███████████████████████████▓▒░
83
+ │░▒▓████████████████████████████████████████▓▒░
84
+ │░▒▓████████████████████████████████████████▓▒░""",
85
+ ]
86
+
87
+ stinky_sad = [
88
+ """│ ▌▌
89
+ │ ▓▓▀
90
+ │ ▄▄▄▀▀▀▀▄▄▄
91
+ │ █ / \\ █
92
+ │ █ ▄ ▄ █
93
+ │ █ °▀ ▀° █
94
+ │ █ ° ── █
95
+ │ █ █
96
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█ """,
97
+ """│
98
+ │ ▌▌
99
+ │ ▓▓▀
100
+ │ ▄▄▄▀▀▀▀▄▄▄
101
+ │ █ / \\ █
102
+ │ █ ▄ ▄ █
103
+ │ █ °▀ ▀° █
104
+ │ █ ° ── █
105
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄▄▄█""",
106
+ ]
107
+
108
+ stinky_confused = [
109
+ """│ ▌▌ ?
110
+ │ ▓▓▀ ?
111
+ │ ▄▄▄▀▀▀▀▄▄▄ ?
112
+ │ █ ─ ^ █
113
+ │ █ ▄ ▄ █
114
+ │ █ ▀ ▀ █
115
+ │ █ ─── █
116
+ │ █ █
117
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█ """,
118
+ """│
119
+ │ ▌▌ ?
120
+ │ ▓▓▀ ?
121
+ │ ▄▄▄▀▀▀▀▄▄▄ ?
122
+ │ █ ─ ^ █
123
+ │ █ ▄ ▄ █
124
+ │ █ ▀ ▀ █
125
+ │ █ ─── █
126
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄▄▄█ """,
127
+ ]
128
+
129
+ stinky_embarassed = [
130
+ """│ ▌▌
131
+ │ ▓▓▀
132
+ │ ▄▄▄▀▀▀▀▄▄▄
133
+ │ █ / \\ █
134
+ │ █ ° █
135
+ │ █ ^ ^ █
136
+ │ █ O ── O █
137
+ │ █ █
138
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█ """,
139
+ """│
140
+ │ ▌▌
141
+ │ ▓▓▀
142
+ │ ▄▄▄▀▀▀▀▄▄▄
143
+ │ █ / \\ █
144
+ │ █ ° █
145
+ │ █ ^ ^ █
146
+ │ █ O ── O █
147
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄▄▄█""",
148
+ ]
149
+
150
+ stinky_turned_around = [
151
+ """│ ▐▐
152
+ │ ▀▓▓
153
+ │ ▄▄▄▀▀▀▀▄▄▄
154
+ │ █ █
155
+ │ █ █
156
+ │ █ █
157
+ │ █ █
158
+ │ █ (_)(_) █
159
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█ """,
160
+ """│
161
+ │ ▐▐
162
+ │ ▀▓▓
163
+ │ ▄▄▄▀▀▀▀▄▄▄
164
+ │ █ █
165
+ │ █ █
166
+ │ █ █
167
+ │ █ █
168
+ │ █▄▄▄▄(_)(_)▄▄▄▄█""",
169
+ ]
170
+ stinky_bored = [
171
+ """│ ▌▌
172
+ │ ▓▓▀
173
+ │ ▄▄▄▀▀▀▀▄▄▄
174
+ │ █ ^ ^ █
175
+ │ █ ── ── █
176
+ │ █ __ █
177
+ │ █ (__) █
178
+ │ █ █
179
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█ """,
180
+ """│
181
+ │ ▌▌
182
+ │ ▓▓▀
183
+ │ ▄▄▄▀▀▀▀▄▄▄
184
+ │ █ ^ ^ █
185
+ │ █ ── ── █
186
+ │ █ __ █
187
+ │ █ (__) █
188
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄▄▄█""",
189
+ ]
190
+
191
+ stinky_unstable = [
192
+ """│ ▌▌
193
+ │ ▓▓▀
194
+ │ ░░░▄▄▄▀▀▀▀▄▄▄
195
+ │ █ ─ ────── ──
196
+ │ ───█─ ─▄ ─▄ █
197
+ │ ▓▓▓▓ ▀ ▀─ ─ █
198
+ │ ── ── ▒▒▒▒
199
+ │ █ ─ ─ ─ ─█ ─ ──
200
+ │ ░░░░░░▄▄▄▄▄▄▄▄█ """,
201
+ """│
202
+ │ ▌▌
203
+ │ ▓▓▀
204
+ │ ▄░░░▀▀▀▄▄▄
205
+ │ █ ── ── █
206
+ │ █ ░──▄ ▄ ───█
207
+ │ ░░░ ▀ ▀ ▓▓
208
+ │ ── ── ─ ─── █
209
+ │ █▄▄▄▄▄▄▄▄▒▒▒▄▒▄▒▒""",
210
+ ]
211
+
212
+ stinky_old = [
213
+ """│ ││
214
+ │ ▓▓-
215
+ │ ▄▄▄▀▀▀▀▄▄▄
216
+ │ █ ─ ─ █
217
+ │ █ █
218
+ │ █ =▀ ▀= █
219
+ │ █ ( ── ) █
220
+ │ █ █
221
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄█ """,
222
+ """│
223
+ │ ││
224
+ │ ▓▓-
225
+ │ ▄▄▄▀▀▀▀▄▄▄
226
+ │ █ ─ ─ █
227
+ │ █ █
228
+ │ █ =▀ ▀= █
229
+ │ █ ( ── ) █
230
+ │ █▄▄▄▄▄▄▄▄▄▄▄▄▄▄█""",
231
+ ]
232
+
233
+ stinky_egg = """│ ▄▀▀▄
234
+ │ ▄█ ▀▄
235
+ │ ▄▀ █ ███▄
236
+ │ ▄▀ █ █ ████▄
237
+ │ █ ▀ █ ███▀█
238
+ │ ██ ▀ ▀ ██
239
+ │ █████▄ ▄██▄ ██
240
+ │ ██████ ▀██▀ ██
241
+ │ ▀███▄▄▄▄▄▄▄▄▄▄█▀ """
242
+
243
+
244
+ exit_sign = """ ░░░░░░░░
245
+ ░╔══════╗░ █▄
246
+ ░║ EXIT ║░ █████
247
+ ░╚══════╝░ █▀
248
+ ░░░░░░░░ """
249
+
250
+
251
+ broken_exit_sign = """ ══════╗ ▓▄
252
+ ║ EX_T ║ ░▒▓██
253
+ ╚════ ═╝ ▓▀ """
254
+
255
+ book = """╓───┬───╖╤═══╕
256
+ ║---│---║╧╤══╧╤
257
+ ║---│---║╒╧══╤╧
258
+ ╙───┴───╜╘═══╧ """
259
+
260
+ cassette = """ ____
261
+ │oV2o│
262
+ ╘════╛"""
263
+
264
+ shield = [
265
+ """ ▄▄▄▄▄
266
+ █ █ █
267
+ █ ▀█▀ █
268
+ █ █ █
269
+ ▀▀▀▀▀ """,
270
+ """
271
+ ▄▄▄▄▄
272
+ █ █ █
273
+ █ ▀█▀ █
274
+ █ █ █
275
+ ▀▀▀▀▀ """,
276
+ ]
@@ -0,0 +1,140 @@
1
+ from typing import Self
2
+
3
+ from tamagonion import art
4
+ from tamagonion.app_data import AppData, Flags
5
+
6
+
7
+ class Pen:
8
+ @staticmethod
9
+ def draw(image: str, pos_y: int, pos_x: int, transparent: bool = False) -> None:
10
+ if transparent:
11
+ image.replace(" ", "\033C")
12
+ move_x = f"\033[{pos_x}C" if pos_x > 0 else ""
13
+ move_y = f"\033[{pos_y}B" if pos_y > 0 else ""
14
+
15
+ print("\033[s", end="")
16
+ print(f"{move_x}{move_y}", end="")
17
+ for line in image.split("\n"):
18
+ print(line, end="")
19
+ print(f"\r{move_x}\033[B", end="")
20
+ print("\033[u", end="")
21
+
22
+
23
+ class Paper:
24
+ app_data: AppData
25
+ instance: Self | None = None
26
+
27
+ def __init(self, app_data: AppData) -> None:
28
+ self.app_data = app_data
29
+
30
+ def __new__(cls, *args, **kwargs) -> Self:
31
+ if cls.instance is None:
32
+ cls.instance = super().__new__(cls)
33
+ cls.instance.__init(*args, **kwargs)
34
+ return cls.instance
35
+
36
+ def draw(self) -> None:
37
+ self.erase()
38
+
39
+ ## Draw the frame
40
+ Pen.draw(art.frame, 0, 0)
41
+
42
+ ## Draw Stinky
43
+
44
+ # Bootstrap statuses
45
+ if self.app_data.info["bootstrap_percent"] < 100:
46
+ Pen.draw(art.stinky_egg, 10, 0)
47
+
48
+ # Network statuses
49
+ elif not self.app_data.info["network_liveness"]:
50
+ Pen.draw(art.stinky_blackout[self.app_data.frame], 9, 0)
51
+ elif not self.app_data.info["has_enough_dir_info"]:
52
+ Pen.draw(art.stinky_confused[self.app_data.frame], 10, 0)
53
+
54
+ # Relay network statuses
55
+ elif not self.app_data.info["reachability"]:
56
+ Pen.draw(art.stinky_turned_around[self.app_data.frame], 10, 0)
57
+ elif not self.app_data.info["good_server_descriptor"]:
58
+ Pen.draw(art.stinky_embarassed[self.app_data.frame], 10, 0)
59
+
60
+ # Relay statuses
61
+ elif Flags.NO_ED_CONSENSUS in self.app_data.flags or {Flags.VALID, Flags.RUNNING} - set(self.app_data.flags):
62
+ Pen.draw(art.stinky_sad[self.app_data.frame], 10, 0)
63
+ elif Flags.STALE_DESC in self.app_data.flags:
64
+ Pen.draw(art.stinky_old[self.app_data.frame], 10, 0)
65
+ elif Flags.STABLE not in self.app_data.flags:
66
+ Pen.draw(art.stinky_unstable[self.app_data.frame], 10, 0)
67
+ elif Flags.MIDDLE_ONLY in self.app_data.flags:
68
+ Pen.draw(art.stinky_bored[self.app_data.frame], 10, 0)
69
+ elif self.app_data.info["dormant"]:
70
+ Pen.draw(art.stinky_asleep[self.app_data.frame], 10, 0)
71
+ else:
72
+ Pen.draw(art.stinky_base[self.app_data.frame], 10, 0)
73
+
74
+ ## Draw the exit sign
75
+
76
+ if Flags.BAD_EXIT in self.app_data.flags:
77
+ Pen.draw(art.broken_exit_sign, 9, 31)
78
+ elif Flags.EXIT in self.app_data.flags:
79
+ Pen.draw(art.exit_sign, 8, 30)
80
+
81
+ ## Draw the books
82
+
83
+ if Flags.HS_DIR in self.app_data.flags:
84
+ Pen.draw(art.book, 15, 30)
85
+
86
+ ## Draw the cassette
87
+
88
+ if Flags.V2_DIR in self.app_data.flags:
89
+ Pen.draw(art.cassette, 16, 1)
90
+
91
+ ## Draw the shield
92
+
93
+ if Flags.GUARD in self.app_data.flags and self.app_data.info["reachability"]:
94
+ Pen.draw(art.shield[self.app_data.frame], 13, 19)
95
+
96
+ ## Slow down the animation
97
+
98
+ if Flags.FAST not in self.app_data.flags and self.app_data.frame_skip:
99
+ self.app_data.frame_skip = False
100
+ else:
101
+ self.app_data.frame ^= 1
102
+ self.app_data.frame_skip = True
103
+
104
+ ## Draw the stats
105
+
106
+ Pen.draw(f"Relay nickname: {self.app_data.relay_name}", 1, 1)
107
+ Pen.draw(f"Uptime: {self.app_data.formated_uptime}", 2, 1)
108
+ Pen.draw(
109
+ f"Connections (N/L/Co/F/Cl): {self.app_data.connection_status_map['NEW']}/{self.app_data.connection_status_map['LAUNCHED']}/{self.app_data.connection_status_map['CONNECTED']}/{self.app_data.connection_status_map['FAILED']}/{self.app_data.connection_status_map['CLOSED']}",
110
+ 3,
111
+ 1,
112
+ )
113
+ Pen.draw(
114
+ f"Download (Cur/Avg/Tot): {AppData.format_bytes(self.app_data.info['bw_event_cache_down'])} / {AppData.format_bytes(self.app_data.info['bw_avg_down'])} / {AppData.format_bytes(self.app_data.info['traffic_read'])}",
115
+ 4,
116
+ 1,
117
+ )
118
+ Pen.draw(
119
+ f"Upload (Cur/Avg/Tot): {AppData.format_bytes(self.app_data.info['bw_event_cache_up'])} / {AppData.format_bytes(self.app_data.info['bw_avg_up'])} / {AppData.format_bytes(self.app_data.info['traffic_written'])}",
120
+ 5,
121
+ 1,
122
+ )
123
+ Pen.draw(f"Version: {self.app_data.version} ({self.app_data.version_status})", 6, 1)
124
+
125
+ if self.app_data.relay_manager.is_local:
126
+ Pen.draw(
127
+ f"Process info (CPU/Mem): {self.app_data.process_info['cpu']}% / {AppData.format_bytes(self.app_data.process_info['memory'])}",
128
+ 7,
129
+ 1,
130
+ )
131
+
132
+ def erase(self, include_frame: bool = False) -> None:
133
+ if include_frame:
134
+ print("\033[0J", end="")
135
+ else:
136
+ split_frame = art.frame.split("\n")
137
+ frame_height = len(split_frame)
138
+ frame_witdh = len(split_frame[0])
139
+ for i in range(1, frame_height - 1):
140
+ Pen.draw(" " * (frame_witdh - 2), i, 1)
@@ -0,0 +1,33 @@
1
+ import os
2
+ from typing import Self
3
+
4
+ from stem.control import Controller, EventType
5
+
6
+
7
+ class RelayManager:
8
+ controller: Controller
9
+ is_local: bool = False
10
+ instance: Self | None = None
11
+
12
+ def __init(self, ip_addr: str, port: str, password: str | None, socket_file: str | None) -> None:
13
+ if socket_file:
14
+ self.controller = Controller.from_socket_file(socket_file)
15
+ else:
16
+ self.controller = Controller.from_port(address=ip_addr, port=int(port)) # type:ignore
17
+ if password:
18
+ self.controller.authenticate("password")
19
+ elif env_password := os.getenv("TAMAGONION_PASSWORD"):
20
+ self.controller.authenticate(env_password)
21
+ else:
22
+ self.controller.authenticate()
23
+
24
+ if socket_file or self.controller.is_localhost():
25
+ self.is_local = True
26
+
27
+ self.controller.add_event_listener(lambda: True, EventType["BW"])
28
+
29
+ def __new__(cls, *args, **kwargs) -> Self:
30
+ if cls.instance is None:
31
+ cls.instance = super().__new__(cls)
32
+ cls.instance.__init(*args, **kwargs)
33
+ return cls.instance