ChaTerminal 2.0.0__tar.gz → 2.0.1__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.
Files changed (38) hide show
  1. {chaterminal-2.0.0 → chaterminal-2.0.1}/ChaTerminal.egg-info/PKG-INFO +1 -1
  2. chaterminal-2.0.1/ChaTerminal.egg-info/SOURCES.txt +33 -0
  3. chaterminal-2.0.1/ChaTerminal.egg-info/top_level.txt +1 -0
  4. {chaterminal-2.0.0 → chaterminal-2.0.1}/PKG-INFO +1 -1
  5. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/core/events.py +8 -0
  6. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/core/state.py +2 -0
  7. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/main.py +1 -0
  8. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/services/auth_service.py +48 -4
  9. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/services/presence_service.py +56 -1
  10. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/services/websocket_service.py +5 -1
  11. chaterminal-2.0.1/chaterminal/ui/console.py +26 -0
  12. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/ui/terminal_ui.py +79 -41
  13. {chaterminal-2.0.0 → chaterminal-2.0.1}/setup.py +1 -1
  14. chaterminal-2.0.0/ChaTerminal/ui/console.py +0 -28
  15. chaterminal-2.0.0/ChaTerminal.egg-info/SOURCES.txt +0 -33
  16. chaterminal-2.0.0/ChaTerminal.egg-info/top_level.txt +0 -1
  17. {chaterminal-2.0.0 → chaterminal-2.0.1}/ChaTerminal.egg-info/dependency_links.txt +0 -0
  18. {chaterminal-2.0.0 → chaterminal-2.0.1}/ChaTerminal.egg-info/entry_points.txt +0 -0
  19. {chaterminal-2.0.0 → chaterminal-2.0.1}/ChaTerminal.egg-info/requires.txt +0 -0
  20. {chaterminal-2.0.0 → chaterminal-2.0.1}/LICENSE +0 -0
  21. {chaterminal-2.0.0 → chaterminal-2.0.1}/README.md +0 -0
  22. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/__init__.py +0 -0
  23. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/__main__.py +0 -0
  24. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/core/__init__.py +0 -0
  25. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/core/logger.py +0 -0
  26. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/core/threads.py +0 -0
  27. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/crypto/__init__.py +0 -0
  28. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/crypto/encryption.py +0 -0
  29. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/services/__init__.py +0 -0
  30. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/services/firebase_service.py +0 -0
  31. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/services/message_service.py +0 -0
  32. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/storage/__init__.py +0 -0
  33. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/storage/database.py +0 -0
  34. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/storage/session_store.py +0 -0
  35. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/ui/__init__.py +0 -0
  36. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/ui/panels.py +0 -0
  37. {chaterminal-2.0.0/ChaTerminal → chaterminal-2.0.1/chaterminal}/ui/splash.py +0 -0
  38. {chaterminal-2.0.0 → chaterminal-2.0.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ChaTerminal
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: A terminal-based encrypted chat system for MemerDevs
5
5
  Home-page: https://github.com/Gofaone315/ChaTerminal
6
6
  Author: Gofaone Tlalang
@@ -0,0 +1,33 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ ChaTerminal.egg-info/PKG-INFO
5
+ ChaTerminal.egg-info/SOURCES.txt
6
+ ChaTerminal.egg-info/dependency_links.txt
7
+ ChaTerminal.egg-info/entry_points.txt
8
+ ChaTerminal.egg-info/requires.txt
9
+ ChaTerminal.egg-info/top_level.txt
10
+ chaterminal/__init__.py
11
+ chaterminal/__main__.py
12
+ chaterminal/main.py
13
+ chaterminal/core/__init__.py
14
+ chaterminal/core/events.py
15
+ chaterminal/core/logger.py
16
+ chaterminal/core/state.py
17
+ chaterminal/core/threads.py
18
+ chaterminal/crypto/__init__.py
19
+ chaterminal/crypto/encryption.py
20
+ chaterminal/services/__init__.py
21
+ chaterminal/services/auth_service.py
22
+ chaterminal/services/firebase_service.py
23
+ chaterminal/services/message_service.py
24
+ chaterminal/services/presence_service.py
25
+ chaterminal/services/websocket_service.py
26
+ chaterminal/storage/__init__.py
27
+ chaterminal/storage/database.py
28
+ chaterminal/storage/session_store.py
29
+ chaterminal/ui/__init__.py
30
+ chaterminal/ui/console.py
31
+ chaterminal/ui/panels.py
32
+ chaterminal/ui/splash.py
33
+ chaterminal/ui/terminal_ui.py
@@ -0,0 +1 @@
1
+ chaterminal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ChaTerminal
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: A terminal-based encrypted chat system for MemerDevs
5
5
  Home-page: https://github.com/Gofaone315/ChaTerminal
6
6
  Author: Gofaone Tlalang
@@ -12,3 +12,11 @@ class EventType(Enum):
12
12
  def put_event(state, event_type: EventType, **kwargs):
13
13
  kwargs["type"] = event_type
14
14
  state.event_queue.put(kwargs)
15
+
16
+ if event_type == EventType.LOGOUT:
17
+ wake_prompt = getattr(state, "wake_prompt", None)
18
+ if callable(wake_prompt):
19
+ try:
20
+ wake_prompt()
21
+ except Exception:
22
+ pass
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
+ from collections.abc import Callable
2
3
  from queue import Queue
3
4
  import threading
4
5
 
@@ -17,3 +18,4 @@ class AppState:
17
18
 
18
19
  event_queue: Queue = field(default_factory=Queue)
19
20
  lock: threading.Lock = field(default_factory=threading.Lock)
21
+ wake_prompt: Callable[[], None] | None = None
@@ -65,6 +65,7 @@ def main():
65
65
  worker_manager = WorkerManager(state)
66
66
  worker_manager.start("WebsocketClient", ws_service.run)
67
67
  worker_manager.start("Heartbeat", presence.heartbeat_loop)
68
+ worker_manager.start("SessionGuard", presence.session_guard_loop)
68
69
  worker_manager.start("MessageSender", message_service.sender_worker)
69
70
 
70
71
  # 7. Run UI - single-threaded, blocks until /exit or Ctrl+C
@@ -14,17 +14,21 @@ class AuthService:
14
14
  session = load_session()
15
15
 
16
16
  if session:
17
- self.state.token = session.get("idToken")
18
- self.state.refresh_token = session.get("refreshToken")
19
- self.state.uid = session.get("localId")
20
- self.state.token_expires_at = session.get("expiresAt", 0)
17
+ self._apply_session(session)
21
18
 
22
19
  if self.state.token_expires_at <= int(time.time()):
23
20
  logger.info("[*] Refreshing session...")
24
21
  if not self.firebase.refresh_token():
25
22
  delete_session()
23
+ self._clear_session()
26
24
  session = None
27
25
 
26
+ if session and not self._stored_session_is_active():
27
+ logger.info("[*] Stored ChaTerminal session was revoked. Relinking required.")
28
+ delete_session()
29
+ self._clear_session()
30
+ session = None
31
+
28
32
  if not session:
29
33
  code = self._generate_activation_code()
30
34
  if not code:
@@ -72,6 +76,46 @@ class AuthService:
72
76
 
73
77
  return True
74
78
 
79
+ def _apply_session(self, session):
80
+ self.state.token = session.get("idToken")
81
+ self.state.refresh_token = session.get("refreshToken")
82
+ self.state.uid = session.get("localId")
83
+ self.state.token_expires_at = session.get("expiresAt", 0)
84
+
85
+ def _clear_session(self):
86
+ self.state.token = None
87
+ self.state.refresh_token = None
88
+ self.state.uid = None
89
+ self.state.token_expires_at = 0
90
+
91
+ def _stored_session_is_active(self):
92
+ try:
93
+ res = self.firebase.get(f"chaterminal/sessions/{self.state.uid}")
94
+ except Exception as e:
95
+ logger.error(f"[!] Failed to validate stored session: {e}")
96
+ return True
97
+
98
+ if res.status_code in (401, 403):
99
+ return False
100
+
101
+ if res.status_code != 200:
102
+ logger.error(f"[!] Failed to validate stored session: {res.status_code} {res.text}")
103
+ return True
104
+
105
+ if res.text.strip() == "null":
106
+ return False
107
+
108
+ try:
109
+ data = res.json()
110
+ except Exception:
111
+ return False
112
+
113
+ if not isinstance(data, dict):
114
+ return False
115
+
116
+ active_device_id = data.get("activeDeviceId")
117
+ return bool(active_device_id and active_device_id == self.state.device_id)
118
+
75
119
  def _generate_activation_code(self):
76
120
  alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
77
121
  attempts = 0
@@ -1,7 +1,9 @@
1
1
  import time
2
2
  from chaterminal.core.logger import logger
3
3
  from chaterminal.core.events import EventType, put_event
4
- from chaterminal.storage.session_store import get_device_name
4
+ from chaterminal.storage.session_store import delete_session, get_device_name
5
+
6
+ SESSION_GUARD_INTERVAL = 5
5
7
 
6
8
  class PresenceService:
7
9
  def __init__(self, state, firebase_client, ws_service):
@@ -54,6 +56,59 @@ class PresenceService:
54
56
 
55
57
  time.sleep(60)
56
58
 
59
+ def session_guard_loop(self):
60
+ while self.state.running:
61
+ reason = self._remote_session_logout_reason()
62
+ if reason:
63
+ self._handle_remote_logout(reason)
64
+ return
65
+
66
+ time.sleep(SESSION_GUARD_INTERVAL)
67
+
68
+ def _remote_session_logout_reason(self):
69
+ if not self.state.uid or not self.state.device_id or not self.state.token:
70
+ return None
71
+
72
+ try:
73
+ res = self.firebase.get(f"chaterminal/sessions/{self.state.uid}")
74
+ except Exception:
75
+ return None
76
+
77
+ if res.status_code in (401, 403):
78
+ return "Your ChaTerminal session is no longer authorised."
79
+
80
+ if res.status_code != 200:
81
+ return None
82
+
83
+ if res.text.strip() == "null":
84
+ return "Your ChaTerminal session was revoked from MemerDevs settings."
85
+
86
+ try:
87
+ session = res.json()
88
+ except Exception:
89
+ return "Your ChaTerminal session record is invalid."
90
+
91
+ if not isinstance(session, dict):
92
+ return "Your ChaTerminal session record is invalid."
93
+
94
+ active_device_id = session.get("activeDeviceId")
95
+ if active_device_id != self.state.device_id:
96
+ return "You were logged out because ChaTerminal is active on another device."
97
+
98
+ return None
99
+
100
+ def _handle_remote_logout(self, reason):
101
+ logger.error(f"[!] {reason}")
102
+ delete_session()
103
+ self.state.token = None
104
+ self.state.refresh_token = None
105
+ put_event(self.state, EventType.LOGOUT, text=reason)
106
+ try:
107
+ if self.ws_service.ws:
108
+ self.ws_service.ws.close()
109
+ except Exception:
110
+ pass
111
+
57
112
  def fetch_online_users(self):
58
113
  try:
59
114
  res = self.firebase.get("users")
@@ -55,7 +55,11 @@ class WebsocketService:
55
55
  active_id = payload.get("activeDeviceId")
56
56
  if active_id and active_id != self.state.device_id:
57
57
  logger.error("\n[!] Logged out from another device.")
58
- put_event(self.state, EventType.LOGOUT)
58
+ put_event(
59
+ self.state,
60
+ EventType.LOGOUT,
61
+ text="You were logged out because ChaTerminal is active on another device.",
62
+ )
59
63
  elif msg_type == "message_ack":
60
64
  logger.info(f"[+] Message acknowledged: {payload.get('msg_id', 'unknown')}")
61
65
  except Exception as e:
@@ -0,0 +1,26 @@
1
+ import os
2
+
3
+ from rich.console import Console
4
+
5
+ try:
6
+ from colorama import just_fix_windows_console
7
+
8
+ just_fix_windows_console()
9
+ except Exception:
10
+ pass
11
+
12
+ def _env_flag(name):
13
+ value = os.environ.get(name, "").strip().lower()
14
+ if not value:
15
+ return None
16
+ return value in {"1", "true", "yes", "on"}
17
+
18
+
19
+ _color_forced = _env_flag("CHATTERMINAL_COLOR")
20
+
21
+ console = Console(
22
+ color_system="auto",
23
+ force_terminal=True if _color_forced is True else None,
24
+ no_color=True if _color_forced is False else None,
25
+ highlight=False,
26
+ )
@@ -8,6 +8,7 @@ from prompt_toolkit.patch_stdout import patch_stdout
8
8
 
9
9
  from chaterminal.core.events import EventType
10
10
  from chaterminal.core.logger import logger
11
+ from chaterminal.storage.session_store import delete_session
11
12
  from chaterminal.ui.panels import get_user_color, activation_panel
12
13
  from chaterminal.ui.console import console
13
14
 
@@ -17,6 +18,7 @@ class TerminalUI:
17
18
  self.state = state
18
19
  self.message_service = message_service
19
20
  self.presence_service = presence_service
21
+ self._prompt_session = None
20
22
 
21
23
  def print_header(self):
22
24
  console.rule(f"[bold cyan]ChaTerminal v2[/bold cyan] - [green]{self.state.username}[/green] - E2EE Secure")
@@ -62,51 +64,83 @@ class TerminalUI:
62
64
  def run(self):
63
65
  """Main loop: drain event queue, then block on user input."""
64
66
  prompt = PromptSession()
67
+ self._prompt_session = prompt
68
+ self.state.wake_prompt = self._wake_prompt
65
69
 
66
- with patch_stdout():
67
- self.print_header()
68
- self.print_history()
69
- console.print("[bold green][+] Connected! Type a command below.[/bold green]\n")
70
-
71
- while self.state.running:
72
- # Drain any pending events first (non-blocking)
73
- self._drain_events()
74
-
75
- # Now block on user input
76
- try:
77
- line = prompt.prompt("> ")
78
- except (KeyboardInterrupt, EOFError):
79
- self.state.running = False
80
- break
81
-
82
- if not line or not line.strip():
83
- continue
84
-
85
- line = line.strip()
86
-
87
- if line == "/exit":
88
- self.state.running = False
89
- break
90
-
91
- elif line == "/help":
92
- console.print("[dim]Commands:[/dim] [yellow]/dm <user> <msg>[/yellow] [yellow]/list[/yellow] [yellow]/exit[/yellow]")
70
+ try:
71
+ with patch_stdout():
72
+ self.print_header()
73
+ self.print_history()
74
+ console.print("[bold green][+] Connected! Type a command below.[/bold green]\n")
75
+
76
+ while True:
77
+ # Drain any pending events first (non-blocking)
78
+ self._drain_events()
79
+
80
+ if not self.state.running:
81
+ break
82
+
83
+ # Now block on user input
84
+ try:
85
+ line = prompt.prompt("> ")
86
+ except (KeyboardInterrupt, EOFError):
87
+ self.state.running = False
88
+ break
89
+
90
+ if not line or not line.strip():
91
+ continue
92
+
93
+ line = line.strip()
94
+
95
+ if line == "/exit":
96
+ self.state.running = False
97
+ break
98
+
99
+ elif line == "/help":
100
+ console.print("[dim]Commands:[/dim] [yellow]/dm <user> <msg>[/yellow] [yellow]/list[/yellow] [yellow]/exit[/yellow]")
101
+
102
+ elif line == "/list":
103
+ online = self.presence_service.fetch_online_users()
104
+ if online:
105
+ console.print(f"[bold cyan][Online][/bold cyan] {', '.join(online)}")
106
+ else:
107
+ console.print("[yellow][!] No users currently online.[/yellow]")
108
+
109
+ elif line.startswith("/dm "):
110
+ parts = line.split(" ", 2)
111
+ if len(parts) < 3:
112
+ console.print("[red][!] Usage: /dm <username> <message>[/red]")
113
+ else:
114
+ self.message_service.queue_message(parts[1], parts[2])
93
115
 
94
- elif line == "/list":
95
- online = self.presence_service.fetch_online_users()
96
- if online:
97
- console.print(f"[bold cyan][Online][/bold cyan] {', '.join(online)}")
98
116
  else:
99
- console.print("[yellow][!] No users currently online.[/yellow]")
117
+ console.print("[red][!] Unknown command. Try /help, /dm, /list, or /exit.[/red]")
118
+ finally:
119
+ if self.state.wake_prompt == self._wake_prompt:
120
+ self.state.wake_prompt = None
121
+ self._prompt_session = None
122
+
123
+ def _wake_prompt(self):
124
+ prompt = self._prompt_session
125
+ if not prompt:
126
+ return
100
127
 
101
- elif line.startswith("/dm "):
102
- parts = line.split(" ", 2)
103
- if len(parts) < 3:
104
- console.print("[red][!] Usage: /dm <username> <message>[/red]")
105
- else:
106
- self.message_service.queue_message(parts[1], parts[2])
128
+ app = prompt.app
129
+
130
+ def exit_prompt():
131
+ try:
132
+ if app.is_running:
133
+ app.exit(result="")
134
+ except Exception:
135
+ pass
107
136
 
108
- else:
109
- console.print("[red][!] Unknown command. Try /help, /dm, /list, or /exit.[/red]")
137
+ try:
138
+ if app.is_running and app.loop and app.loop.is_running():
139
+ app.loop.call_soon_threadsafe(exit_prompt)
140
+ elif app.is_running:
141
+ exit_prompt()
142
+ except Exception:
143
+ exit_prompt()
110
144
 
111
145
  def _drain_events(self):
112
146
  """Consume all pending events from the queue without blocking."""
@@ -144,5 +178,9 @@ class TerminalUI:
144
178
  console.print(event.get("text", ""))
145
179
 
146
180
  elif event_type == EventType.LOGOUT:
147
- console.print("\n[bold red][!] You were logged out from another device.[/bold red]")
181
+ delete_session()
182
+ self.state.token = None
183
+ self.state.refresh_token = None
184
+ message = event.get("text") or "You were logged out from another device."
185
+ console.print(f"\n[bold red][!] {message}[/bold red]")
148
186
  self.state.running = False
@@ -6,7 +6,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8")
6
6
 
7
7
  setup(
8
8
  name="ChaTerminal",
9
- version="2.0.0",
9
+ version="2.0.1",
10
10
  author="Gofaone Tlalang",
11
11
  author_email="gofaone315@memerdevs.com",
12
12
  description="A terminal-based encrypted chat system for MemerDevs",
@@ -1,28 +0,0 @@
1
- import os
2
-
3
- from rich.console import Console
4
-
5
-
6
- def _env_flag(name):
7
- value = os.environ.get(name, "").strip().lower()
8
- if not value:
9
- return None
10
- return value in {"1", "true", "yes", "on"}
11
-
12
-
13
- def supports_ansi():
14
- forced = _env_flag("CHATTERMINAL_COLOR")
15
- if forced is not None:
16
- return forced
17
-
18
- return False
19
-
20
-
21
- _use_color = supports_ansi()
22
-
23
- console = Console(
24
- color_system="auto" if _use_color else None,
25
- force_terminal=True if _use_color else False,
26
- no_color=not _use_color,
27
- highlight=False,
28
- )
@@ -1,33 +0,0 @@
1
- LICENSE
2
- README.md
3
- setup.py
4
- ChaTerminal/__init__.py
5
- ChaTerminal/__main__.py
6
- ChaTerminal/main.py
7
- ChaTerminal.egg-info/PKG-INFO
8
- ChaTerminal.egg-info/SOURCES.txt
9
- ChaTerminal.egg-info/dependency_links.txt
10
- ChaTerminal.egg-info/entry_points.txt
11
- ChaTerminal.egg-info/requires.txt
12
- ChaTerminal.egg-info/top_level.txt
13
- ChaTerminal/core/__init__.py
14
- ChaTerminal/core/events.py
15
- ChaTerminal/core/logger.py
16
- ChaTerminal/core/state.py
17
- ChaTerminal/core/threads.py
18
- ChaTerminal/crypto/__init__.py
19
- ChaTerminal/crypto/encryption.py
20
- ChaTerminal/services/__init__.py
21
- ChaTerminal/services/auth_service.py
22
- ChaTerminal/services/firebase_service.py
23
- ChaTerminal/services/message_service.py
24
- ChaTerminal/services/presence_service.py
25
- ChaTerminal/services/websocket_service.py
26
- ChaTerminal/storage/__init__.py
27
- ChaTerminal/storage/database.py
28
- ChaTerminal/storage/session_store.py
29
- ChaTerminal/ui/__init__.py
30
- ChaTerminal/ui/console.py
31
- ChaTerminal/ui/panels.py
32
- ChaTerminal/ui/splash.py
33
- ChaTerminal/ui/terminal_ui.py
@@ -1 +0,0 @@
1
- ChaTerminal
File without changes
File without changes
File without changes