veilchat 0.2.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,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: veilchat
3
+ Version: 0.2.0
4
+ Summary: Privacy-first terminal social network and chat
5
+ Author: Razie1
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: click>=8.0.0
10
+ Requires-Dist: requests>=2.31.0
11
+ Requires-Dist: bcrypt>=4.0.0
12
+ Provides-Extra: tor
13
+ Requires-Dist: PySocks>=1.7.0; extra == "tor"
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "veilchat"
7
+ version = "0.2.0"
8
+ description = "Privacy-first terminal social network and chat"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Razie1" }]
13
+ dependencies = [
14
+ "click>=8.0.0",
15
+ "requests>=2.31.0",
16
+ "bcrypt>=4.0.0",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ tor = ["PySocks>=1.7.0"]
21
+
22
+ [project.scripts]
23
+ vt = "vt_cli:vt"
24
+ vc = "vc_cli:vc"
25
+ veiltalk = "vt_cli:vt"
26
+ veilchat = "vc_cli:vc"
27
+
28
+ [tool.setuptools]
29
+ packages = ["veilchat", "veilchat.core", "veilchat.utils"]
30
+ py-modules = ["vt_cli", "vc_cli"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Veilchat chat CLI (vc).
4
+ """
5
+ import getpass
6
+ import re
7
+ import sys
8
+
9
+ import click
10
+
11
+ from veilchat.config import DEFAULT_REGISTRY
12
+
13
+
14
+ @click.group()
15
+ @click.version_option(version="0.2.0")
16
+ def vc():
17
+ """🕶️ Veilchat — terminal chat rooms."""
18
+ pass
19
+
20
+
21
+ @vc.command()
22
+ @click.option("--name", "-n", required=True)
23
+ @click.option("--port", "-p", default=5555, type=int)
24
+ @click.option("--password", "--pass", default=None)
25
+ @click.option("--master-key", default=None,
26
+ help="Shared secret for --adminkey bypass")
27
+ @click.option("--registry", default=DEFAULT_REGISTRY)
28
+ def open(name, port, password, master_key, registry):
29
+ """Open a new chat room."""
30
+ from veilchat.core.server import start_server
31
+
32
+ if not re.match(r"^[a-zA-Z0-9_\- ]{1,32}$", name):
33
+ click.echo("\033[91m✗ Room name must be 1-32 chars\033[0m"); sys.exit(1)
34
+
35
+ click.echo(f"\n\033[95m🕶️ Opening '{name}' on port {port}\033[0m")
36
+ if password: click.echo("\033[93m🔒 Password protected\033[0m")
37
+ if master_key: click.echo("\033[93m🔑 Master key bypass enabled\033[0m")
38
+ click.echo(f"\033[90m📡 Registry: {registry}\033[0m\n")
39
+
40
+ try:
41
+ start_server(port=port, server_name=name,
42
+ registry_url=registry, password=password,
43
+ master_key=master_key)
44
+ except KeyboardInterrupt:
45
+ click.echo("\n\033[91m[Room closed]\033[0m")
46
+ sys.exit(0)
47
+
48
+
49
+ @vc.command()
50
+ @click.argument("name", required=False)
51
+ @click.option("--registry", default=DEFAULT_REGISTRY)
52
+ def search(name, registry):
53
+ """Search for chat rooms. Leave blank to list all."""
54
+ from veilchat.utils.registry import RegistryClient
55
+
56
+ c = RegistryClient(registry)
57
+ if not c.check_connection():
58
+ click.echo("\033[91m❌ Registry unreachable\033[0m"); sys.exit(1)
59
+
60
+ servers = c.list_servers()
61
+ if name:
62
+ servers = [s for s in servers if name.lower() in s.get("name", "").lower()]
63
+
64
+ click.echo(f"\n\033[95m🕶️ {'All Rooms' if not name else f'Search: {name}'}\033[0m")
65
+ click.echo(f"\033[90m{'━' * 50}\033[0m\n")
66
+
67
+ if not servers:
68
+ click.echo("\033[93m📭 No rooms\033[0m"); return
69
+
70
+ for s in servers:
71
+ uc = s.get("user_count", 0)
72
+ color = "\033[92m" if uc >= 10 else "\033[93m" if uc >= 5 else "\033[95m"
73
+ click.echo(f"{color}{s.get('name','?')}\033[0m")
74
+ if s.get("description"):
75
+ click.echo(f" \033[90m{s['description']}\033[0m")
76
+ click.echo(f" 👥 \033[93m{uc}\033[0m \033[90mvc enter {s.get('name','')}\033[0m\n")
77
+
78
+
79
+ @vc.command()
80
+ @click.argument("name")
81
+ @click.option("--registry", default=DEFAULT_REGISTRY)
82
+ @click.option("--direct", is_flag=True)
83
+ @click.option("--adminkey", is_flag=True)
84
+ @click.option("--tor", is_flag=True, help="Route through local Tor SOCKS5 (127.0.0.1:9050)")
85
+ def enter(name, registry, direct, adminkey, tor):
86
+ """Enter a chat room."""
87
+ from veilchat.core.client import start_client
88
+ from veilchat.utils.registry import resolve_server
89
+ from veilchat.utils.session import (
90
+ master_key_exists, get_master_key, get_session,
91
+ )
92
+ from veilchat.core.social import WhisprSocial
93
+
94
+ click.echo(f"\n\033[95m🕶️ Entering '{name}'\033[0m\n")
95
+
96
+ raw_key = None
97
+ if adminkey:
98
+ u = get_session()
99
+ if not u:
100
+ click.echo("\033[91m✗ Login required for --adminkey\033[0m"); sys.exit(1)
101
+ s = WhisprSocial()
102
+ if not s.is_staff(u):
103
+ click.echo("\033[91m✗ Staff only\033[0m"); sys.exit(1)
104
+ if not master_key_exists():
105
+ click.echo("\033[91m✗ No master key set\033[0m"); sys.exit(1)
106
+
107
+ # Load from local store — user proved identity when they set it.
108
+ # If they want re-auth, delete ~/.veilchat/masterkey and re-set.
109
+ raw_key = get_master_key()
110
+ click.echo("\033[92m✓ Master key loaded (server will verify via HMAC)\033[0m\n")
111
+
112
+ if direct:
113
+ host, port = (name.split(":") + ["5555"])[:2]
114
+ try:
115
+ port = int(port)
116
+ except ValueError:
117
+ click.echo("\033[91m✗ Invalid port\033[0m"); sys.exit(1)
118
+ else:
119
+ host, port = resolve_server(name, registry)
120
+ if not host:
121
+ click.echo(f"\033[91m❌ Room '{name}' not found\033[0m"); sys.exit(1)
122
+
123
+ try:
124
+ start_client(host, port, admin_key=raw_key, use_tor=tor)
125
+ except KeyboardInterrupt:
126
+ click.echo("\n\033[91m[Left room]\033[0m")
127
+ sys.exit(0)
128
+
129
+
130
+ veilchat = vc
131
+
132
+ if __name__ == "__main__":
133
+ vc()
@@ -0,0 +1,7 @@
1
+ """
2
+ Veilchat - Chat beyond the surface.
3
+ Privacy-first terminal social network and chat.
4
+ """
5
+
6
+ __version__ = "0.2.0"
7
+ __author__ = "Razie1"
@@ -0,0 +1,11 @@
1
+ """
2
+ Central config. Everything reads from here so we have one source of truth.
3
+ """
4
+ import os
5
+
6
+ DEFAULT_REGISTRY = os.environ.get(
7
+ "VEILCHAT_REGISTRY",
8
+ "https://veil.pythonanywhere.com",
9
+ ).rstrip("/")
10
+
11
+ VEILCHAT_DIR = os.path.expanduser("~/.veilchat")
@@ -0,0 +1,3 @@
1
+ from .server import ChatServer, start_server
2
+ from .client import ChatClient, start_client
3
+ from .social import WhisprSocial
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Veilchat chat client.
4
+ Tor is opt-in via the `use_tor` argument or VEILCHAT_TOR=1 env var.
5
+ """
6
+ import getpass
7
+ import os
8
+ import socket
9
+ import sys
10
+ import threading
11
+ import time
12
+
13
+ from ..utils.session import compute_master_hmac, get_master_key
14
+
15
+
16
+ class ChatClient:
17
+ def __init__(self, host, port, admin_key=None, use_tor=False):
18
+ self.host = host
19
+ self.port = port
20
+ self.admin_key = admin_key
21
+ self.nickname = ""
22
+ self.connected = True
23
+
24
+ if use_tor or os.environ.get("VEILCHAT_TOR") == "1":
25
+ try:
26
+ import socks
27
+ socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 9050)
28
+ socket.socket = socks.socksocket
29
+ except ImportError:
30
+ print("\033[91m[PySocks not installed — cannot use Tor]\033[0m")
31
+ sys.exit(1)
32
+
33
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
34
+
35
+ # ─────────────────────────────────────────
36
+ # HANDSHAKE — synchronous, before we spawn threads
37
+ # ─────────────────────────────────────────
38
+
39
+ def _send_line(self, s: str) -> None:
40
+ self.sock.sendall(s.encode("utf-8") + b"\n")
41
+
42
+ def _recv_line(self) -> str:
43
+ buf = b""
44
+ while not buf.endswith(b"\n"):
45
+ chunk = self.sock.recv(1)
46
+ if not chunk:
47
+ raise ConnectionError("disconnected")
48
+ buf += chunk
49
+ return buf.decode("utf-8", errors="replace").strip()
50
+
51
+ def _handshake(self) -> bool:
52
+ """Handle room password + master key + auth. Returns True if logged in."""
53
+ while True:
54
+ try:
55
+ msg = self._recv_line()
56
+ except ConnectionError:
57
+ print("\033[91m[Connection closed during handshake]\033[0m")
58
+ return False
59
+
60
+ if msg == "ROOM_PASS":
61
+ if self.admin_key:
62
+ self._send_line("MASTER_REQ")
63
+ nonce_hex = self._recv_line()
64
+ if not nonce_hex.startswith("MASTER_NONCE:"):
65
+ print("\033[91m[Unexpected server response]\033[0m")
66
+ return False
67
+ nonce = bytes.fromhex(nonce_hex.split(":", 1)[1])
68
+ resp = compute_master_hmac(self.admin_key, nonce)
69
+ self._send_line(f"MASTER_RESP:{resp}")
70
+ else:
71
+ pw = getpass.getpass("\033[95m🔒 Room password: \033[0m")
72
+ self._send_line(f"PASS:{pw}")
73
+
74
+ elif msg == "PASS_OK":
75
+ print("\033[92m[Room password accepted]\033[0m")
76
+
77
+ elif msg == "WRONG_PASS":
78
+ print("\033[91m[Wrong password — access denied]\033[0m")
79
+ return False
80
+
81
+ elif msg == "BANNED":
82
+ print("\033[91m[You are banned from this server]\033[0m")
83
+ return False
84
+
85
+ elif msg == "AUTH_TYPE":
86
+ print("\n\033[96m1. Login\033[0m")
87
+ print("\033[96m2. Register\033[0m")
88
+ choice = input("\nChoice (1/2): ").strip()
89
+ if choice == "1":
90
+ self._send_line("LOGIN")
91
+ elif choice == "2":
92
+ self._send_line("REGISTER")
93
+ else:
94
+ print("\033[91m[Invalid choice]\033[0m")
95
+ return False
96
+
97
+ elif msg == "REG_USER":
98
+ self.nickname = input("\n\033[96mChoose a username: \033[0m").strip()
99
+ self._send_line(self.nickname)
100
+
101
+ elif msg == "REG_PASS":
102
+ pw = getpass.getpass("\033[96mChoose a password: \033[0m")
103
+ confirm = getpass.getpass("\033[96mConfirm: \033[0m")
104
+ if pw != confirm:
105
+ print("\033[91m[Passwords don't match]\033[0m")
106
+ return False
107
+ self._send_line(pw)
108
+
109
+ elif msg == "REG_OK":
110
+ print("\033[92m[Account created!]\033[0m")
111
+
112
+ elif msg == "REG_FAIL":
113
+ print("\033[91m[Registration failed]\033[0m")
114
+ return False
115
+
116
+ elif msg == "USER_EXISTS":
117
+ print("\033[91m[Username already taken]\033[0m")
118
+ return False
119
+
120
+ elif msg == "LOGIN_USER":
121
+ self.nickname = input("\n\033[96mUsername: \033[0m").strip()
122
+ self._send_line(self.nickname)
123
+
124
+ elif msg == "LOGIN_PASS":
125
+ pw = getpass.getpass("\033[96mPassword: \033[0m")
126
+ self._send_line(pw)
127
+
128
+ elif msg == "LOGIN_OK":
129
+ print("\033[92m[Login successful!]\033[0m")
130
+
131
+ elif msg == "LOGIN_FAIL":
132
+ print("\033[91m[Login failed — wrong password]\033[0m")
133
+ return False
134
+
135
+ elif msg == "NO_ACCOUNT":
136
+ print("\033[91m[Account doesn't exist — register first]\033[0m")
137
+ return False
138
+
139
+ elif msg == "ALREADY_ONLINE":
140
+ print("\033[91m[Account already logged in]\033[0m")
141
+ return False
142
+
143
+ elif msg == "CONNECTED":
144
+ return True
145
+
146
+ else:
147
+ # Non-protocol line — likely a chat message arriving early. Print it.
148
+ print(msg)
149
+
150
+ # ─────────────────────────────────────────
151
+ # RUNTIME
152
+ # ─────────────────────────────────────────
153
+
154
+ def _recv_loop(self):
155
+ buf = ""
156
+ while self.connected:
157
+ try:
158
+ chunk = self.sock.recv(2048).decode("utf-8", errors="replace")
159
+ except OSError:
160
+ break
161
+ if not chunk:
162
+ break
163
+ buf += chunk
164
+ while "\n" in buf:
165
+ line, buf = buf.split("\n", 1)
166
+ print(f"\r{line}")
167
+ print(f"\033[96m{self.nickname}\033[0m > ", end="", flush=True)
168
+ if self.connected:
169
+ print("\n\033[91m[Connection lost]\033[0m")
170
+ self.connected = False
171
+
172
+ def _send_loop(self):
173
+ while self.connected:
174
+ try:
175
+ line = input(f"\033[96m{self.nickname}\033[0m > ")
176
+ except (EOFError, KeyboardInterrupt):
177
+ break
178
+ if line.strip().lower() == "/quit":
179
+ break
180
+ if line.strip():
181
+ try:
182
+ self.sock.sendall(line.encode("utf-8") + b"\n")
183
+ except OSError:
184
+ break
185
+ self.connected = False
186
+ try:
187
+ self.sock.close()
188
+ except OSError:
189
+ pass
190
+
191
+ def start(self):
192
+ print(f"\033[93m[Connecting to {self.host}:{self.port}...]\033[0m")
193
+ try:
194
+ self.sock.connect((self.host, self.port))
195
+ except OSError as e:
196
+ print(f"\033[91m[Could not connect: {e}]\033[0m")
197
+ sys.exit(1)
198
+
199
+ if not self._handshake():
200
+ self.sock.close()
201
+ sys.exit(1)
202
+
203
+ # Server sends a welcome banner after CONNECTED — drain it briefly
204
+ time.sleep(0.2)
205
+
206
+ threading.Thread(target=self._recv_loop, daemon=True).start()
207
+ self._send_loop()
208
+
209
+
210
+ def start_client(host, port, admin_key=None, use_tor=False):
211
+ c = ChatClient(host, port, admin_key=admin_key, use_tor=use_tor)
212
+ try:
213
+ c.start()
214
+ except KeyboardInterrupt:
215
+ print("\n\033[91m[Disconnected]\033[0m")
216
+ sys.exit(0)
217
+
218
+
219
+ if __name__ == "__main__":
220
+ import argparse
221
+ p = argparse.ArgumentParser(description="Veilchat chat client")
222
+ p.add_argument("host")
223
+ p.add_argument("port", nargs="?", type=int, default=5555)
224
+ p.add_argument("--tor", action="store_true")
225
+ args = p.parse_args()
226
+ start_client(args.host, args.port, use_tor=args.tor)