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.
- veilchat-0.2.0/PKG-INFO +13 -0
- veilchat-0.2.0/pyproject.toml +30 -0
- veilchat-0.2.0/setup.cfg +4 -0
- veilchat-0.2.0/vc_cli.py +133 -0
- veilchat-0.2.0/veilchat/_init_.py +7 -0
- veilchat-0.2.0/veilchat/config.py +11 -0
- veilchat-0.2.0/veilchat/core/_init_.py +3 -0
- veilchat-0.2.0/veilchat/core/client.py +226 -0
- veilchat-0.2.0/veilchat/core/server.py +624 -0
- veilchat-0.2.0/veilchat/core/social.py +597 -0
- veilchat-0.2.0/veilchat/setup.py +27 -0
- veilchat-0.2.0/veilchat/utils/_init_.py +6 -0
- veilchat-0.2.0/veilchat/utils/registry.py +137 -0
- veilchat-0.2.0/veilchat/utils/session.py +245 -0
- veilchat-0.2.0/veilchat.egg-info/PKG-INFO +13 -0
- veilchat-0.2.0/veilchat.egg-info/SOURCES.txt +19 -0
- veilchat-0.2.0/veilchat.egg-info/dependency_links.txt +1 -0
- veilchat-0.2.0/veilchat.egg-info/entry_points.txt +5 -0
- veilchat-0.2.0/veilchat.egg-info/requires.txt +6 -0
- veilchat-0.2.0/veilchat.egg-info/top_level.txt +3 -0
- veilchat-0.2.0/vt_cli.py +460 -0
veilchat-0.2.0/PKG-INFO
ADDED
|
@@ -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"]
|
veilchat-0.2.0/setup.cfg
ADDED
veilchat-0.2.0/vc_cli.py
ADDED
|
@@ -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,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,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)
|