Skip to content

Instantly share code, notes, and snippets.

@Hammer2900
Created June 9, 2026 09:09
Show Gist options
  • Select an option

  • Save Hammer2900/12fc5fe4582f7f806b0a52214d444bd4 to your computer and use it in GitHub Desktop.

Select an option

Save Hammer2900/12fc5fe4582f7f806b0a52214d444bd4 to your computer and use it in GitHub Desktop.
fix to py_ipfs_node
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