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

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
venv

428
TUI.py
View file

@ -1,5 +1,27 @@
import sys
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 = {
"python-socketio[client]": "socketio",
@ -31,30 +53,6 @@ def 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
CONFIG_FILE = "settings.json"
@ -92,6 +90,64 @@ class CommandCompleter(Completer):
if cmd.startswith(text):
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:
def __init__(self):
self.server_url = ""
@ -114,6 +170,15 @@ class TyClient:
self.pending_windows = []
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):
if os.path.exists(CONFIG_FILE):
try:
@ -128,12 +193,32 @@ class TyClient:
self.history = cfg.get("history", {})
self.preserved = cfg.get("preserved", {})
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
except:
return False
return False
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 = {
"server_url": self.server_url,
"username": self.username,
@ -143,7 +228,11 @@ class TyClient:
"groups": self.groups,
"history": self.history,
"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:
json.dump(cfg, f, indent=4, ensure_ascii=False)
@ -151,9 +240,9 @@ class TyClient:
def play_dial_tones(self, target_uin):
self.dialing_uin = target_uin
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
sd.play(tone, protocol.SAMPLE_RATE)
sd.play(tone, 44100)
sd.wait()
for _ in range(30):
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):
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)
samples_per_tone = int(protocol.SAMPLE_RATE * protocol.TONE_DURATION)
current_sample = int(protocol.SAMPLE_RATE * 0.3)
samples_per_tone = int(44100 * 0.1)
current_sample = int(44100 * 0.3)
sender_name = self.username if sender_uin == self.uin else f"UIN {sender_uin}"
msg_buffer = f"[{sender_name}]: "
@ -181,9 +270,9 @@ class TyClient:
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/protocol.SAMPLE_RATE)
frequencies = np.fft.rfftfreq(len(chunk), d=1/44100)
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:
try:
char = chr(ascii_code)
@ -191,7 +280,7 @@ class TyClient:
self.refresh_ui()
except:
pass
time.sleep(protocol.TONE_DURATION)
time.sleep(0.1)
current_sample += samples_per_tone
sd.wait()
self.save_config()
@ -204,12 +293,38 @@ class TyClient:
self.save_config()
def send_sys_packet(self, to_uin, cmd):
audio_b = protocol.text_to_audio(f"SYS:{cmd}")
socket_manager.sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(audio_b).decode('utf-8')})
text_payload = f"SYS:{cmd}"
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):
audio_b = protocol.generate_service_tone(freq)
socket_manager.sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(audio_b).decode('utf-8')})
for p_name in proto_order:
encoded = self.plugin_manager.safe_encode(p_name, text_payload)
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):
if self.app:
@ -225,10 +340,28 @@ def status_checker_thread():
if client.contacts[uin]["status"] == "online" and client.preserved.get(uin):
while client.preserved[uin]:
p_text = client.preserved[uin].pop(0)
audio_b = protocol.text_to_audio(p_text)
p_b64 = base64.b64encode(audio_b).decode('utf-8')
socket_manager.sio.emit("relay_packet", {"to_uin": uin, "payload": p_b64})
client.add_to_history(uin, f"[You]: {p_text}")
proto_to_use = client.peer_session_protocols.get(uin, client.primary_protocol)
encoded = client.plugin_manager.safe_encode(proto_to_use, 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()
time.sleep(10)
@ -241,14 +374,14 @@ def make_layout():
elif info.get("unread", 0) > 0: status_str = f" ({info['unread']})"
elif info.get("status") == "online": status_str = " *"
content = f" {uin}{status_str}"
if client.active_chat == uin: style = "class:contact-active"
elif idx == client.selected_contact_idx and get_app().layout.has_focus(sidebar_window): style = "class:contact-focused"
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) and client.current_tab == "chat": style = "class:contact-focused"
else: style = "class:contact"
tokens.extend([(style, f"{content}\n")])
for g_id, g_info in client.groups.items():
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"
tokens.extend([(style, f"{content}\n")])
return tokens
@ -263,7 +396,7 @@ def make_layout():
("class:desc", "Messenger on custom protocol named\n"),
("class:desc", "AcoustiOverSocket inspired by rtty\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),
("class:help-tip", f"{DEV_SIGNATURE}\n")
])
@ -285,6 +418,45 @@ def make_layout():
tokens.append(("", line + "\n"))
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():
text = "".join(t[1] for t in get_main_text())
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")
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(
height=3,
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!")
input_field.text = ""
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')
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})
proto_to_use = client.primary_protocol
encoded = client.plugin_manager.safe_encode(proto_to_use, f"SYS:ROOM_MSG:{client.active_chat}:{client.uin}:{text}")
if encoded is None:
for p in client.plugin_manager.protocols:
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:
socket_manager.sio.emit("relay_packet", {"to_uin": owner_uin, "payload": p_b64})
client.add_to_history(client.active_chat, f"[You]: {text}")
client.add_to_history(client.active_chat, "[SYSTEM]: Local engine failure encoding text frame.")
input_field.text = ""
return
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...")
input_field.text = ""
return
if text.lower() == "/alert":
client.send_service_tone(client.active_chat, protocol.FREQ_ALERT)
client.add_to_history(client.active_chat, "[SYSTEM]: Sent URGENT alert signal (breaks DND)...")
client.send_service_tone(client.active_chat, 1400)
client.add_to_history(client.active_chat, "[SYSTEM]: Sent URGENT alert signal...")
input_field.text = ""
return
if len(text) > 300:
@ -417,20 +605,48 @@ def make_layout():
input_field.text = ""
return
audio_b = protocol.text_to_audio(text)
p_b64 = base64.b64encode(audio_b).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}")
proto_to_use = client.peer_session_protocols.get(client.active_chat, client.primary_protocol)
encoded = client.plugin_manager.safe_encode(proto_to_use, 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, 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:
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()
client.add_to_history(client.active_chat, "[SYSTEM]: Conversation pipeline failed. No working/enabled matching protocols.")
input_field.text = ""
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():
if not client.pending_requests: return []
@ -476,7 +692,7 @@ def make_layout():
def has_welcome_popup(): return client.show_welcome_popup
root_container = FloatContainer(
content=VSplit([sidebar_window, right_side]),
content=VSplit([sidebar_window, right_display]),
floats=[
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),
@ -486,12 +702,76 @@ def make_layout():
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):
if event.app.layout.has_focus(input_field): event.app.layout.focus(sidebar_window)
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):
all_chats = list(client.contacts.keys()) + list(client.groups.keys())
if all_chats:
@ -503,7 +783,7 @@ def make_layout():
client.contacts[target]["attention"] = False
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):
all_chats = list(client.contacts.keys()) + list(client.groups.keys())
if all_chats:
@ -542,7 +822,7 @@ def make_layout():
win_info = client.pending_windows.pop(0)
target_uin = win_info["uin"]
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.refresh_ui()
@ -551,7 +831,7 @@ def make_layout():
def _(event):
win_info = client.pending_windows.pop(0)
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.refresh_ui()
@ -610,8 +890,6 @@ def main():
except: pass
is_new_registration = False
# Initialize the cross-module link so network callbacks can manipulate local state
socket_manager.init_network(client)
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_NO = 1800
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()
class Plugin:
def __init__(self):
self.name = "Audio Protocol"
def text_to_audio(text):
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 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 detect_service_tone(audio_data):
try:
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:
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 = ""
@ -63,5 +45,52 @@ def fast_decode(audio_data):
text += chr(ascii_code)
current_sample += samples_per_tone
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 threading
import sounddevice as sd
import protocol
sio = socketio.Client()
_client_instance = None
def init_network(client_instance):
"""Binds the network events to the active TUI client instance state."""
global _client_instance
_client_instance = client_instance
@ -19,76 +17,118 @@ def incoming_packet(data):
from_uin = data["from_uin"]
payload_base64 = data["payload"]
try:
audio_bytes = base64.b64decode(payload_base64.encode('utf-8'))
audio_data = np.frombuffer(audio_bytes, dtype=np.float32)
raw_bytes = base64.b64decode(payload_base64.encode('utf-8'))
srv_tone = protocol.detect_service_tone(audio_data)
if srv_tone:
sd.play(audio_data, protocol.SAMPLE_RATE)
if from_uin not in _client_instance.contacts:
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
if raw_bytes.startswith(b"SERVICE:"):
parts = raw_bytes.split(b":", 2)
proto_name = parts[1].decode('utf-8')
sig_bytes = parts[2]
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)
sig_type = _client_instance.plugin_manager.safe_decode(proto_name, sig_bytes)
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()
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
elif sig_type == "ALERT":
_client_instance.pending_windows.append({"type": "alert", "uin": 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:
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 _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()
except Exception as e:
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
@sio.event
@ -109,4 +149,5 @@ def error(data):
_client_instance.dialing_uin = None
if target in _client_instance.contacts:
_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