Created
June 9, 2026 09:09
-
-
Save Hammer2900/12fc5fe4582f7f806b0a52214d444bd4 to your computer and use it in GitHub Desktop.
fix to py_ipfs_node
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import signal | |
| import os | |
| import sys | |
| import signal | |
| if hasattr(signal, 'pthread_sigmask'): | |
| try: | |
| signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGPIPE}) | |
| except Exception: | |
| pass | |
| try: | |
| import resource | |
| soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) | |
| target = 65535 | |
| if hard != resource.RLIM_INFINITY and hard < target: | |
| target = hard | |
| resource.setrlimit(resource.RLIMIT_NOFILE, (target, target)) | |
| except Exception: | |
| pass | |
| import json | |
| import time | |
| import tempfile | |
| import threading | |
| import queue | |
| import hashlib | |
| import tkinter as tk | |
| from tkinter import messagebox, scrolledtext, simpledialog | |
| from typing import Optional, Dict, Any | |
| from cryptography.fernet import Fernet | |
| import base64 | |
| from cryptography.hazmat.primitives import hashes | |
| from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC | |
| from ipfs_node import IpfsNode | |
| ADMIN_SECRET_HASH = hashlib.sha256(b'super_admin_secret_2024').hexdigest() | |
| BG = '#1a1b26' | |
| BG2 = '#16161e' | |
| BG3 = '#1f2030' | |
| ACCENT = '#7aa2f7' | |
| ACCENT2 = '#bb9af7' | |
| GREEN = '#9ece6a' | |
| RED = '#f7768e' | |
| YELLOW = '#e0af68' | |
| FG = '#c0caf5' | |
| FG_DIM = '#565f89' | |
| FG_SYSTEM = '#73daca' | |
| BORDER = '#292e42' | |
| class P2PDagSecureChat: | |
| def __init__(self, nickname: str, topic: str, password: str, admin_secret: str, gui_queue: queue.Queue): | |
| self.nickname = nickname | |
| self.topic = topic | |
| self.gui_queue = gui_queue | |
| self.is_admin = hashlib.sha256(admin_secret.encode()).hexdigest() == ADMIN_SECRET_HASH | |
| self.admin_proof = hashlib.sha256(admin_secret.encode()).hexdigest() if self.is_admin else '' | |
| salt = b'ipfs_secure_salt_fixed' | |
| kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=100000) | |
| key = base64.urlsafe_b64encode(kdf.derive(password.encode())) | |
| self._fernet = Fernet(key) | |
| self.node: Optional[IpfsNode] = None | |
| self.latest_msg_cid: Optional[str] = None | |
| self.known_messages: Dict[str, Dict[str, Any]] = {} | |
| self.banned_users: set = set() | |
| self.online_users: Dict[str, float] = {} | |
| self._running = False | |
| self._receive_thread = None | |
| self._heartbeat_thread = None | |
| def start(self): | |
| self.gui_queue.put({'type': 'system', 'text': 'Запуск ноды Kubo IPFS...'}) | |
| self.node = IpfsNode.ephemeral().__enter__() | |
| self._running = True | |
| self.gui_queue.put( | |
| { | |
| 'type': 'system', | |
| 'text': f"Нода запущена. PeerID: {self.node.peer_id}\nПодписка на топик '{self.topic}'...", | |
| } | |
| ) | |
| self.online_users[self.nickname] = time.time() | |
| self._receive_thread = threading.Thread(target=self._listen_pubsub_loop, daemon=True) | |
| self._heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True) | |
| self._receive_thread.start() | |
| self._heartbeat_thread.start() | |
| self._broadcast_join() | |
| def _heartbeat_loop(self): | |
| for _ in range(5): | |
| if not self._running: | |
| return | |
| time.sleep(3) | |
| if self._running: | |
| self._broadcast_presence() | |
| self._prune_offline_users() | |
| while self._running: | |
| time.sleep(15) | |
| if self._running: | |
| self._broadcast_presence() | |
| self._prune_offline_users() | |
| def _broadcast_presence(self): | |
| self._send_control({'msg_type': 'presence', 'sender': self.nickname}) | |
| if self.node: | |
| try: | |
| peers = self.node.swarm.peers() | |
| peer_count = len(peers) if peers else 0 | |
| self.gui_queue.put({'type': 'status_peers', 'count': peer_count}) | |
| except Exception: | |
| pass | |
| def _broadcast_join(self): | |
| self._send_control({'msg_type': 'join', 'sender': self.nickname}) | |
| def _broadcast_leave(self): | |
| self._send_control({'msg_type': 'leave', 'sender': self.nickname}) | |
| def _send_control(self, payload: dict): | |
| """Отправляет служебное сообщение без сохранения в историю.""" | |
| if not self.node: | |
| return | |
| payload['timestamp'] = time.time() | |
| try: | |
| serialized = json.dumps(payload).encode() | |
| encrypted = self._fernet.encrypt(serialized) | |
| with tempfile.TemporaryDirectory() as d: | |
| p = os.path.join(d, 'ctrl.bin') | |
| with open(p, 'wb') as f: | |
| f.write(encrypted) | |
| cid = self.node.files.publish(p) | |
| self.node.pubsub.publish(self.topic, cid) | |
| except Exception: | |
| pass | |
| def _prune_offline_users(self): | |
| """Удаляет из списка тех, от кого давно не было сигналов (таймаут 45 сек).""" | |
| now = time.time() | |
| timeout = 45.0 | |
| stale_users = [] | |
| for nick, last_seen in list(self.online_users.items()): | |
| if nick == self.nickname: | |
| continue | |
| if now - last_seen > timeout: | |
| stale_users.append(nick) | |
| if stale_users: | |
| for nick in stale_users: | |
| self.online_users.pop(nick, None) | |
| self.gui_queue.put({'type': 'event', 'text': f'✦ {nick} пропал из сети (таймаут)'}) | |
| self.gui_queue.put({'type': 'users', 'users': list(self.online_users.keys())}) | |
| def _listen_pubsub_loop(self): | |
| while self._running: | |
| try: | |
| with self.node.pubsub.subscribe(self.topic) as sub: | |
| self.gui_queue.put({'type': 'system', 'text': f'Успешно подписались на топик {self.topic}'}) | |
| while self._running: | |
| try: | |
| message = sub.next_message(timeout=1.0) | |
| if not message: | |
| continue | |
| cid = message.data.decode().strip() | |
| if cid not in self.known_messages: | |
| self._process_new_cid(cid) | |
| except Exception as e: | |
| err_msg = str(e).lower() | |
| if 'timeout' in err_msg or 'deadline exceeded' in err_msg: | |
| continue | |
| raise e | |
| except Exception as e: | |
| if not self._running: | |
| break | |
| self.gui_queue.put( | |
| {'type': 'system', 'text': f'Сбой подписки PubSub: {e}. Повторное подключение через 3 сек...'} | |
| ) | |
| time.sleep(3.0) | |
| def _process_new_cid(self, cid: str, depth: int = 0): | |
| """ | |
| Инициализирует фоновую загрузку и обработку нового CID. | |
| Этот метод выполняется мгновенно и не блокирует вызывающий поток. | |
| """ | |
| if not hasattr(self, '_downloading_cids'): | |
| self._downloading_cids = set() | |
| if cid in self.known_messages or cid in self._downloading_cids: | |
| return | |
| if depth > 5: | |
| return | |
| self._downloading_cids.add(cid) | |
| threading.Thread(target=self._download_and_process_worker, args=(cid, depth), daemon=True).start() | |
| def _download_and_process_worker(self, cid: str, depth: int): | |
| """ | |
| Фоновый воркер: скачивает зашифрованный файл из IPFS, дешифрует | |
| и обновляет состояние чата/интерфейса. | |
| """ | |
| try: | |
| with tempfile.TemporaryDirectory() as d: | |
| p = os.path.join(d, 'enc.bin') | |
| self.node.files.download(cid, p) | |
| with open(p, 'rb') as f: | |
| enc = f.read() | |
| payload = json.loads(self._fernet.decrypt(enc).decode()) | |
| self.known_messages[cid] = payload | |
| mtype = payload.get('msg_type', 'chat') | |
| sender = payload.get('sender', '?') | |
| now = time.time() | |
| if mtype == 'presence': | |
| self._update_user(sender, now) | |
| elif mtype == 'join': | |
| was_online = sender in self.online_users | |
| self._update_user(sender, now) | |
| if sender != self.nickname: | |
| if not was_online: | |
| self.gui_queue.put({'type': 'event', 'text': f'✦ {sender} подключился'}) | |
| self._broadcast_presence() | |
| elif mtype == 'leave': | |
| self.online_users.pop(sender, None) | |
| self.gui_queue.put({'type': 'users', 'users': list(self.online_users.keys())}) | |
| if sender != self.nickname: | |
| self.gui_queue.put({'type': 'event', 'text': f'✦ {sender} отключился'}) | |
| elif mtype == 'ban': | |
| if payload.get('admin_proof') == ADMIN_SECRET_HASH: | |
| target = payload.get('target', '') | |
| self.banned_users.add(target) | |
| self.gui_queue.put({'type': 'system', 'text': f'🚫 Администратор заблокировал: {target}'}) | |
| self.gui_queue.put({'type': 'users', 'users': list(self.online_users.keys())}) | |
| else: | |
| self._update_user(sender, now) | |
| if sender not in self.banned_users: | |
| target = payload.get('target') | |
| if not target or target == self.nickname or sender == self.nickname: | |
| self.gui_queue.put( | |
| { | |
| 'type': 'chat', | |
| 'sender': sender, | |
| 'text': payload['text'], | |
| 'is_private': bool(target), | |
| } | |
| ) | |
| parent = payload.get('parent_cid') | |
| if parent: | |
| self._process_new_cid(parent, depth + 1) | |
| self.latest_msg_cid = cid | |
| except Exception: | |
| pass | |
| finally: | |
| if hasattr(self, '_downloading_cids'): | |
| self._downloading_cids.discard(cid) | |
| def _update_user(self, nick: str, ts: float): | |
| self.online_users[nick] = ts | |
| self.gui_queue.put({'type': 'users', 'users': list(self.online_users.keys())}) | |
| def send_message(self, text: str, target: str = None, msg_type: str = 'chat'): | |
| if not self.node: | |
| return | |
| payload = { | |
| 'sender': self.nickname, | |
| 'text': text, | |
| 'timestamp': time.time(), | |
| 'parent_cid': self.latest_msg_cid, | |
| 'msg_type': msg_type, | |
| 'target': target, | |
| 'admin_proof': self.admin_proof if msg_type == 'ban' else '', | |
| } | |
| serialized = json.dumps(payload).encode() | |
| encrypted = self._fernet.encrypt(serialized) | |
| with tempfile.TemporaryDirectory() as d: | |
| p = os.path.join(d, 'msg.bin') | |
| with open(p, 'wb') as f: | |
| f.write(encrypted) | |
| cid = self.node.files.publish(p) | |
| self.known_messages[cid] = payload | |
| self.latest_msg_cid = cid | |
| self.node.pubsub.publish(self.topic, cid) | |
| def stop(self): | |
| self._running = False | |
| try: | |
| self._broadcast_leave() | |
| time.sleep(1.2) | |
| except Exception: | |
| pass | |
| if self.node: | |
| try: | |
| self.node.__exit__(None, None, None) | |
| except Exception: | |
| pass | |
| class ChatAppGUI: | |
| def __init__(self, root: tk.Tk, profile_id: str): | |
| self.root = root | |
| self.profile_id = profile_id | |
| self.config_dir = f'config_profile_{profile_id}' | |
| self.config_path = os.path.join(self.config_dir, 'settings.json') | |
| os.makedirs(self.config_dir, exist_ok=True) | |
| self.gui_queue: queue.Queue = queue.Queue() | |
| self.chat_core: Optional[P2PDagSecureChat] = None | |
| self._selected_user: Optional[str] = None | |
| self.root.title(f'IPFS Secure Chat — профиль {profile_id}') | |
| self.root.geometry('820x560') | |
| self.root.configure(bg=BG) | |
| self.root.resizable(True, True) | |
| self.saved_config = {} | |
| if os.path.exists(self.config_path): | |
| try: | |
| with open(self.config_path, 'r') as f: | |
| self.saved_config = json.load(f) | |
| except Exception: | |
| pass | |
| self._first_run = not bool(self.saved_config) | |
| if self._first_run: | |
| self._build_login_ui(ask_admin=True) | |
| else: | |
| self._build_login_ui(ask_admin=False) | |
| self.root.after(100, self._process_queue) | |
| def _build_login_ui(self, ask_admin: bool): | |
| self._login_frame = tk.Frame(self.root, bg=BG, padx=40, pady=30) | |
| self._login_frame.place(relx=0.5, rely=0.5, anchor='center') | |
| tk.Label( | |
| self._login_frame, text='IPFS Secure Chat', font=('JetBrains Mono', 18, 'bold'), fg=ACCENT, bg=BG | |
| ).pack(pady=(0, 4)) | |
| tk.Label( | |
| self._login_frame, text=f'Профиль {self.profile_id}', font=('JetBrains Mono', 9), fg=FG_DIM, bg=BG | |
| ).pack(pady=(0, 20)) | |
| def _field(label, show=None): | |
| tk.Label(self._login_frame, text=label, font=('JetBrains Mono', 9), fg=FG_DIM, bg=BG, anchor='w').pack( | |
| fill='x' | |
| ) | |
| e = tk.Entry( | |
| self._login_frame, | |
| font=('JetBrains Mono', 11), | |
| bg=BG3, | |
| fg=FG, | |
| insertbackground=FG, | |
| relief='flat', | |
| bd=6, | |
| show=show if show else '', | |
| ) | |
| e.pack(fill='x', pady=(2, 10), ipady=4) | |
| return e | |
| self._nick_entry = _field('Никнейм') | |
| self._pass_entry = _field('Пароль workspace', show='•') | |
| saved_nick = self.saved_config.get('nickname', '') | |
| saved_pass = self.saved_config.get('workspace_password', '') | |
| if saved_nick: | |
| self._nick_entry.insert(0, saved_nick) | |
| if saved_pass: | |
| self._pass_entry.insert(0, saved_pass) | |
| if ask_admin: | |
| self._admin_entry = _field('Admin-секрет (только при первом запуске)', show='•') | |
| saved_admin = self.saved_config.get('admin_secret', '') | |
| if saved_admin: | |
| self._admin_entry.insert(0, saved_admin) | |
| else: | |
| self._admin_entry = None | |
| btn = tk.Button( | |
| self._login_frame, | |
| text='Войти', | |
| font=('JetBrains Mono', 11, 'bold'), | |
| bg=ACCENT, | |
| fg=BG, | |
| activebackground=ACCENT2, | |
| relief='flat', | |
| cursor='hand2', | |
| bd=0, | |
| command=self._on_login, | |
| ) | |
| btn.pack(fill='x', pady=(6, 0), ipady=6) | |
| self.root.bind('<Return>', lambda _: self._on_login()) | |
| def _on_login(self): | |
| nick = self._nick_entry.get().strip() | |
| pwd = self._pass_entry.get().strip() | |
| if not nick or not pwd: | |
| messagebox.showerror('Ошибка', 'Никнейм и пароль обязательны!') | |
| return | |
| admin_secret = ( | |
| self._admin_entry.get().strip() if self._admin_entry else self.saved_config.get('admin_secret', '') | |
| ) | |
| config = { | |
| 'nickname': nick, | |
| 'workspace_password': pwd, | |
| 'topic': self.saved_config.get('topic', 'my-ipfs-secure-workflow-v1'), | |
| 'admin_secret': admin_secret, | |
| } | |
| with open(self.config_path, 'w') as f: | |
| json.dump(config, f) | |
| self._login_frame.destroy() | |
| self.root.unbind('<Return>') | |
| self._build_chat_ui() | |
| self._start_core(config) | |
| def _build_chat_ui(self): | |
| top = tk.Frame(self.root, bg=BG2, pady=6, padx=14) | |
| top.pack(fill='x', side='top') | |
| tk.Label(top, text='IPFS Secure Chat', font=('JetBrains Mono', 11, 'bold'), fg=ACCENT, bg=BG2).pack(side='left') | |
| self._status_lbl = tk.Label(top, text='● подключение...', font=('JetBrains Mono', 9), fg=YELLOW, bg=BG2) | |
| self._status_lbl.pack(side='right', padx=8) | |
| body = tk.Frame(self.root, bg=BG) | |
| body.pack(fill='both', expand=True) | |
| msg_frame = tk.Frame(body, bg=BG) | |
| msg_frame.pack(side='left', fill='both', expand=True, padx=(8, 0), pady=8) | |
| self._history = scrolledtext.ScrolledText( | |
| msg_frame, | |
| wrap='word', | |
| state='disabled', | |
| font=('JetBrains Mono', 10), | |
| bg=BG3, | |
| fg=FG, | |
| insertbackground=FG, | |
| relief='flat', | |
| bd=0, | |
| selectbackground=BORDER, | |
| selectforeground=FG, | |
| padx=10, | |
| pady=8, | |
| ) | |
| self._history.pack(fill='both', expand=True) | |
| self._history.tag_config('system', foreground=FG_SYSTEM) | |
| self._history.tag_config('event', foreground=YELLOW) | |
| self._history.tag_config('private', foreground=ACCENT2) | |
| self._history.tag_config('nick', foreground=ACCENT) | |
| self._history.tag_config('self', foreground=GREEN) | |
| self._history.tag_config('dim', foreground=FG_DIM) | |
| self._history.tag_config('error', foreground=RED) | |
| right = tk.Frame(body, bg=BG2, width=175) | |
| right.pack(side='right', fill='y', padx=8, pady=8) | |
| right.pack_propagate(False) | |
| tk.Label(right, text='Онлайн', font=('JetBrains Mono', 9, 'bold'), fg=FG_DIM, bg=BG2).pack( | |
| pady=(8, 4), anchor='w', padx=8 | |
| ) | |
| self._user_list = tk.Listbox( | |
| right, | |
| font=('JetBrains Mono', 10), | |
| bg=BG2, | |
| fg=FG, | |
| selectbackground=BORDER, | |
| selectforeground=ACCENT, | |
| relief='flat', | |
| bd=0, | |
| activestyle='none', | |
| highlightthickness=0, | |
| ) | |
| self._user_list.pack(fill='both', expand=True, padx=6) | |
| self._user_list.bind('<<ListboxSelect>>', self._on_user_select) | |
| btn_frame = tk.Frame(right, bg=BG2) | |
| btn_frame.pack(fill='x', padx=6, pady=6) | |
| self._pm_btn = tk.Button( | |
| btn_frame, | |
| text='✉ PM', | |
| font=('JetBrains Mono', 9), | |
| bg=ACCENT, | |
| fg=BG, | |
| activebackground=ACCENT2, | |
| relief='flat', | |
| cursor='hand2', | |
| bd=0, | |
| state='disabled', | |
| command=self._do_pm, | |
| ) | |
| self._pm_btn.pack(fill='x', pady=(0, 4), ipady=3) | |
| self._ban_btn = tk.Button( | |
| btn_frame, | |
| text='🚫 Бан', | |
| font=('JetBrains Mono', 9), | |
| bg=BG3, | |
| fg=RED, | |
| activebackground=BG3, | |
| relief='flat', | |
| cursor='hand2', | |
| bd=0, | |
| state='disabled', | |
| command=self._do_ban, | |
| ) | |
| self._ban_btn.pack(fill='x', ipady=3) | |
| inp_frame = tk.Frame(self.root, bg=BG2, padx=8, pady=8) | |
| inp_frame.pack(fill='x', side='bottom') | |
| self._msg_entry = tk.Entry( | |
| inp_frame, | |
| font=('JetBrains Mono', 11), | |
| bg=BG3, | |
| fg=FG, | |
| insertbackground=FG, | |
| relief='flat', | |
| bd=6, | |
| ) | |
| self._msg_entry.pack(side='left', fill='x', expand=True, ipady=5, padx=(0, 8)) | |
| self._msg_entry.bind('<Return>', lambda _: self._send_message()) | |
| send_btn = tk.Button( | |
| inp_frame, | |
| text='Отправить', | |
| font=('JetBrains Mono', 10, 'bold'), | |
| bg=ACCENT, | |
| fg=BG, | |
| activebackground=ACCENT2, | |
| relief='flat', | |
| cursor='hand2', | |
| bd=0, | |
| command=self._send_message, | |
| ) | |
| send_btn.pack(side='right', ipady=5, ipadx=12) | |
| def _on_user_select(self, _evt=None): | |
| sel = self._user_list.curselection() | |
| if not sel: | |
| self._selected_user = None | |
| self._pm_btn.config(state='disabled') | |
| self._ban_btn.config(state='disabled') | |
| return | |
| selected_raw = self._user_list.get(sel[0]) | |
| nick = selected_raw.replace('★ ', '').replace(' ', '').strip() | |
| if nick == (self.chat_core.nickname if self.chat_core else ''): | |
| self._selected_user = None | |
| self._pm_btn.config(state='disabled') | |
| self._ban_btn.config(state='disabled') | |
| return | |
| self._selected_user = nick | |
| self._pm_btn.config(state='normal') | |
| is_admin = self.chat_core and self.chat_core.is_admin | |
| self._ban_btn.config(state='normal' if is_admin else 'disabled') | |
| def _do_pm(self): | |
| if not self._selected_user or not self.chat_core: | |
| return | |
| target = self._selected_user | |
| text = simpledialog.askstring( | |
| 'Приватное сообщение', | |
| f'Сообщение для {target}:', | |
| parent=self.root, | |
| ) | |
| if text and text.strip(): | |
| self.chat_core.send_message(text.strip(), target=target) | |
| self._append(f'[PM → {target}] ', 'dim') | |
| self._append(text.strip() + '\n', 'private') | |
| def _do_ban(self): | |
| if not self._selected_user or not self.chat_core: | |
| return | |
| if not self.chat_core.is_admin: | |
| messagebox.showerror('Ошибка', 'Нет прав администратора') | |
| return | |
| target = self._selected_user | |
| if messagebox.askyesno('Подтверждение', f'Заблокировать {target}?'): | |
| self.chat_core.send_message( | |
| f'Пользователь {target} заблокирован.', | |
| target=target, | |
| msg_type='ban', | |
| ) | |
| self._append(f'🚫 Вы заблокировали {target}\n', 'error') | |
| def _start_core(self, config: dict): | |
| self.chat_core = P2PDagSecureChat( | |
| nickname=config['nickname'], | |
| topic=config['topic'], | |
| password=config['workspace_password'], | |
| admin_secret=config.get('admin_secret', ''), | |
| gui_queue=self.gui_queue, | |
| ) | |
| threading.Thread(target=self.chat_core.start, daemon=True).start() | |
| def _process_queue(self): | |
| try: | |
| while True: | |
| msg = self.gui_queue.get_nowait() | |
| mtype = msg.get('type') | |
| if mtype == 'system': | |
| self._append(f'[sys] {msg["text"]}\n', 'system') | |
| elif mtype == 'status_peers': | |
| count = msg.get('count', 0) | |
| if hasattr(self, '_status_lbl'): | |
| self._status_lbl.config(text=f'● онлайн (пиров: {count})', fg=GREEN if count > 0 else YELLOW) | |
| elif mtype == 'event': | |
| self._append(msg['text'] + '\n', 'event') | |
| elif mtype == 'chat': | |
| sender = msg['sender'] | |
| me = self.chat_core.nickname if self.chat_core else '' | |
| if msg.get('is_private'): | |
| self._append(f'[PM] ', 'dim') | |
| tag = 'self' if sender == me else 'private' | |
| self._append(f'<{sender}>', tag) | |
| self._append(f': {msg["text"]}\n', 'dim') | |
| else: | |
| tag = 'self' if sender == me else 'nick' | |
| self._append(f'<{sender}>', tag) | |
| self._append(f': {msg["text"]}\n') | |
| elif mtype == 'users': | |
| self._refresh_user_list(msg['users']) | |
| except queue.Empty: | |
| pass | |
| finally: | |
| self.root.after(100, self._process_queue) | |
| def _append(self, text: str, tag: str = ''): | |
| self._history.config(state='normal') | |
| if tag: | |
| self._history.insert('end', text, tag) | |
| else: | |
| self._history.insert('end', text) | |
| self._history.config(state='disabled') | |
| self._history.yview('end') | |
| def _refresh_user_list(self, users: list): | |
| if not hasattr(self, '_user_list'): | |
| return | |
| sel = self._user_list.curselection() | |
| sel_val = None | |
| if sel: | |
| raw_sel = self._user_list.get(sel[0]) | |
| sel_val = raw_sel.replace('★ ', '').replace(' ', '').strip() | |
| self._user_list.delete(0, 'end') | |
| me = self.chat_core.nickname if self.chat_core else '' | |
| for u in sorted(users): | |
| label = f'★ {u}' if u == me else f' {u}' | |
| self._user_list.insert('end', label) | |
| if sel_val: | |
| for i in range(self._user_list.size()): | |
| raw_item = self._user_list.get(i) | |
| item_clean = raw_item.replace('★ ', '').replace(' ', '').strip() | |
| if item_clean == sel_val: | |
| self._user_list.selection_set(i) | |
| break | |
| self._on_user_select() | |
| def _send_message(self): | |
| if not self.chat_core: | |
| return | |
| text = self._msg_entry.get().strip() | |
| if not text: | |
| return | |
| self._msg_entry.delete(0, 'end') | |
| self.chat_core.send_message(text) | |
| self._append(f'<{self.chat_core.nickname}>', 'self') | |
| self._append(f': {text}\n') | |
| def on_closing(self): | |
| if self.chat_core: | |
| self.chat_core.stop() | |
| self.root.destroy() | |
| os._exit(0) | |
| def run_server(profile_id: str): | |
| print(f'Сервер, профиль {profile_id}...') | |
| config_path = os.path.join(f'config_profile_{profile_id}', 'settings.json') | |
| if not os.path.exists(config_path): | |
| print('Ошибка: сначала создайте конфиг через GUI') | |
| return | |
| with open(config_path) as f: | |
| config = json.load(f) | |
| q = queue.Queue() | |
| core = P2PDagSecureChat( | |
| nickname=config['nickname'] + '_Server', | |
| topic=config['topic'], | |
| password=config['workspace_password'], | |
| admin_secret=config.get('admin_secret', ''), | |
| gui_queue=q, | |
| ) | |
| core.start() | |
| try: | |
| while True: | |
| try: | |
| msg = q.get(timeout=1.0) | |
| if msg['type'] == 'system': | |
| print(f'[SYS] {msg["text"]}') | |
| elif msg['type'] == 'chat': | |
| print(f'[MSG] <{msg["sender"]}>: {msg["text"]}') | |
| elif msg['type'] == 'event': | |
| print(f'[EVT] {msg["text"]}') | |
| except queue.Empty: | |
| pass | |
| except KeyboardInterrupt: | |
| core.stop() | |
| print('Остановлено.') | |
| if __name__ == '__main__': | |
| profile_id = sys.argv[1] if len(sys.argv) > 1 else '1' | |
| mode = sys.argv[2] if len(sys.argv) > 2 else 'gui' | |
| if mode == 'server': | |
| run_server(profile_id) | |
| else: | |
| root = tk.Tk() | |
| app = ChatAppGUI(root, profile_id) | |
| root.protocol('WM_DELETE_WINDOW', app.on_closing) | |
| root.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment