diff --git a/client.py b/TUI.py similarity index 74% rename from client.py rename to TUI.py index 68eee50..10127ae 100644 --- a/client.py +++ b/TUI.py @@ -40,7 +40,6 @@ import threading import webbrowser import numpy as np import sounddevice as sd -import socketio from prompt_toolkit.application import Application from prompt_toolkit.key_binding import KeyBindings @@ -54,19 +53,14 @@ from prompt_toolkit.filters import has_focus, Condition from prompt_toolkit.data_structures import Point from prompt_toolkit.completion import Completer, Completion -BASE_FREQ = 600 -FREQ_STEP = 25 -TONE_DURATION = 0.1 -SAMPLE_RATE = 44100 +# Import decoupled local modules +import protocol +import socket_manager + CONFIG_FILE = "settings.json" WIKI_URL = "https://git.idkmail.ru/lohrrrr/TyChat-Client/wiki" DEV_SIGNATURE = "For you, from IDKMail Dev Group" -FREQ_QUERY = 1200 -FREQ_ALERT = 1400 -FREQ_RESP_YES = 1600 -FREQ_RESP_NO = 1800 - ASCII_ART = r""" _______ _____ _ _ |__ __| / ____| | | | @@ -78,8 +72,6 @@ ASCII_ART = r""" |___/ """ -sio = socketio.Client() - class CommandCompleter(Completer): def __init__(self): self.completions = { @@ -159,77 +151,21 @@ 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(SAMPLE_RATE * 1.0), False) + t = np.linspace(0, 1.0, int(protocol.SAMPLE_RATE * 1.0), False) tone = np.sin(425 * t * 2 * np.pi) * 0.5 - sd.play(tone, SAMPLE_RATE) + sd.play(tone, protocol.SAMPLE_RATE) sd.wait() for _ in range(30): if self.dialing_uin != target_uin or not self.loop_running: break time.sleep(0.1) - def generate_service_tone(self, 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(self, 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 detect_service_tone(self, 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(self, audio_data): - try: - 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 - except: - return "" - def play_and_decode(self, audio_bytes, sender_uin): audio_data = np.frombuffer(audio_bytes, dtype=np.float32) - sd.play(audio_data, SAMPLE_RATE) + sd.play(audio_data, protocol.SAMPLE_RATE) time.sleep(0.3) - samples_per_tone = int(SAMPLE_RATE * TONE_DURATION) - current_sample = int(SAMPLE_RATE * 0.3) + samples_per_tone = int(protocol.SAMPLE_RATE * protocol.TONE_DURATION) + current_sample = int(protocol.SAMPLE_RATE * 0.3) sender_name = self.username if sender_uin == self.uin else f"UIN {sender_uin}" msg_buffer = f"[{sender_name}]: " @@ -245,9 +181,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/SAMPLE_RATE) + frequencies = np.fft.rfftfreq(len(chunk), d=1/protocol.SAMPLE_RATE) detected_freq = frequencies[np.argmax(fft_data)] - ascii_code = int(round((detected_freq - BASE_FREQ) / FREQ_STEP)) + ascii_code = int(round((detected_freq - protocol.BASE_FREQ) / protocol.FREQ_STEP)) if 0 <= ascii_code < 65535: try: char = chr(ascii_code) @@ -255,7 +191,7 @@ class TyClient: self.refresh_ui() except: pass - time.sleep(TONE_DURATION) + time.sleep(protocol.TONE_DURATION) current_sample += samples_per_tone sd.wait() self.save_config() @@ -268,12 +204,12 @@ class TyClient: self.save_config() def send_sys_packet(self, to_uin, cmd): - audio_b = self.text_to_audio(f"SYS:{cmd}") - sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(audio_b).decode('utf-8')}) + 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')}) def send_service_tone(self, to_uin, freq): - audio_b = self.generate_service_tone(freq) - sio.emit("relay_packet", {"to_uin": to_uin, "payload": base64.b64encode(audio_b).decode('utf-8')}) + 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')}) def refresh_ui(self): if self.app: @@ -281,112 +217,17 @@ class TyClient: client = TyClient() -@sio.event -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) - - srv_tone = client.detect_service_tone(audio_data) - if srv_tone: - sd.play(audio_data, SAMPLE_RATE) - if from_uin not in client.contacts: - return - if srv_tone == FREQ_QUERY: - if client.is_busy: - client.send_service_tone(from_uin, FREQ_RESP_YES) - else: - client.pending_windows.append({"type": "query", "uin": from_uin}) - client.refresh_ui() - return - elif srv_tone == FREQ_ALERT: - client.pending_windows.append({"type": "alert", "uin": from_uin}) - client.refresh_ui() - return - elif srv_tone == FREQ_RESP_YES: - client.add_to_history(from_uin, f"[SYSTEM]: Quick status answer -> Yes, I'm busy") - return - elif srv_tone == FREQ_RESP_NO: - client.add_to_history(from_uin, f"[SYSTEM]: Quick status answer -> No, go on") - return - - fast_text = client.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.groups: - client.add_to_history(room_id, f"[UIN {sender_uin}]: {actual_msg}") - return - elif cmd == "REQ_ADD": - if from_uin in client.blocklist: - client.send_sys_packet(from_uin, "RES_DEC") - elif client.is_busy: - client.send_sys_packet(from_uin, "RES_BSY") - else: - if from_uin not in client.pending_requests and from_uin not in client.contacts: - client.pending_requests.append(from_uin) - client.refresh_ui() - return - elif cmd == "RES_ACC": - if client.dialing_uin == from_uin: - client.dialing_uin = None - if from_uin not in client.contacts: - client.contacts[from_uin] = {"status": "online", "unread": 0, "attention": False} - client.history[from_uin] = ["[SYSTEM]: Handshake accepted. Contact added."] - client.save_config() - client.active_chat = from_uin - client.refresh_ui() - return - elif cmd == "RES_DEC" or cmd == "RES_BSY": - if client.dialing_uin == from_uin: - client.dialing_uin = None - return - - if from_uin not in client.contacts: - return - if client.active_chat != from_uin: - client.contacts[from_uin]["unread"] += 1 - - threading.Thread(target=client.play_and_decode, args=(audio_bytes, from_uin), daemon=True).start() - except Exception as e: - pass - -@sio.event -def online_statuses_response(data): - for uin, status in data.items(): - if uin in client.contacts: - client.contacts[uin]["status"] = status - client.refresh_ui() - -@sio.event -def error(data): - msg = data.get("message") - target = data.get("target_uin") - if msg == "offline" and target: - if client.dialing_uin == target: - client.dialing_uin = None - if target in client.contacts: - client.contacts[target]["status"] = "offline" - client.add_to_history(target, f"[SYSTEM]: They went offline. Outgoing stack will be preserved.") - def status_checker_thread(): while client.loop_running: - if sio.connected and client.contacts: - sio.emit("check_online_statuses", list(client.contacts.keys())) + if socket_manager.sio.connected and client.contacts: + socket_manager.sio.emit("check_online_statuses", list(client.contacts.keys())) for uin in list(client.contacts.keys()): if client.contacts[uin]["status"] == "online" and client.preserved.get(uin): while client.preserved[uin]: p_text = client.preserved[uin].pop(0) - audio_b = client.text_to_audio(p_text) + audio_b = protocol.text_to_audio(p_text) p_b64 = base64.b64encode(audio_b).decode('utf-8') - sio.emit("relay_packet", {"to_uin": uin, "payload": p_b64}) + socket_manager.sio.emit("relay_packet", {"to_uin": uin, "payload": p_b64}) client.add_to_history(uin, f"[You]: {p_text}") client.save_config() time.sleep(10) @@ -477,7 +318,7 @@ def make_layout(): if text.lower() == "/exit": client.loop_running = False - sio.disconnect() + socket_manager.sio.disconnect() get_app().exit() return @@ -549,25 +390,25 @@ 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 = client.text_to_audio(f"SYS:ROOM_MSG:{client.active_chat}:{client.uin}:{text}") + 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": - sio.emit("relay_packet", {"to_uin": m, "payload": p_b64}) + socket_manager.sio.emit("relay_packet", {"to_uin": m, "payload": p_b64}) else: - sio.emit("relay_packet", {"to_uin": owner_uin, "payload": p_b64}) + socket_manager.sio.emit("relay_packet", {"to_uin": owner_uin, "payload": p_b64}) client.add_to_history(client.active_chat, f"[You]: {text}") input_field.text = "" return if text.lower() == "/query": - client.send_service_tone(client.active_chat, FREQ_QUERY) + client.send_service_tone(client.active_chat, protocol.FREQ_QUERY) 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, FREQ_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)...") input_field.text = "" return @@ -576,10 +417,10 @@ def make_layout(): input_field.text = "" return - audio_b = client.text_to_audio(text) + audio_b = protocol.text_to_audio(text) p_b64 = base64.b64encode(audio_b).decode('utf-8') if client.contacts[client.active_chat]["status"] == "online": - sio.emit("relay_packet", {"to_uin": client.active_chat, "payload": p_b64}) + 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] = [] @@ -701,7 +542,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, FREQ_RESP_YES) + client.send_service_tone(target_uin, protocol.FREQ_RESP_YES) client.add_to_history(target_uin, "[You -> SYSTEM]: Sent Dial Tone -> Yes, I'm busy.") client.refresh_ui() @@ -710,7 +551,7 @@ def make_layout(): def _(event): win_info = client.pending_windows.pop(0) target_uin = win_info["uin"] - client.send_service_tone(target_uin, FREQ_RESP_NO) + client.send_service_tone(target_uin, protocol.FREQ_RESP_NO) client.add_to_history(target_uin, "[You -> SYSTEM]: Sent Dial Tone -> No, go on.") client.refresh_ui() @@ -735,7 +576,7 @@ def make_layout(): @kb.add('c-c') def _(event): client.loop_running = False - sio.disconnect() + socket_manager.sio.disconnect() event.app.exit() return Layout(root_container, focused_element=input_field), kb @@ -770,6 +611,9 @@ def main(): 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(): print("!" * 60) print("WARNING: All data is local.") @@ -779,7 +623,7 @@ def main(): if not client.server_url.startswith("http://") and not client.server_url.startswith("https://"): client.server_url = "http://" + client.server_url - try: sio.connect(client.server_url, transports=['websocket']) + try: socket_manager.sio.connect(client.server_url, transports=['websocket']) except Exception as e: return print("\n1. Register\n2. Login") @@ -789,7 +633,7 @@ def main(): event_wait = threading.Event() - @sio.event + @socket_manager.sio.event def register_response(data): nonlocal is_new_registration if data["status"] == "success": @@ -799,7 +643,7 @@ def main(): is_new_registration = True event_wait.set() - @sio.event + @socket_manager.sio.event def login_response(data): if data["status"] == "success": client.uin = username_or_uin @@ -807,8 +651,8 @@ def main(): client.password = password event_wait.set() - if mode == "1": sio.emit("register", {"username": username_or_uin, "password": password}) - else: sio.emit("login", {"uin": username_or_uin, "password": password}) + if mode == "1": socket_manager.sio.emit("register", {"username": username_or_uin, "password": password}) + else: socket_manager.sio.emit("login", {"uin": username_or_uin, "password": password}) event_wait.wait() if not client.uin: return @@ -816,8 +660,8 @@ def main(): input("Press Enter to open TUI...") else: try: - sio.connect(client.server_url, transports=['websocket']) - sio.emit("login", {"uin": client.uin, "password": client.password}) + socket_manager.sio.connect(client.server_url, transports=['websocket']) + socket_manager.sio.emit("login", {"uin": client.uin, "password": client.password}) except: pass if is_new_registration: @@ -836,4 +680,4 @@ def main(): client.app.run() if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/compile.py b/compile.py new file mode 100644 index 0000000..b8e9022 --- /dev/null +++ b/compile.py @@ -0,0 +1,40 @@ +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() \ No newline at end of file diff --git a/protocol.py b/protocol.py new file mode 100644 index 0000000..fa07b06 --- /dev/null +++ b/protocol.py @@ -0,0 +1,67 @@ +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 + +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): + 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: + 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 + except: + return "" \ No newline at end of file diff --git a/socket_manager.py b/socket_manager.py new file mode 100644 index 0000000..7bd5733 --- /dev/null +++ b/socket_manager.py @@ -0,0 +1,112 @@ +import socketio +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 + +@sio.event +def incoming_packet(data): + if not _client_instance: return + 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) + + 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 + + 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() + except Exception as e: + pass + +@sio.event +def online_statuses_response(data): + if not _client_instance: return + for uin, status in data.items(): + if uin in _client_instance.contacts: + _client_instance.contacts[uin]["status"] = status + _client_instance.refresh_ui() + +@sio.event +def error(data): + if not _client_instance: return + msg = data.get("message") + target = data.get("target_uin") + if msg == "offline" and target: + if _client_instance.dialing_uin == target: + _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.") \ No newline at end of file