Незаконченное обновление с добавлением модулей

This commit is contained in:
WHE Team 2026-05-19 15:19:27 +00:00
parent ec504574f2
commit 6dab73fae9
11 changed files with 805 additions and 223 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
settings.json settings.json
venv

428
TUI.py
View file

@ -1,5 +1,27 @@
import sys import sys
import subprocess import subprocess
import os
import json
import time
import base64
import re
import threading
import webbrowser
import importlib
import numpy as np
import sounddevice as sd
from prompt_toolkit.application import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, WindowAlign
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.widgets import TextArea, Frame
from prompt_toolkit.styles import Style
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import has_focus, Condition
from prompt_toolkit.data_structures import Point
from prompt_toolkit.completion import Completer, Completion
REQUIRED_PACKAGES = { REQUIRED_PACKAGES = {
"python-socketio[client]": "socketio", "python-socketio[client]": "socketio",
@ -31,30 +53,6 @@ def auto_install_deps():
auto_install_deps() auto_install_deps()
import os
import json
import time
import base64
import re
import threading
import webbrowser
import numpy as np
import sounddevice as sd
from prompt_toolkit.application import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import HSplit, VSplit, Window, FloatContainer, Float, ConditionalContainer, WindowAlign
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.widgets import TextArea, Frame
from prompt_toolkit.styles import Style
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import has_focus, Condition
from prompt_toolkit.data_structures import Point
from prompt_toolkit.completion import Completer, Completion
# Import decoupled local modules
import protocol
import socket_manager import socket_manager
CONFIG_FILE = "settings.json" CONFIG_FILE = "settings.json"
@ -92,6 +90,64 @@ class CommandCompleter(Completer):
if cmd.startswith(text): if cmd.startswith(text):
yield Completion(cmd, start_position=-len(text), display_meta=desc) yield Completion(cmd, start_position=-len(text), display_meta=desc)
class PluginManager:
def __init__(self, client_instance):
self.client = client_instance
self.protocols = {}
self.transports = {}
self.load_plugins()
def load_plugins(self):
for folder, collection, base_mod in [('protocols', self.protocols, 'protocols'), ('transports', self.transports, 'transports')]:
if not os.path.exists(folder):
os.makedirs(folder)
with open(os.path.join(folder, '__init__.py'), 'w') as f: pass
for f in os.listdir(folder):
if f.endswith('.py') and f != '__init__.py':
mod_name = f[:-3]
try:
mod = importlib.import_module(f"{base_mod}.{mod_name}")
if hasattr(mod, 'Plugin'):
instance = mod.Plugin()
collection[instance.name] = {"instance": instance, "enabled": True, "failed": False}
except:
pass
def safe_encode(self, proto_name, text):
proto = self.protocols.get(proto_name)
if not proto or not proto["enabled"] or proto["failed"]:
return None
try:
return proto["instance"].encode(text)
except:
proto["failed"] = True
proto["enabled"] = False
self.client.add_to_history(self.client.active_chat or "SYSTEM", f"[SYSTEM]: Protocol '{proto_name}' crashed and was disabled.")
return None
def safe_decode(self, proto_name, data):
proto = self.protocols.get(proto_name)
if not proto or not proto["enabled"] or proto["failed"]:
return None
try:
return proto["instance"].decode(data)
except:
proto["failed"] = True
proto["enabled"] = False
self.client.add_to_history(self.client.active_chat or "SYSTEM", f"[SYSTEM]: Protocol '{proto_name}' crashed and was disabled.")
return None
def safe_generate_service(self, proto_name, sig_type):
proto = self.protocols.get(proto_name)
if not proto or not proto["enabled"] or proto["failed"]:
return None
try:
return proto["instance"].generate_service_signal(sig_type)
except:
proto["failed"] = True
proto["enabled"] = False
return None
class TyClient: class TyClient:
def __init__(self): def __init__(self):
self.server_url = "" self.server_url = ""
@ -114,6 +170,15 @@ class TyClient:
self.pending_windows = [] self.pending_windows = []
self.show_welcome_popup = False self.show_welcome_popup = False
self.current_tab = "chat"
self.settings_cursor = 0
self.settings_section = "protocols"
self.primary_protocol = ""
self.primary_transport = ""
self.peer_session_protocols = {}
self.plugin_manager = PluginManager(self)
def load_config(self): def load_config(self):
if os.path.exists(CONFIG_FILE): if os.path.exists(CONFIG_FILE):
try: try:
@ -128,12 +193,32 @@ class TyClient:
self.history = cfg.get("history", {}) self.history = cfg.get("history", {})
self.preserved = cfg.get("preserved", {}) self.preserved = cfg.get("preserved", {})
self.blocklist = cfg.get("blocklist", []) self.blocklist = cfg.get("blocklist", [])
self.primary_protocol = cfg.get("primary_protocol", "")
self.primary_transport = cfg.get("primary_transport", "")
saved_protos = cfg.get("enabled_protocols", {})
for p_name, state in saved_protos.items():
if p_name in self.plugin_manager.protocols:
self.plugin_manager.protocols[p_name]["enabled"] = state
saved_trans = cfg.get("enabled_transports", {})
for t_name, state in saved_trans.items():
if t_name in self.plugin_manager.transports:
self.plugin_manager.transports[t_name]["enabled"] = state
if not self.primary_protocol and self.plugin_manager.protocols:
self.primary_protocol = list(self.plugin_manager.protocols.keys())[0]
if not self.primary_transport and self.plugin_manager.transports:
self.primary_transport = list(self.plugin_manager.transports.keys())[0]
return True return True
except: except:
return False return False
return False return False
def save_config(self): def save_config(self):
enabled_protos = {k: v["enabled"] for k, v in self.plugin_manager.protocols.items()}
enabled_trans = {k: v["enabled"] for k, v in self.plugin_manager.transports.items()}
cfg = { cfg = {
"server_url": self.server_url, "server_url": self.server_url,
"username": self.username, "username": self.username,
@ -143,7 +228,11 @@ class TyClient:
"groups": self.groups, "groups": self.groups,
"history": self.history, "history": self.history,
"preserved": self.preserved, "preserved": self.preserved,
"blocklist": self.blocklist "blocklist": self.blocklist,
"primary_protocol": self.primary_protocol,
"primary_transport": self.primary_transport,
"enabled_protocols": enabled_protos,
"enabled_transports": enabled_trans
} }
with open(CONFIG_FILE, "w", encoding="utf-8") as f: with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=4, ensure_ascii=False) json.dump(cfg, f, indent=4, ensure_ascii=False)
@ -151,9 +240,9 @@ class TyClient:
def play_dial_tones(self, target_uin): def play_dial_tones(self, target_uin):
self.dialing_uin = target_uin self.dialing_uin = target_uin
while self.dialing_uin == target_uin and self.loop_running: while self.dialing_uin == target_uin and self.loop_running:
t = np.linspace(0, 1.0, int(protocol.SAMPLE_RATE * 1.0), False) t = np.linspace(0, 1.0, int(44100 * 1.0), False)
tone = np.sin(425 * t * 2 * np.pi) * 0.5 tone = np.sin(425 * t * 2 * np.pi) * 0.5
sd.play(tone, protocol.SAMPLE_RATE) sd.play(tone, 44100)
sd.wait() sd.wait()
for _ in range(30): for _ in range(30):
if self.dialing_uin != target_uin or not self.loop_running: if self.dialing_uin != target_uin or not self.loop_running:
@ -162,10 +251,10 @@ class TyClient:
def play_and_decode(self, audio_bytes, sender_uin): def play_and_decode(self, audio_bytes, sender_uin):
audio_data = np.frombuffer(audio_bytes, dtype=np.float32) audio_data = np.frombuffer(audio_bytes, dtype=np.float32)
sd.play(audio_data, protocol.SAMPLE_RATE) sd.play(audio_data, 44100)
time.sleep(0.3) time.sleep(0.3)
samples_per_tone = int(protocol.SAMPLE_RATE * protocol.TONE_DURATION) samples_per_tone = int(44100 * 0.1)
current_sample = int(protocol.SAMPLE_RATE * 0.3) current_sample = int(44100 * 0.3)
sender_name = self.username if sender_uin == self.uin else f"UIN {sender_uin}" sender_name = self.username if sender_uin == self.uin else f"UIN {sender_uin}"
msg_buffer = f"[{sender_name}]: " msg_buffer = f"[{sender_name}]: "
@ -181,9 +270,9 @@ class TyClient:
break break
window_data = np.hanning(len(chunk)) window_data = np.hanning(len(chunk))
fft_data = np.abs(np.fft.rfft(chunk * window_data)) fft_data = np.abs(np.fft.rfft(chunk * window_data))
frequencies = np.fft.rfftfreq(len(chunk), d=1/protocol.SAMPLE_RATE) frequencies = np.fft.rfftfreq(len(chunk), d=1/44100)
detected_freq = frequencies[np.argmax(fft_data)] detected_freq = frequencies[np.argmax(fft_data)]
ascii_code = int(round((detected_freq - protocol.BASE_FREQ) / protocol.FREQ_STEP)) ascii_code = int(round((detected_freq - 600) / 25))
if 0 <= ascii_code < 65535: if 0 <= ascii_code < 65535:
try: try:
char = chr(ascii_code) char = chr(ascii_code)
@ -191,7 +280,7 @@ class TyClient:
self.refresh_ui() self.refresh_ui()
except: except:
pass pass
time.sleep(protocol.TONE_DURATION) time.sleep(0.1)
current_sample += samples_per_tone current_sample += samples_per_tone
sd.wait() sd.wait()
self.save_config() self.save_config()
@ -204,12 +293,38 @@ class TyClient:
self.save_config() self.save_config()
def send_sys_packet(self, to_uin, cmd): def send_sys_packet(self, to_uin, cmd):
audio_b = protocol.text_to_audio(f"SYS:{cmd}") text_payload = f"SYS:{cmd}"
socket_manager.sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(audio_b).decode('utf-8')}) proto_order = []
if self.primary_protocol:
proto_order.append(self.primary_protocol)
for p in self.plugin_manager.protocols:
if p not in proto_order:
proto_order.append(p)
def send_service_tone(self, to_uin, freq): for p_name in proto_order:
audio_b = protocol.generate_service_tone(freq) encoded = self.plugin_manager.safe_encode(p_name, text_payload)
socket_manager.sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(audio_b).decode('utf-8')}) if encoded is not None:
wrapped = f"NATIVE:{p_name}:".encode('utf-8') + encoded
socket_manager.sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(wrapped).decode('utf-8')})
return
def send_service_tone(self, to_uin, freq_or_type):
sig_map = {1200: "QUERY", 1400: "ALERT", 1600: "RESP_YES", 1800: "RESP_NO"}
sig_type = sig_map.get(freq_or_type, str(freq_or_type))
proto_order = []
if self.primary_protocol:
proto_order.append(self.primary_protocol)
for p in self.plugin_manager.protocols:
if p not in proto_order:
proto_order.append(p)
for p_name in proto_order:
encoded = self.plugin_manager.safe_generate_service(p_name, sig_type)
if encoded is not None:
wrapped = f"SERVICE:{p_name}:".encode('utf-8') + encoded
socket_manager.sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(wrapped).decode('utf-8')})
return
def refresh_ui(self): def refresh_ui(self):
if self.app: if self.app:
@ -225,10 +340,28 @@ def status_checker_thread():
if client.contacts[uin]["status"] == "online" and client.preserved.get(uin): if client.contacts[uin]["status"] == "online" and client.preserved.get(uin):
while client.preserved[uin]: while client.preserved[uin]:
p_text = client.preserved[uin].pop(0) p_text = client.preserved[uin].pop(0)
audio_b = protocol.text_to_audio(p_text)
p_b64 = base64.b64encode(audio_b).decode('utf-8') proto_to_use = client.peer_session_protocols.get(uin, client.primary_protocol)
socket_manager.sio.emit("relay_packet", {"to_uin": uin, "payload": p_b64}) encoded = client.plugin_manager.safe_encode(proto_to_use, p_text)
client.add_to_history(uin, f"[You]: {p_text}")
if encoded is None:
proto_order = [p for p in client.plugin_manager.protocols if p != proto_to_use]
for p_name in proto_order:
encoded = client.plugin_manager.safe_encode(p_name, p_text)
if encoded is not None:
proto_to_use = p_name
client.peer_session_protocols[uin] = p_name
break
if encoded is not None:
wrapped = f"NATIVE:{proto_to_use}:".encode('utf-8') + encoded
p_b64 = base64.b64encode(wrapped).decode('utf-8')
socket_manager.sio.emit("relay_packet", {"to_uin": uin, "payload": p_b64})
client.add_to_history(uin, f"[You]: {p_text}")
else:
client.preserved[uin].insert(0, p_text)
client.add_to_history(uin, "[SYSTEM]: Failed to negotiate protocol. Stalling output.")
break
client.save_config() client.save_config()
time.sleep(10) time.sleep(10)
@ -241,14 +374,14 @@ def make_layout():
elif info.get("unread", 0) > 0: status_str = f" ({info['unread']})" elif info.get("unread", 0) > 0: status_str = f" ({info['unread']})"
elif info.get("status") == "online": status_str = " *" elif info.get("status") == "online": status_str = " *"
content = f" {uin}{status_str}" content = f" {uin}{status_str}"
if client.active_chat == uin: style = "class:contact-active" if client.active_chat == uin and client.current_tab == "chat": style = "class:contact-active"
elif idx == client.selected_contact_idx and get_app().layout.has_focus(sidebar_window): style = "class:contact-focused" elif idx == client.selected_contact_idx and get_app().layout.has_focus(sidebar_window) and client.current_tab == "chat": style = "class:contact-focused"
else: style = "class:contact" else: style = "class:contact"
tokens.extend([(style, f"{content}\n")]) tokens.extend([(style, f"{content}\n")])
for g_id, g_info in client.groups.items(): for g_id, g_info in client.groups.items():
content = f" [G] {g_info['title']}" content = f" [G] {g_info['title']}"
if client.active_chat == g_id: style = "class:contact-active" if client.active_chat == g_id and client.current_tab == "chat": style = "class:contact-active"
else: style = "class:contact" else: style = "class:contact"
tokens.extend([(style, f"{content}\n")]) tokens.extend([(style, f"{content}\n")])
return tokens return tokens
@ -263,7 +396,7 @@ def make_layout():
("class:desc", "Messenger on custom protocol named\n"), ("class:desc", "Messenger on custom protocol named\n"),
("class:desc", "AcoustiOverSocket inspired by rtty\n"), ("class:desc", "AcoustiOverSocket inspired by rtty\n"),
("", "\n"), ("", "\n"),
("class:help-tip", "Type / to trigger interactive command menu\n"), ("class:help-tip", "Type / to trigger interactive command menu | F2: Settings Panel\n"),
("", "\n" * 2), ("", "\n" * 2),
("class:help-tip", f"{DEV_SIGNATURE}\n") ("class:help-tip", f"{DEV_SIGNATURE}\n")
]) ])
@ -285,6 +418,45 @@ def make_layout():
tokens.append(("", line + "\n")) tokens.append(("", line + "\n"))
return tokens return tokens
def get_settings_text():
tokens = [("class:popup-title", " === SETTINGS & PLUGINS PANEL ===\n\n")]
proto_style = "class:contact-focused" if client.settings_section == "protocols" else "class:contact"
trans_style = "class:contact-focused" if client.settings_section == "transports" else "class:contact"
tokens.append((proto_style, " [ PROTOCOLS ] "))
tokens.append(("", " "))
tokens.append((trans_style, " [ TRANSPORTS ] \n\n"))
collection = client.plugin_manager.protocols if client.settings_section == "protocols" else client.plugin_manager.transports
primary = client.primary_protocol if client.settings_section == "protocols" else client.primary_transport
items = list(collection.keys())
if not items:
tokens.append(("", " No items found in this section.\n"))
else:
for idx, name in enumerate(items):
info = collection[name]
marker = "[X]" if info["enabled"] else "[ ]"
prim_marker = " (Primary)" if name == primary else ""
fail_marker = " [CRASHED/DISABLED]" if info["failed"] else ""
if idx == client.settings_cursor:
style = "class:contact-active"
item_str = f" > {marker} {name}{prim_marker}{fail_marker}\n"
else:
style = "class:contact"
item_str = f" {marker} {name}{prim_marker}{fail_marker}\n"
tokens.append((style, item_str))
tokens.append(("", "\n--- Controls ---\n"))
tokens.append(("class:help-tip", " Left/Right: Switch sections\n"))
tokens.append(("class:help-tip", " Up/Down: Select item\n"))
tokens.append(("class:help-tip", " Space: Toggle Enable/Disable\n"))
tokens.append(("class:help-tip", " P: Set chosen item as Primary\n"))
tokens.append(("class:help-tip", " F2: Return back to Chat Panel\n"))
return tokens
def get_cursor_pos(): def get_cursor_pos():
text = "".join(t[1] for t in get_main_text()) text = "".join(t[1] for t in get_main_text())
newlines = text.count('\n') newlines = text.count('\n')
@ -302,6 +474,9 @@ def make_layout():
main_window = Frame(Window(content=main_control, wrap_lines=True), title=get_main_title, style="class:border") main_window = Frame(Window(content=main_control, wrap_lines=True), title=get_main_title, style="class:border")
settings_control = FormattedTextControl(get_settings_text, focusable=True)
settings_window = Frame(Window(content=settings_control, wrap_lines=True), title="Settings Router", style="class:border")
input_field = TextArea( input_field = TextArea(
height=3, height=3,
prompt="> ", prompt="> ",
@ -390,26 +565,39 @@ def make_layout():
client.add_to_history(client.active_chat, "[SYSTEM]: CANNOT SEND. Owner of this room is offline for sync!") client.add_to_history(client.active_chat, "[SYSTEM]: CANNOT SEND. Owner of this room is offline for sync!")
input_field.text = "" input_field.text = ""
return return
audio_b = protocol.text_to_audio(f"SYS:ROOM_MSG:{client.active_chat}:{client.uin}:{text}")
p_b64 = base64.b64encode(audio_b).decode('utf-8') proto_to_use = client.primary_protocol
if owner_uin == client.uin: encoded = client.plugin_manager.safe_encode(proto_to_use, f"SYS:ROOM_MSG:{client.active_chat}:{client.uin}:{text}")
for m in group["members"]: if encoded is None:
if m != client.uin and client.contacts.get(m, {}).get("status") == "online": for p in client.plugin_manager.protocols:
socket_manager.sio.emit("relay_packet", {"to_uin": m, "payload": p_b64}) encoded = client.plugin_manager.safe_encode(p, f"SYS:ROOM_MSG:{client.active_chat}:{client.uin}:{text}")
if encoded is not None:
proto_to_use = p
break
if encoded is not None:
wrapped = f"NATIVE:{proto_to_use}:".encode('utf-8') + encoded
p_b64 = base64.b64encode(wrapped).decode('utf-8')
if owner_uin == client.uin:
for m in group["members"]:
if m != client.uin and client.contacts.get(m, {}).get("status") == "online":
socket_manager.sio.emit("relay_packet", {"to_uin": m, "payload": p_b64})
else:
socket_manager.sio.emit("relay_packet", {"to_uin": owner_uin, "payload": p_b64})
client.add_to_history(client.active_chat, f"[You]: {text}")
else: else:
socket_manager.sio.emit("relay_packet", {"to_uin": owner_uin, "payload": p_b64}) client.add_to_history(client.active_chat, "[SYSTEM]: Local engine failure encoding text frame.")
client.add_to_history(client.active_chat, f"[You]: {text}")
input_field.text = "" input_field.text = ""
return return
if text.lower() == "/query": if text.lower() == "/query":
client.send_service_tone(client.active_chat, protocol.FREQ_QUERY) client.send_service_tone(client.active_chat, 1200)
client.add_to_history(client.active_chat, "[SYSTEM]: Sent friendly status query...") client.add_to_history(client.active_chat, "[SYSTEM]: Sent friendly status query...")
input_field.text = "" input_field.text = ""
return return
if text.lower() == "/alert": if text.lower() == "/alert":
client.send_service_tone(client.active_chat, protocol.FREQ_ALERT) client.send_service_tone(client.active_chat, 1400)
client.add_to_history(client.active_chat, "[SYSTEM]: Sent URGENT alert signal (breaks DND)...") client.add_to_history(client.active_chat, "[SYSTEM]: Sent URGENT alert signal...")
input_field.text = "" input_field.text = ""
return return
if len(text) > 300: if len(text) > 300:
@ -417,20 +605,48 @@ def make_layout():
input_field.text = "" input_field.text = ""
return return
audio_b = protocol.text_to_audio(text) proto_to_use = client.peer_session_protocols.get(client.active_chat, client.primary_protocol)
p_b64 = base64.b64encode(audio_b).decode('utf-8') encoded = client.plugin_manager.safe_encode(proto_to_use, text)
if client.contacts[client.active_chat]["status"] == "online":
socket_manager.sio.emit("relay_packet", {"to_uin": client.active_chat, "payload": p_b64}) if encoded is None:
client.add_to_history(client.active_chat, f"[You]: {text}") proto_order = [p for p in client.plugin_manager.protocols if p != proto_to_use]
for p_name in proto_order:
encoded = client.plugin_manager.safe_encode(p_name, text)
if encoded is not None:
proto_to_use = p_name
client.peer_session_protocols[client.active_chat] = p_name
break
if encoded is not None:
wrapped = f"NATIVE:{proto_to_use}:".encode('utf-8') + encoded
p_b64 = base64.b64encode(wrapped).decode('utf-8')
if client.contacts[client.active_chat]["status"] == "online":
socket_manager.sio.emit("relay_packet", {"to_uin": client.active_chat, "payload": p_b64})
client.add_to_history(client.active_chat, f"[You]: {text}")
else:
if client.active_chat not in client.preserved: client.preserved[client.active_chat] = []
client.preserved[client.active_chat].append(text)
client.add_to_history(client.active_chat, f"[You]: {text} (Preserved)")
client.save_config()
else: else:
if client.active_chat not in client.preserved: client.preserved[client.active_chat] = [] client.add_to_history(client.active_chat, "[SYSTEM]: Conversation pipeline failed. No working/enabled matching protocols.")
client.preserved[client.active_chat].append(text)
client.add_to_history(client.active_chat, f"[You]: {text} (Preserved)")
client.save_config()
input_field.text = "" input_field.text = ""
input_field.accept_handler = accept_handler input_field.accept_handler = accept_handler
right_side = HSplit([main_window, input_window])
@Condition
def is_chat_panel():
return client.current_tab == "chat"
@Condition
def is_settings_panel():
return client.current_tab == "settings"
right_display = HSplit([
ConditionalContainer(content=main_window, filter=is_chat_panel),
ConditionalContainer(content=settings_window, filter=is_settings_panel),
input_window
])
def get_popup_text(): def get_popup_text():
if not client.pending_requests: return [] if not client.pending_requests: return []
@ -476,7 +692,7 @@ def make_layout():
def has_welcome_popup(): return client.show_welcome_popup def has_welcome_popup(): return client.show_welcome_popup
root_container = FloatContainer( root_container = FloatContainer(
content=VSplit([sidebar_window, right_side]), content=VSplit([sidebar_window, right_display]),
floats=[ floats=[
Float(content=ConditionalContainer(content=popup_window, filter=has_pending_request), transparent=False), Float(content=ConditionalContainer(content=popup_window, filter=has_pending_request), transparent=False),
Float(content=ConditionalContainer(content=signal_popup_window, filter=has_pending_window), transparent=False), Float(content=ConditionalContainer(content=signal_popup_window, filter=has_pending_window), transparent=False),
@ -486,12 +702,76 @@ def make_layout():
kb = KeyBindings() kb = KeyBindings()
@kb.add('tab', filter=~has_pending_request & ~has_pending_window & ~has_welcome_popup) @kb.add('f2')
def _(event):
if client.current_tab == "chat":
client.current_tab = "settings"
client.settings_cursor = 0
else:
client.current_tab = "chat"
client.refresh_ui()
@kb.add('left', filter=is_settings_panel)
def _(event):
if client.settings_section == "transports":
client.settings_section = "protocols"
client.settings_cursor = 0
client.refresh_ui()
@kb.add('right', filter=is_settings_panel)
def _(event):
if client.settings_section == "protocols":
client.settings_section = "transports"
client.settings_cursor = 0
client.refresh_ui()
@kb.add('up', filter=is_settings_panel)
def _(event):
collection = client.plugin_manager.protocols if client.settings_section == "protocols" else client.plugin_manager.transports
items = list(collection.keys())
if items:
client.settings_cursor = (client.settings_cursor - 1) % len(items)
client.refresh_ui()
@kb.add('down', filter=is_settings_panel)
def _(event):
collection = client.plugin_manager.protocols if client.settings_section == "protocols" else client.plugin_manager.transports
items = list(collection.keys())
if items:
client.settings_cursor = (client.settings_cursor + 1) % len(items)
client.refresh_ui()
@kb.add('space', filter=is_settings_panel)
def _(event):
collection = client.plugin_manager.protocols if client.settings_section == "protocols" else client.plugin_manager.transports
items = list(collection.keys())
if items:
name = items[client.settings_cursor]
collection[name]["enabled"] = not collection[name]["enabled"]
client.save_config()
client.refresh_ui()
@kb.add('p', filter=is_settings_panel)
@kb.add('P', filter=is_settings_panel)
def _(event):
collection = client.plugin_manager.protocols if client.settings_section == "protocols" else client.plugin_manager.transports
items = list(collection.keys())
if items:
name = items[client.settings_cursor]
if collection[name]["enabled"]:
if client.settings_section == "protocols":
client.primary_protocol = name
else:
client.primary_transport = name
client.save_config()
client.refresh_ui()
@kb.add('tab', filter=~has_pending_request & ~has_pending_window & ~has_welcome_popup & is_chat_panel)
def _(event): def _(event):
if event.app.layout.has_focus(input_field): event.app.layout.focus(sidebar_window) if event.app.layout.has_focus(input_field): event.app.layout.focus(sidebar_window)
else: event.app.layout.focus(input_field) else: event.app.layout.focus(input_field)
@kb.add('up', filter=has_focus(sidebar_window) & ~has_pending_request & ~has_pending_window & ~has_welcome_popup) @kb.add('up', filter=has_focus(sidebar_window) & ~has_pending_request & ~has_pending_window & ~has_welcome_popup & is_chat_panel)
def _(event): def _(event):
all_chats = list(client.contacts.keys()) + list(client.groups.keys()) all_chats = list(client.contacts.keys()) + list(client.groups.keys())
if all_chats: if all_chats:
@ -503,7 +783,7 @@ def make_layout():
client.contacts[target]["attention"] = False client.contacts[target]["attention"] = False
client.refresh_ui() client.refresh_ui()
@kb.add('down', filter=has_focus(sidebar_window) & ~has_pending_request & ~has_pending_window & ~has_welcome_popup) @kb.add('down', filter=has_focus(sidebar_window) & ~has_pending_request & ~has_pending_window & ~has_welcome_popup & is_chat_panel)
def _(event): def _(event):
all_chats = list(client.contacts.keys()) + list(client.groups.keys()) all_chats = list(client.contacts.keys()) + list(client.groups.keys())
if all_chats: if all_chats:
@ -542,7 +822,7 @@ def make_layout():
win_info = client.pending_windows.pop(0) win_info = client.pending_windows.pop(0)
target_uin = win_info["uin"] target_uin = win_info["uin"]
client.is_busy = True client.is_busy = True
client.send_service_tone(target_uin, protocol.FREQ_RESP_YES) client.send_service_tone(target_uin, 1600)
client.add_to_history(target_uin, "[You -> SYSTEM]: Sent Dial Tone -> Yes, I'm busy.") client.add_to_history(target_uin, "[You -> SYSTEM]: Sent Dial Tone -> Yes, I'm busy.")
client.refresh_ui() client.refresh_ui()
@ -551,7 +831,7 @@ def make_layout():
def _(event): def _(event):
win_info = client.pending_windows.pop(0) win_info = client.pending_windows.pop(0)
target_uin = win_info["uin"] target_uin = win_info["uin"]
client.send_service_tone(target_uin, protocol.FREQ_RESP_NO) client.send_service_tone(target_uin, 1800)
client.add_to_history(target_uin, "[You -> SYSTEM]: Sent Dial Tone -> No, go on.") client.add_to_history(target_uin, "[You -> SYSTEM]: Sent Dial Tone -> No, go on.")
client.refresh_ui() client.refresh_ui()
@ -610,8 +890,6 @@ def main():
except: pass except: pass
is_new_registration = False is_new_registration = False
# Initialize the cross-module link so network callbacks can manipulate local state
socket_manager.init_network(client) socket_manager.init_network(client)
if not client.load_config(): if not client.load_config():

View file

@ -1,40 +0,0 @@
import os
import subprocess
import sys
import shutil
import platform
REPO_URL = "https://git.idkmail.ru/lohrrrr/TyChat-Client.git"
PROJECT_DIR = "TyChat-Client-Build"
VENV_DIR = os.path.join(PROJECT_DIR, "venv")
ENTRY_POINT = "TUI.py"
def run_cmd(cmd, cwd=None):
subprocess.run(cmd, cwd=cwd, check=True)
def setup_build():
if os.path.exists(PROJECT_DIR):
shutil.rmtree(PROJECT_DIR)
print(f"[*] Cloning repository...")
run_cmd(["git", "clone", REPO_URL, PROJECT_DIR])
print(f"[*] Creating virtual environment...")
run_cmd([sys.executable, "-m", "venv", VENV_DIR])
if platform.system() == "Windows":
pip_path = os.path.join(VENV_DIR, "Scripts", "pip")
pyinstaller_path = os.path.join(VENV_DIR, "Scripts", "pyinstaller")
else:
pip_path = os.path.join(VENV_DIR, "bin", "pip")
pyinstaller_path = os.path.join(VENV_DIR, "bin", "pyinstaller")
print(f"[*] Installing dependencies...")
run_cmd([pip_path, "install", "pyinstaller", "socketio", "websocket-client", "numpy", "sounddevice", "prompt_toolkit"])
print(f"[*] Building standalone binary...")
run_cmd([pyinstaller_path, "--onefile", "--name", "TyChat", ENTRY_POINT], cwd=PROJECT_DIR)
print(f"\n[!] Build Complete! Find your binary in: {os.path.join(PROJECT_DIR, 'dist')}")
if __name__ == "__main__":
setup_build()

124
extra/compile.py Normal file
View file

@ -0,0 +1,124 @@
# TyChat TUI auto installer
# This is bugged pice of shit because of crossplatform
# I will only officialy supporting Windows and UNIX-Like systems excluding MacOS and KolibriOS
import os
import sys
import shutil
import subprocess
import platform
import urllib.request
import zipfile
# Configuration, you can change if you like, for example, making a fork of my messenger
REPO_URL = "https://git.idkmail.ru/lohrrrr/TyChat-Client.git"
PROJECT_DIR = "TyChat-Client-Build"
VENV_DIR = os.path.join(PROJECT_DIR, "setup-venv")
ENTRY_POINT = "TUI.py"
DEPENDENCIES = [
"pyinstaller",
"python-socketio",
"websocket-client",
"numpy",
"sounddevice",
"prompt_toolkit"
]
GREEN = '\033[92m'
RESET = '\033[0m'
def run_command(command, cwd=None, shell=False):
"""Wrapper to run shell commands."""
try:
subprocess.run(command, cwd=cwd, check=True, shell=shell)
except subprocess.CalledProcessError as e:
print(f"[ERROR] Command failed: {' '.join(command)}")
sys.exit(1)
def get_venv_paths():
"""Determine paths based on the operating system and check for valid executables."""
# Resolve to an absolute path so altering cwd downstream doesn't break relative lookups
absolute_venv = os.path.abspath(VENV_DIR)
if platform.system() == "Windows":
return os.path.join(absolute_venv, "Scripts", "python.exe"), os.path.join(absolute_venv, "Scripts", "pip.exe")
# Check for python3, fallback to python
python_path = os.path.join(absolute_venv, "bin", "python3")
if not os.path.exists(python_path):
python_path = os.path.join(absolute_venv, "bin", "python")
# Check for pip3, fallback to pip
pip_path = os.path.join(absolute_venv, "bin", "pip3")
if not os.path.exists(pip_path):
pip_path = os.path.join(absolute_venv, "bin", "pip")
return python_path, pip_path
def download_and_extract_zip(url, target_dir):
"""Downloads the repository source ZIP and extracts its contents into target_dir."""
zip_path = "repo_archive.zip"
try:
print(" -> Git not found. Downloading source ZIP archive...")
# Download the zip file
urllib.request.urlretrieve(url, zip_path)
print(" -> Extracting archive...")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
# The archive extracts into a root folder like 'TyChat-Client' or 'TyChat-Client-main'
# We look at the first directory level inside the ZIP
root_in_zip = zip_ref.namelist()[0].split('/')[0]
zip_ref.extractall(".")
# Rename the extracted folder to match your expected PROJECT_DIR
if os.path.exists(root_in_zip):
os.rename(root_in_zip, target_dir)
except Exception as e:
print(f"[ERROR] Failed to download or extract the repository ZIP: {e}")
sys.exit(1)
finally:
# Clean up the downloaded zip file
if os.path.exists(zip_path):
os.remove(zip_path)
def main():
# Cleanup existing build
if os.path.exists(PROJECT_DIR):
shutil.rmtree(PROJECT_DIR)
print("[*] Cloning repository...")
# Check if git is available in the system PATH
if shutil.which("git"):
run_command(["git", "clone", REPO_URL, PROJECT_DIR])
else:
# Fallback to the direct zip archive if git is missing
zip_url = "https://git.idkmail.ru/lohrrrr/TyChat-Client/archive/main.zip"
download_and_extract_zip(zip_url, PROJECT_DIR)
print("[*] Creating virtual environment...")
run_command([sys.executable, "-m", "venv", VENV_DIR])
python_exe, pip_exe = get_venv_paths()
print("[*] Upgrading pip...")
run_command([python_exe, "-m", "pip", "install", "--upgrade", "pip"])
print("[*] Installing dependencies...")
for dep in DEPENDENCIES:
print(f" -> Installing {dep}...")
run_command([pip_exe, "install", dep])
print("[*] Building standalone binary...")
# Run PyInstaller (and pray to the god)
run_command([python_exe, "-m", "PyInstaller", "--onefile", "--name", "TyChat", ENTRY_POINT], cwd=PROJECT_DIR)
dist_dir = os.path.join(PROJECT_DIR, "dist")
if os.path.exists(dist_dir):
print(f"\n{GREEN}Your executable file is located in {os.path.abspath(dist_dir)}{RESET}")
else:
print(f"\n[!] Build complete, but 'dist' directory not found at: {dist_dir}.")
if __name__ == "__main__":
main()

View file

@ -10,43 +10,25 @@ FREQ_ALERT = 1400
FREQ_RESP_YES = 1600 FREQ_RESP_YES = 1600
FREQ_RESP_NO = 1800 FREQ_RESP_NO = 1800
def generate_service_tone(frequency, duration=0.4): class Plugin:
t = np.linspace(0, duration, int(SAMPLE_RATE * duration), False) def __init__(self):
tone = np.sin(frequency * t * 2 * np.pi) * 0.7 self.name = "Audio Protocol"
envelope = np.ones_like(tone)
fade_len = int(SAMPLE_RATE * 0.05)
envelope[:fade_len] = np.linspace(0, 1, fade_len)
envelope[-fade_len:] = np.linspace(1, 0, fade_len)
return (tone * envelope).astype(np.float32).tobytes()
def text_to_audio(text): def encode(self, text: str) -> bytes:
audio_signals = [] audio_signals = []
t_start = np.linspace(0, 0.3, int(SAMPLE_RATE * 0.3), False) t_start = np.linspace(0, 0.3, int(SAMPLE_RATE * 0.3), False)
audio_signals.append(np.sin(1000 * t_start * 2 * np.pi)) audio_signals.append(np.sin(1000 * t_start * 2 * np.pi))
for char in text: for char in text:
freq = BASE_FREQ + ord(char) * FREQ_STEP freq = BASE_FREQ + ord(char) * FREQ_STEP
t = np.linspace(0, TONE_DURATION, int(SAMPLE_RATE * TONE_DURATION), False) t = np.linspace(0, TONE_DURATION, int(SAMPLE_RATE * TONE_DURATION), False)
audio_signals.append(np.sin(freq * t * 2 * np.pi)) audio_signals.append(np.sin(freq * t * 2 * np.pi))
full_audio = np.concatenate(audio_signals).astype(np.float32) full_audio = np.concatenate(audio_signals).astype(np.float32)
if np.max(np.abs(full_audio)) > 0: if np.max(np.abs(full_audio)) > 0:
full_audio = full_audio / np.max(np.abs(full_audio)) full_audio = full_audio / np.max(np.abs(full_audio))
return full_audio.tobytes() return full_audio.tobytes()
def detect_service_tone(audio_data): def decode(self, data: bytes) -> str:
try: audio_data = np.frombuffer(data, dtype=np.float32)
window_data = np.hanning(len(audio_data))
fft_data = np.abs(np.fft.rfft(audio_data * window_data))
frequencies = np.fft.rfftfreq(len(audio_data), d=1/SAMPLE_RATE)
detected_freq = frequencies[np.argmax(fft_data)]
for target in [FREQ_QUERY, FREQ_ALERT, FREQ_RESP_YES, FREQ_RESP_NO]:
if abs(detected_freq - target) <= 15:
return target
return None
except:
return None
def fast_decode(audio_data):
try:
samples_per_tone = int(SAMPLE_RATE * TONE_DURATION) samples_per_tone = int(SAMPLE_RATE * TONE_DURATION)
current_sample = int(SAMPLE_RATE * 0.3) current_sample = int(SAMPLE_RATE * 0.3)
text = "" text = ""
@ -63,5 +45,52 @@ def fast_decode(audio_data):
text += chr(ascii_code) text += chr(ascii_code)
current_sample += samples_per_tone current_sample += samples_per_tone
return text return text
except:
return "" def generate_service_signal(self, signal_type: str) -> bytes:
sig_map = {"QUERY": FREQ_QUERY, "ALERT": FREQ_ALERT, "RESP_YES": FREQ_RESP_YES, "RESP_NO": FREQ_RESP_NO}
freq = sig_map.get(signal_type, FREQ_QUERY)
t = np.linspace(0, 0.4, int(SAMPLE_RATE * 0.4), False)
tone = np.sin(freq * t * 2 * np.pi) * 0.7
envelope = np.ones_like(tone)
fade_len = int(SAMPLE_RATE * 0.05)
envelope[:fade_len] = np.linspace(0, 1, fade_len)
envelope[-fade_len:] = np.linspace(1, 0, fade_len)
return (tone * envelope).astype(np.float32).tobytes()
def detect_service_signal(self, data: bytes) -> str:
try:
audio_data = np.frombuffer(data, dtype=np.float32)
window_data = np.hanning(len(audio_data))
fft_data = np.abs(np.fft.rfft(audio_data * window_data))
frequencies = np.fft.rfftfreq(len(audio_data), d=1/SAMPLE_RATE)
detected_freq = frequencies[np.argmax(fft_data)]
sig_map = {FREQ_QUERY: "QUERY", FREQ_ALERT: "ALERT", FREQ_RESP_YES: "RESP_YES", FREQ_RESP_NO: "RESP_NO"}
for target_freq, sig_type in sig_map.items():
if abs(detected_freq - target_freq) <= 15:
return sig_type
return None
except:
return None
def generate_service_tone(frequency, duration=0.4):
t = np.linspace(0, duration, int(SAMPLE_RATE * duration), False)
tone = np.sin(frequency * t * 2 * np.pi) * 0.7
envelope = np.ones_like(tone)
fade_len = int(SAMPLE_RATE * 0.05)
envelope[:fade_len] = np.linspace(0, 1, fade_len)
envelope[-fade_len:] = np.linspace(1, 0, fade_len)
return (tone * envelope).astype(np.float32).tobytes()
def text_to_audio(text):
p = Plugin()
return p.encode(text)
def detect_service_tone(audio_data):
p = Plugin()
sig = p.detect_service_signal(audio_data.tobytes())
sig_map = {"QUERY": FREQ_QUERY, "ALERT": FREQ_ALERT, "RESP_YES": FREQ_RESP_YES, "RESP_NO": FREQ_RESP_NO}
return sig_map.get(sig, None)
def fast_decode(audio_data):
p = Plugin()
return p.decode(audio_data.tobytes())

24
protocols/__init__.py Normal file
View file

@ -0,0 +1,24 @@
import os
import importlib
import logging
class BaseProtocol:
name = "Base"
def encode(self, text: str) -> bytes: raise NotImplementedError
def decode(self, data: bytes) -> str: raise NotImplementedError
def generate_service_signal(self, signal_type: str) -> bytes: raise NotImplementedError
def detect_service_signal(self, data: bytes) -> str: raise NotImplementedError
def load_protocols():
plugins = {}
for f in os.listdir('./protocols'):
if f.endswith('.py') and f != '__init__.py':
mod_name = f[:-3]
try:
mod = importlib.import_module(f'protocols.{mod_name}')
if hasattr(mod, 'ProtocolPlugin'):
instance = mod.ProtocolPlugin()
plugins[instance.name] = {"instance": instance, "enabled": True, "failed": False}
except Exception as e:
logging.error(f"Failed to load protocol {mod_name}: {e}")
return plugins

View file

@ -0,0 +1,73 @@
import numpy as np
BASE_FREQ = 600
FREQ_STEP = 25
TONE_DURATION = 0.1
SAMPLE_RATE = 44100
FREQ_QUERY = 1200
FREQ_ALERT = 1400
FREQ_RESP_YES = 1600
FREQ_RESP_NO = 1800
class Plugin:
def __init__(self):
self.name = "Audio Protocol"
def encode(self, text: str) -> bytes:
audio_signals = []
t_start = np.linspace(0, 0.3, int(SAMPLE_RATE * 0.3), False)
audio_signals.append(np.sin(1000 * t_start * 2 * np.pi))
for char in text:
freq = BASE_FREQ + ord(char) * FREQ_STEP
t = np.linspace(0, TONE_DURATION, int(SAMPLE_RATE * TONE_DURATION), False)
audio_signals.append(np.sin(freq * t * 2 * np.pi))
full_audio = np.concatenate(audio_signals).astype(np.float32)
if np.max(np.abs(full_audio)) > 0:
full_audio = full_audio / np.max(np.abs(full_audio))
return full_audio.tobytes()
def decode(self, data: bytes) -> str:
audio_data = np.frombuffer(data, dtype=np.float32)
samples_per_tone = int(SAMPLE_RATE * TONE_DURATION)
current_sample = int(SAMPLE_RATE * 0.3)
text = ""
while current_sample < len(audio_data):
chunk = audio_data[current_sample : current_sample + samples_per_tone]
if len(chunk) < samples_per_tone:
break
window_data = np.hanning(len(chunk))
fft_data = np.abs(np.fft.rfft(chunk * window_data))
frequencies = np.fft.rfftfreq(len(chunk), d=1/SAMPLE_RATE)
detected_freq = frequencies[np.argmax(fft_data)]
ascii_code = int(round((detected_freq - BASE_FREQ) / FREQ_STEP))
if 0 <= ascii_code < 65535:
text += chr(ascii_code)
current_sample += samples_per_tone
return text
def generate_service_signal(self, signal_type: str) -> bytes:
sig_map = {"QUERY": FREQ_QUERY, "ALERT": FREQ_ALERT, "RESP_YES": FREQ_RESP_YES, "RESP_NO": FREQ_RESP_NO}
freq = sig_map.get(signal_type, FREQ_QUERY)
t = np.linspace(0, 0.4, int(SAMPLE_RATE * 0.4), False)
tone = np.sin(freq * t * 2 * np.pi) * 0.7
envelope = np.ones_like(tone)
fade_len = int(SAMPLE_RATE * 0.05)
envelope[:fade_len] = np.linspace(0, 1, fade_len)
envelope[-fade_len:] = np.linspace(1, 0, fade_len)
return (tone * envelope).astype(np.float32).tobytes()
def detect_service_signal(self, data: bytes) -> str:
try:
audio_data = np.frombuffer(data, dtype=np.float32)
window_data = np.hanning(len(audio_data))
fft_data = np.abs(np.fft.rfft(audio_data * window_data))
frequencies = np.fft.rfftfreq(len(audio_data), d=1/SAMPLE_RATE)
detected_freq = frequencies[np.argmax(fft_data)]
sig_map = {FREQ_QUERY: "QUERY", FREQ_ALERT: "ALERT", FREQ_RESP_YES: "RESP_YES", FREQ_RESP_NO: "RESP_NO"}
for target_freq, sig_type in sig_map.items():
if abs(detected_freq - target_freq) <= 15:
return sig_type
return None
except:
return None

View file

@ -0,0 +1,21 @@
class Plugin:
def __init__(self):
self.name = "Raw Text Protocol"
def encode(self, text: str) -> bytes:
return text.encode('utf-8')
def decode(self, data: bytes) -> str:
return data.decode('utf-8')
def generate_service_signal(self, signal_type: str) -> bytes:
return f"SIG:{signal_type}".encode('utf-8')
def detect_service_signal(self, data: bytes) -> str:
try:
decoded = data.decode('utf-8')
if decoded.startswith("SIG:"):
return decoded.split(":", 1)[1]
return None
except:
return None

View file

@ -3,13 +3,11 @@ import base64
import numpy as np import numpy as np
import threading import threading
import sounddevice as sd import sounddevice as sd
import protocol
sio = socketio.Client() sio = socketio.Client()
_client_instance = None _client_instance = None
def init_network(client_instance): def init_network(client_instance):
"""Binds the network events to the active TUI client instance state."""
global _client_instance global _client_instance
_client_instance = client_instance _client_instance = client_instance
@ -19,76 +17,118 @@ def incoming_packet(data):
from_uin = data["from_uin"] from_uin = data["from_uin"]
payload_base64 = data["payload"] payload_base64 = data["payload"]
try: try:
audio_bytes = base64.b64decode(payload_base64.encode('utf-8')) raw_bytes = base64.b64decode(payload_base64.encode('utf-8'))
audio_data = np.frombuffer(audio_bytes, dtype=np.float32)
srv_tone = protocol.detect_service_tone(audio_data) if raw_bytes.startswith(b"SERVICE:"):
if srv_tone: parts = raw_bytes.split(b":", 2)
sd.play(audio_data, protocol.SAMPLE_RATE) proto_name = parts[1].decode('utf-8')
if from_uin not in _client_instance.contacts: sig_bytes = parts[2]
return
if srv_tone == protocol.FREQ_QUERY:
if _client_instance.is_busy:
_client_instance.send_service_tone(from_uin, protocol.FREQ_RESP_YES)
else:
_client_instance.pending_windows.append({"type": "query", "uin": from_uin})
_client_instance.refresh_ui()
return
elif srv_tone == protocol.FREQ_ALERT:
_client_instance.pending_windows.append({"type": "alert", "uin": from_uin})
_client_instance.refresh_ui()
return
elif srv_tone == protocol.FREQ_RESP_YES:
_client_instance.add_to_history(from_uin, f"[SYSTEM]: Quick status answer -> Yes, I'm busy")
return
elif srv_tone == protocol.FREQ_RESP_NO:
_client_instance.add_to_history(from_uin, f"[SYSTEM]: Quick status answer -> No, go on")
return
fast_text = protocol.fast_decode(audio_data)
if fast_text.startswith("SYS:"):
cmd = fast_text[4:]
if cmd.startswith("ROOM_MSG:"):
parts = cmd.split(":", 3)
if len(parts) >= 4:
room_id = parts[1]
sender_uin = parts[2]
actual_msg = parts[3]
if room_id in _client_instance.groups:
_client_instance.add_to_history(room_id, f"[UIN {sender_uin}]: {actual_msg}")
return
elif cmd == "REQ_ADD":
if from_uin in _client_instance.blocklist:
_client_instance.send_sys_packet(from_uin, "RES_DEC")
elif _client_instance.is_busy:
_client_instance.send_sys_packet(from_uin, "RES_BSY")
else:
if from_uin not in _client_instance.pending_requests and from_uin not in _client_instance.contacts:
_client_instance.pending_requests.append(from_uin)
_client_instance.refresh_ui()
return
elif cmd == "RES_ACC":
if _client_instance.dialing_uin == from_uin:
_client_instance.dialing_uin = None
if from_uin not in _client_instance.contacts:
_client_instance.contacts[from_uin] = {"status": "online", "unread": 0, "attention": False}
_client_instance.history[from_uin] = ["[SYSTEM]: Handshake accepted. Contact added."]
_client_instance.save_config()
_client_instance.active_chat = from_uin
_client_instance.refresh_ui()
return
elif cmd == "RES_DEC" or cmd == "RES_BSY":
if _client_instance.dialing_uin == from_uin:
_client_instance.dialing_uin = None
return
if from_uin not in _client_instance.contacts:
return
if _client_instance.active_chat != from_uin:
_client_instance.contacts[from_uin]["unread"] += 1
threading.Thread(target=_client_instance.play_and_decode, args=(audio_bytes, from_uin), daemon=True).start() sig_type = _client_instance.plugin_manager.safe_decode(proto_name, sig_bytes)
except Exception as e: if not sig_type:
sig_type = _client_instance.plugin_manager.protocols.get(proto_name, {}).get("instance").detect_service_signal(sig_bytes)
if sig_type:
if from_uin not in _client_instance.contacts: return
if sig_type == "QUERY":
if _client_instance.is_busy:
_client_instance.send_service_tone(from_uin, 1600)
else:
_client_instance.pending_windows.append({"type": "query", "uin": from_uin})
_client_instance.refresh_ui()
elif sig_type == "ALERT":
_client_instance.pending_windows.append({"type": "alert", "uin": from_uin})
_client_instance.refresh_ui()
elif sig_type == "RESP_YES":
if _client_instance.dialing_uin == from_uin:
_client_instance.dialing_uin = None
_client_instance.add_to_history(from_uin, "[SYSTEM]: Peer reports they are currently BUSY.")
elif sig_type == "RESP_NO":
if _client_instance.dialing_uin == from_uin:
_client_instance.dialing_uin = None
_client_instance.add_to_history(from_uin, "[SYSTEM]: Peer reports they are AVAILABLE.")
return
if raw_bytes.startswith(b"NATIVE:"):
parts = raw_bytes.split(b":", 2)
proto_name = parts[1].decode('utf-8')
enc_bytes = parts[2]
if not _client_instance.plugin_manager.protocols.get(proto_name, {}).get("enabled"):
return
decoded_text = _client_instance.plugin_manager.safe_decode(proto_name, enc_bytes)
if decoded_text is None:
return
_client_instance.peer_session_protocols[from_uin] = proto_name
if decoded_text.startswith("SYS:"):
cmd = decoded_text[4:]
if cmd == "REQ_ADD":
if from_uin not in _client_instance.contacts:
if from_uin not in _client_instance.pending_requests:
_client_instance.pending_requests.append(from_uin)
_client_instance.refresh_ui()
else:
_client_instance.send_sys_packet(from_uin, "RES_ACC")
return
elif cmd == "RES_ACC":
if _client_instance.dialing_uin == from_uin:
_client_instance.dialing_uin = None
if from_uin not in _client_instance.contacts:
_client_instance.contacts[from_uin] = {"status": "online", "unread": 0, "attention": False}
_client_instance.history[from_uin] = ["[SYSTEM]: System connection handshakes accepted and bound!"]
_client_instance.save_config()
_client_instance.active_chat = from_uin
_client_instance.refresh_ui()
return
elif cmd == "RES_DEC":
if _client_instance.dialing_uin == from_uin:
_client_instance.dialing_uin = None
return
elif cmd.startswith("REQ_JOIN_ROOM:"):
r_name = cmd.split(":", 1)[1]
r_id = f"ROOM:{_client_instance.uin}:{r_name}"
if r_id in _client_instance.groups:
if from_uin not in _client_instance.groups[r_id]["members"]:
_client_instance.groups[r_id]["members"].append(from_uin)
_client_instance.save_config()
_client_instance.send_sys_packet(from_uin, f"RES_JOIN_ROOM_OK:{r_name}:" + ",".join(_client_instance.groups[r_id]["members"]))
_client_instance.add_to_history(r_id, f"[SYSTEM]: User UIN {from_uin} joined the room sync list.")
return
elif cmd.startswith("RES_JOIN_ROOM_OK:"):
parts = cmd.split(":", 2)
r_name = parts[1]
m_list = parts[2].split(",")
r_id = f"ROOM:{from_uin}:{r_name}"
if r_id in _client_instance.groups:
_client_instance.groups[r_id]["members"] = m_list
_client_instance.save_config()
_client_instance.add_to_history(r_id, f"[SYSTEM]: Room synchronization complete. Members online: {len(m_list)}")
return
elif cmd.startswith("ROOM_MSG:"):
parts = cmd.split(":", 3)
r_id = parts[1]
sender_uin = parts[2]
msg_body = parts[3]
if r_id in _client_instance.groups:
_client_instance.add_to_history(r_id, f"[UIN {sender_uin}]: {msg_body}")
if _client_instance.uin == _client_instance.groups[r_id]["owner"]:
for m in _client_instance.groups[r_id]["members"]:
if m != _client_instance.uin and m != sender_uin and _client_instance.contacts.get(m, {}).get("status") == "online":
sio.emit("relay_packet", {"to_uin": m, "payload": payload_base64})
return
if from_uin not in _client_instance.contacts: return
if _client_instance.active_chat != from_uin:
_client_instance.contacts[from_uin]["unread"] += 1
sender_name = f"UIN {from_uin}"
_client_instance.add_to_history(from_uin, f"[{sender_name}]: {decoded_text}")
except:
pass pass
@sio.event @sio.event
@ -109,4 +149,5 @@ def error(data):
_client_instance.dialing_uin = None _client_instance.dialing_uin = None
if target in _client_instance.contacts: if target in _client_instance.contacts:
_client_instance.contacts[target]["status"] = "offline" _client_instance.contacts[target]["status"] = "offline"
_client_instance.add_to_history(target, f"[SYSTEM]: They went offline. Outgoing stack will be preserved.") _client_instance.add_to_history(target, "[SYSTEM]: Contact went offline. Transmission preserved.")
_client_instance.refresh_ui()

0
transports/__init__.py Normal file
View file

View file

@ -0,0 +1,31 @@
import socketio
class Plugin:
def __init__(self):
self.name = "Socket.IO Transport"
def connect(self, server_url: str) -> bool:
from socket_manager import sio
try:
if not sio.connected:
sio.connect(server_url, transports=['websocket'])
return True
except:
return False
def disconnect(self) -> None:
from socket_manager import sio
try:
sio.disconnect()
except:
pass
def send_packet(self, to_uin: str, payload_b64: str) -> bool:
from socket_manager import sio
try:
if sio.connected:
sio.emit("relay_packet", {"to_uin": to_uin, "payload": payload_b64})
return True
return False
except:
return False