#!/usr/bin/env python3 """ Modern ICQ Client for Linux Built with PyQt6 and custom ICQ protocol library FIXED STATUS DETECTION ONLY """ import sys import time from datetime import datetime from typing import Dict, List, Optional from dataclasses import dataclass, field from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QTextEdit, QLineEdit, QPushButton, QLabel, QSplitter, QFrame, QStackedWidget, QMessageBox, QDialog, QFormLayout, QDialogButtonBox, QComboBox, QScrollBar, QMenu ) from PyQt6.QtCore import ( Qt, QSize, pyqtSignal, QTimer, QThread, QDateTime ) from PyQt6.QtGui import ( QFont, QColor, QTextCursor, QTextCharFormat, QBrush, QPalette, QIcon, QPixmap ) # Import ICQ library from icq_client import ( ICQClient, S_ONLINE, S_AWAY, S_NA, S_DND, S_OCCUPIED, S_INVISIBLE, S_FFC, XSTATUS_MOBILE, XSTATUS_NAMES, status_to_int, status_to_str, FEEDBAG_CLASS_BUDDY, FEEDBAG_CLASS_GROUP, FeedbagItem ) # ================ Data Models ================ @dataclass class Contact: uin: str nickname: str = "" status: int = 0 xstatus_index: int = 0 xstatus_title: str = "" xstatus_message: str = "" unread_messages: int = 0 group_id: int = 0 authorized: bool = False @dataclass class ContactGroup: id: int name: str contacts: List[str] = field(default_factory=list) @dataclass class ChatMessage: text: str sender: str timestamp: datetime is_outgoing: bool = False # ================ Styles ================ MODERN_STYLE = """ /* Main window */ QMainWindow { background-color: #0d1117; color: #c9d1d9; } /* Tree widget */ QTreeWidget { background-color: #161b22; border: none; color: #c9d1d9; font-size: 13px; outline: none; } QTreeWidget::item { padding: 8px; border-bottom: 1px solid #21262d; min-height: 50px; } QTreeWidget::item:selected { background-color: #1f2937; border-left: 3px solid #7c3aed; } QTreeWidget::item:hover { background-color: #1c2128; } QTreeWidget::branch { background-color: #161b22; } /* FIX: Input field - explicit colors */ QLineEdit { background-color: #21262d; border: 2px solid #30363d; border-radius: 8px; padding: 10px 15px; color: #c9d1d9; font-size: 14px; selection-background-color: #7c3aed; selection-color: #ffffff; } QLineEdit:focus { border-color: #7c3aed; background-color: #21262d; color: #c9d1d9; } QLineEdit:disabled { background-color: #161b22; color: #484f58; } /* FIX: Placeholder text */ QLineEdit::placeholder { color: #484f58; } /* Text edit for messages */ QTextEdit { background-color: #0d1117; border: none; color: #c9d1d9; font-size: 14px; padding: 15px; selection-background-color: #7c3aed; selection-color: #ffffff; } /* Buttons */ QPushButton { background-color: #238636; border: 1px solid #2ea043; border-radius: 8px; padding: 8px 16px; color: #ffffff; font-weight: bold; font-size: 13px; } QPushButton:hover { background-color: #2ea043; } QPushButton:pressed { background-color: #238636; } QPushButton:disabled { background-color: #21262d; color: #484f58; border-color: #30363d; } /* Labels */ QLabel { color: #8b949e; font-size: 12px; } /* Scroll bars */ QScrollBar:vertical { background-color: #161b22; width: 8px; border-radius: 4px; margin: 0px; } QScrollBar::handle:vertical { background-color: #30363d; border-radius: 4px; min-height: 30px; } QScrollBar::handle:vertical:hover { background-color: #484f58; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } QScrollBar:horizontal { height: 0px; } /* Splitter */ QSplitter::handle { background-color: #21262d; width: 2px; } /* Chat header */ QFrame#chatHeader { background-color: #161b22; border-bottom: 1px solid #21262d; padding: 10px; } /* Combo box */ QComboBox { background-color: #21262d; border: 2px solid #30363d; border-radius: 8px; padding: 8px 15px; color: #c9d1d9; font-size: 13px; min-width: 140px; } QComboBox:hover { border-color: #7c3aed; } QComboBox::drop-down { border: none; padding-right: 10px; } QComboBox QAbstractItemView { background-color: #21262d; color: #c9d1d9; selection-background-color: #7c3aed; border-radius: 4px; padding: 4px; outline: none; } QComboBox QAbstractItemView::item { padding: 8px 12px; min-height: 30px; } /* Menu */ QMenu { background-color: #21262d; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; padding: 4px; } QMenu::item { padding: 8px 20px; border-radius: 4px; } QMenu::item:selected { background-color: #7c3aed; color: #ffffff; } QMenu::separator { height: 1px; background-color: #30363d; margin: 4px 10px; } /* Dialog */ QDialog { background-color: #161b22; border: 1px solid #21262d; } /* Message box */ QMessageBox { background-color: #161b22; color: #c9d1d9; } QMessageBox QLabel { color: #c9d1d9; font-size: 14px; } QMessageBox QPushButton { min-width: 80px; padding: 8px 16px; } /* Progress bar */ QProgressBar { background-color: #21262d; border: none; border-radius: 4px; text-align: center; color: #c9d1d9; font-size: 12px; } QProgressBar::chunk { background-color: #7c3aed; border-radius: 4px; } """ # ================ FIXED: Status Functions ================ def create_status_text(status: int) -> str: """Return correct status text - FIXED to properly detect Online""" # Offline if status == 0xFFFFFFFF or status == -1: return "Offline" # Online - status 0 or S_ONLINE (0x00000000) if status == 0 or status == S_ONLINE: return "Online" # Use status_to_int for normalization actual_status = status_to_int(status) # Explicit checks for each status if actual_status == S_ONLINE or actual_status == 0: return "Online" elif actual_status == S_AWAY: return "Away" elif actual_status == S_NA: return "N/A" elif actual_status == S_DND: return "DND" elif actual_status == S_OCCUPIED: return "Occupied" elif actual_status == S_INVISIBLE: return "Invisible" elif actual_status == S_FFC: return "Free for Chat" else: # FIX: If we can't determine the status, default to Online # instead of showing N/A for unknown but online users return "Online" def get_status_color(status: int) -> str: """Get color for status indicator""" if status == 0xFFFFFFFF or status == -1: return "#484f58" # Gray for offline actual_status = status_to_int(status) colors = { S_ONLINE: "#3fb950", # Green S_AWAY: "#d2991d", # Yellow S_NA: "#f85149", # Red S_DND: "#f85149", # Red S_OCCUPIED: "#f0883e", # Orange S_INVISIBLE: "#484f58", # Gray S_FFC: "#58a6ff", # Blue } # FIX: Default to green (Online) instead of returning None return colors.get(actual_status, "#3fb950") # ================ Worker Thread ================ class ICQWorker(QThread): """Thread for handling ICQ connection and events""" login_successful = pyqtSignal() login_failed = pyqtSignal(str) message_received = pyqtSignal(str, str) status_changed = pyqtSignal(str, int) user_offline = pyqtSignal(str) auth_request = pyqtSignal(str, str) contact_list_received = pyqtSignal(list) roster_loaded = pyqtSignal() user_info_received = pyqtSignal(str, dict) disconnected = pyqtSignal() message_ack = pyqtSignal(str, int) def __init__(self, uin: int, password: str): super().__init__() self.uin = uin self.password = password self.client = ICQClient() self.running = True self._info_buffer = {} self._keepalive_timer = time.time() self._last_message_time = 0 def setup_client(self): """Configure ICQ client callbacks""" self.client.icq_server = "kicq.ru" self.client.icq_port = "5190" self.client.uin = self.uin self.client.password = self.password def on_login(cl): print("=== LOGIN SUCCESSFUL ===") self._keepalive_timer = time.time() time.sleep(0.5) if self.client.logged_in: cl.set_status(S_ONLINE) time.sleep(0.3) self._request_roster_safe() def on_message(cl, msg, uin): print(f"=== MESSAGE from {uin}") self.message_received.emit(uin, msg) def on_status(cl, uin, raw_status): # FIX: Better status normalization print(f"=== RAW STATUS from {uin}: {raw_status} (0x{raw_status:08X})") # Normalize status if raw_status == 0 or raw_status == S_ONLINE: status = S_ONLINE elif raw_status == 0xFFFFFFFF: status = 0xFFFFFFFF elif raw_status == S_INVISIBLE: status = S_INVISIBLE else: # Use status_to_int for other cases status = status_to_int(raw_status) # If result is 0, it's Online if status == 0: status = S_ONLINE print(f"=== NORMALIZED STATUS: {uin} -> {create_status_text(status)}") self.status_changed.emit(uin, status) def on_offline(cl, uin): print(f"=== OFFLINE: {uin}") self.user_offline.emit(uin) def on_auth(cl, uin, reason): print(f"=== AUTH from {uin}") self.auth_request.emit(uin, reason) try: cl.send_auth_response(uin, True) except: pass def on_contact_list(cl, name, contacts): print(f"=== CONTACTS: {len(cl.feedbag_items)} items") if cl.feedbag_items: self.contact_list_received.emit(cl.feedbag_items) self.roster_loaded.emit() QTimer.singleShot(1000, lambda: self._request_all_statuses(cl)) def on_user_info(cl, uin, info_type, info_data): if uin not in self._info_buffer: self._info_buffer[uin] = {} if isinstance(info_data, dict): self._info_buffer[uin].update(info_data) self.user_info_received.emit(uin, self._info_buffer[uin]) def on_msg_ack(cl, uin, msg_id): self.message_ack.emit(uin, msg_id) self.client.on_login = on_login self.client.on_message_recv = on_message self.client.on_status_change = on_status self.client.on_user_offline = on_offline self.client.on_auth_request = on_auth self.client.on_contact_list_recv = on_contact_list self.client.on_user_general_info = on_user_info self.client.on_user_info_more = on_user_info self.client.on_msg_ack = on_msg_ack def _request_roster_safe(self): if not self.client.logged_in: return print("=== Requesting roster ===") try: self.client.request_feedbag() except Exception as e: print(f"Feedbag error: {e}") def _request_all_statuses(self, cl): if not self.running or not self.client.logged_in: return contacts_to_query = [] for item in cl.feedbag_items: if item.class_id == FEEDBAG_CLASS_BUDDY and item.name.isdigit(): contacts_to_query.append(int(item.name)) print(f"Requesting info for {len(contacts_to_query)} contacts") for uin_int in contacts_to_query[:20]: if not self.running or not self.client.logged_in: break try: cl.request_info_short(uin_int) time.sleep(0.3) except Exception as e: print(f"Info error for {uin_int}: {e}") def run(self): self.setup_client() try: print(f"Connecting to {self.client.icq_server}:{self.client.icq_port}") self.client.login(S_ONLINE) if not self.client.wait_login(90): self.login_failed.emit("Login timeout") return print("Login successful") if self.client.logged_in: self.client.set_status(S_ONLINE) self.login_successful.emit() while self.running: if not self.client.logged_in: print("Connection lost") if self.running: self.disconnected.emit() break current_time = time.time() if current_time - self._keepalive_timer >= 40: try: if current_time - self._last_message_time > 5: self.client.send_keep_alive() self._keepalive_timer = current_time except Exception as e: print(f"Keep-alive error: {e}") time.sleep(0.5) except Exception as e: print(f"Worker error: {e}") import traceback traceback.print_exc() if self.running: self.login_failed.emit(str(e)) finally: print("Worker stopping...") def send_message_safe(self, uin: str, msg: str) -> bool: if not self.client.logged_in: return False try: self.client.set_status(S_ONLINE) self.client.send_message(uin, msg) self._last_message_time = time.time() self._keepalive_timer = time.time() print(f"Message sent to {uin}") return True except Exception as e: print(f"Send error: {e}") return False def stop(self): print("Stopping worker...") self.running = False time.sleep(0.3) # ================ Main Window ================ class ModernICQClient(QMainWindow): def __init__(self): super().__init__() self.contacts: Dict[str, Contact] = {} self.groups: Dict[int, ContactGroup] = {} self.chats: Dict[str, List[ChatMessage]] = {} self.current_chat_uin: Optional[str] = None self.worker: Optional[ICQWorker] = None self.loading_dialog = None self.init_ui() def init_ui(self): """Initialize the user interface""" self.setWindowTitle("Modern ICQ") self.setGeometry(100, 100, 1100, 700) self.setStyleSheet(MODERN_STYLE) central = QWidget() self.setCentralWidget(central) main_layout = QHBoxLayout(central) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) splitter = QSplitter(Qt.Orientation.Horizontal) self.create_contact_panel(splitter) self.create_chat_panel(splitter) splitter.setSizes([320, 780]) main_layout.addWidget(splitter) QTimer.singleShot(100, self.show_login_dialog) def create_contact_panel(self, parent): """Create the contact list panel""" panel = QWidget() layout = QVBoxLayout(panel) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) # Header header = QFrame() header.setFixedHeight(50) header.setStyleSheet(""" QFrame { background-color: #161b22; border-bottom: 1px solid #21262d; } """) header_layout = QHBoxLayout(header) header_layout.setContentsMargins(15, 0, 15, 0) title = QLabel("Messages") title.setStyleSheet("font-weight: bold; font-size: 16px; color: #c9d1d9;") header_layout.addWidget(title) header_layout.addStretch() add_btn = QPushButton("+") add_btn.setFixedSize(30, 30) add_btn.setStyleSheet("font-size: 18px;") add_btn.clicked.connect(self.show_add_contact_dialog) header_layout.addWidget(add_btn) layout.addWidget(header) # Search search_container = QWidget() search_container.setStyleSheet("background-color: #161b22;") search_layout = QHBoxLayout(search_container) search_layout.setContentsMargins(10, 5, 10, 10) self.search_input = QLineEdit() self.search_input.setPlaceholderText("Search contacts...") self.search_input.textChanged.connect(self.filter_contacts) self.search_input.setMinimumHeight(35) self.search_input.setStyleSheet(""" QLineEdit { background-color: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; color: #c9d1d9; font-size: 13px; } QLineEdit:focus { border-color: #7c3aed; color: #c9d1d9; } QLineEdit::placeholder { color: #484f58; } """) search_layout.addWidget(self.search_input) layout.addWidget(search_container) # Status selector status_container = QWidget() status_container.setStyleSheet("background-color: #161b22;") status_layout = QHBoxLayout(status_container) status_layout.setContentsMargins(10, 0, 10, 5) self.status_combo = QComboBox() self.status_combo.addItem("Online", S_ONLINE) self.status_combo.addItem("Away", S_AWAY) self.status_combo.addItem("N/A", S_NA) self.status_combo.addItem("DND", S_DND) self.status_combo.addItem("Invisible", S_INVISIBLE) self.status_combo.currentIndexChanged.connect(self.change_status) self.status_combo.setMinimumHeight(35) self.status_combo.setStyleSheet(""" QComboBox { background-color: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; color: #c9d1d9; font-size: 13px; } QComboBox:hover { border-color: #7c3aed; } QComboBox QAbstractItemView { background-color: #21262d; color: #c9d1d9; selection-background-color: #7c3aed; } """) status_layout.addWidget(self.status_combo) layout.addWidget(status_container) # Contact tree self.contact_tree = QTreeWidget() self.contact_tree.setHeaderHidden(True) self.contact_tree.setIndentation(15) self.contact_tree.itemClicked.connect(self.contact_selected) self.contact_tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.contact_tree.customContextMenuRequested.connect(self.show_contact_context_menu) layout.addWidget(self.contact_tree) parent.addWidget(panel) def create_chat_panel(self, parent): """Create the chat panel""" panel = QWidget() layout = QVBoxLayout(panel) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) self.chat_stack = QStackedWidget() # Empty state empty_widget = QWidget() empty_widget.setStyleSheet("background-color: #0d1117;") empty_layout = QVBoxLayout(empty_widget) empty_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) empty_label = QLabel("Select a chat to start messaging") empty_label.setStyleSheet("font-size: 16px; color: #484f58;") empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter) empty_layout.addWidget(empty_label) self.chat_stack.addWidget(empty_widget) # Chat view chat_widget = QWidget() chat_layout = QVBoxLayout(chat_widget) chat_layout.setContentsMargins(0, 0, 0, 0) chat_layout.setSpacing(0) # Chat header self.chat_header = QFrame() self.chat_header.setFixedHeight(55) self.chat_header.setStyleSheet(""" QFrame { background-color: #161b22; border-bottom: 1px solid #21262d; } """) header_layout = QHBoxLayout(self.chat_header) header_layout.setContentsMargins(15, 0, 15, 0) info_layout = QVBoxLayout() info_layout.setSpacing(2) self.chat_title = QLabel("") self.chat_title.setStyleSheet("font-weight: bold; font-size: 14px; color: #c9d1d9;") info_layout.addWidget(self.chat_title) self.chat_status = QLabel("") self.chat_status.setStyleSheet("font-size: 11px;") info_layout.addWidget(self.chat_status) header_layout.addLayout(info_layout) header_layout.addStretch() chat_layout.addWidget(self.chat_header) # Messages area self.messages_area = QTextEdit() self.messages_area.setReadOnly(True) chat_layout.addWidget(self.messages_area) # Input area input_frame = QFrame() input_frame.setFixedHeight(65) input_frame.setStyleSheet(""" QFrame { background-color: #161b22; border-top: 1px solid #21262d; } """) input_layout = QHBoxLayout(input_frame) input_layout.setContentsMargins(10, 10, 10, 10) self.message_input = QLineEdit() self.message_input.setPlaceholderText("Type a message...") self.message_input.returnPressed.connect(self.send_message) self.message_input.setMinimumHeight(40) self.message_input.setStyleSheet(""" QLineEdit { background-color: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 8px 12px; color: #c9d1d9; font-size: 14px; } QLineEdit:focus { border-color: #7c3aed; color: #c9d1d9; } QLineEdit::placeholder { color: #484f58; } """) input_layout.addWidget(self.message_input) send_btn = QPushButton("Send") send_btn.setFixedSize(70, 40) send_btn.clicked.connect(self.send_message) input_layout.addWidget(send_btn) chat_layout.addWidget(input_frame) self.chat_stack.addWidget(chat_widget) layout.addWidget(self.chat_stack) parent.addWidget(panel) def show_login_dialog(self): """Show login dialog""" dialog = QDialog(self) dialog.setWindowTitle("Login to ICQ") dialog.setFixedSize(400, 280) layout = QVBoxLayout(dialog) layout.setSpacing(15) layout.setContentsMargins(30, 30, 30, 30) title = QLabel("Sign in to ICQ") title.setStyleSheet("font-size: 20px; font-weight: bold; color: #c9d1d9;") title.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(title) uin_input = QLineEdit() uin_input.setPlaceholderText("Enter your UIN") uin_input.setMinimumHeight(40) uin_input.setStyleSheet(""" QLineEdit { background-color: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 10px 12px; color: #c9d1d9; font-size: 14px; } QLineEdit:focus { border-color: #7c3aed; color: #c9d1d9; } QLineEdit::placeholder { color: #484f58; } """) layout.addWidget(uin_input) pass_input = QLineEdit() pass_input.setPlaceholderText("Enter your password") pass_input.setEchoMode(QLineEdit.EchoMode.Password) pass_input.setMinimumHeight(40) pass_input.setStyleSheet(uin_input.styleSheet()) layout.addWidget(pass_input) login_btn = QPushButton("Connect") login_btn.setMinimumHeight(40) login_btn.clicked.connect(dialog.accept) layout.addWidget(login_btn) if dialog.exec() == QDialog.DialogCode.Accepted: try: uin = int(uin_input.text()) password = pass_input.text() if uin and password: self.show_loading_dialog() self.start_connection(uin, password) except ValueError: QMessageBox.critical(self, "Error", "Invalid UIN") def show_loading_dialog(self): """Show loading dialog""" self.loading_dialog = QDialog(self) self.loading_dialog.setWindowTitle("Connecting...") self.loading_dialog.setFixedSize(300, 100) layout = QVBoxLayout(self.loading_dialog) label = QLabel("Connecting to ICQ...") label.setStyleSheet("color: #c9d1d9; font-size: 16px;") label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(label) self.loading_dialog.show() def start_connection(self, uin: int, password: str): """Start ICQ connection""" if self.worker: self.worker.stop() self.worker.wait(2000) self.worker = ICQWorker(uin, password) self.worker.login_successful.connect(self.on_login_success) self.worker.login_failed.connect(self.on_login_failed) self.worker.message_received.connect(self.on_message) self.worker.status_changed.connect(self.on_contact_status_change) self.worker.user_offline.connect(self.on_contact_offline) self.worker.roster_loaded.connect(self.on_roster_loaded) self.worker.contact_list_received.connect(self.process_feedbag) self.worker.user_info_received.connect(self.update_contact_info) self.worker.disconnected.connect(self.on_disconnected) self.worker.message_ack.connect(self.on_message_ack) self.worker.start() def on_login_success(self): """Handle successful login""" print("=== Login successful ===") self.setWindowTitle(f"Modern ICQ - {self.worker.uin}") if self.loading_dialog: self.loading_dialog.close() self.loading_dialog = None def on_login_failed(self, error_msg: str): """Handle login failure""" if self.loading_dialog: self.loading_dialog.close() self.loading_dialog = None QMessageBox.critical(self, "Login Failed", error_msg) def on_disconnected(self): """Handle disconnection""" if self.worker and self.worker.running: QMessageBox.warning(self, "Disconnected", "Connection lost.") def process_feedbag(self, feedbag_items: List[FeedbagItem]): """Process feedbag items""" if not feedbag_items: return existing_statuses = {} for uin, contact in self.contacts.items(): existing_statuses[uin] = contact.status self.contacts.clear() self.groups.clear() for item in feedbag_items: if item.class_id == FEEDBAG_CLASS_GROUP: gid = item.group_id if item.group_id > 0 else item.item_id gname = item.name if item.name else "General" if gid not in self.groups: self.groups[gid] = ContactGroup(id=gid, name=gname) if 0 not in self.groups: self.groups[0] = ContactGroup(id=0, name="Contacts") for item in feedbag_items: if item.class_id == FEEDBAG_CLASS_BUDDY and item.name.isdigit(): uin = item.name gid = item.group_id if item.group_id in self.groups else 0 # FIX: Default to S_ONLINE for new contacts old_status = existing_statuses.get(uin, S_ONLINE) contact = Contact( uin=uin, group_id=gid, nickname=uin, status=old_status ) self.contacts[uin] = contact self.groups[gid].contacts.append(uin) print(f"Loaded {len(self.contacts)} contacts") self.build_contact_tree() def build_contact_tree(self): """Build contact tree""" self.contact_tree.clear() for group_id, group in self.groups.items(): if not group.contacts: continue group_item = QTreeWidgetItem() group_item.setText(0, f"{group.name} ({len(group.contacts)})") group_item.setData(0, Qt.ItemDataRole.UserRole, f"group_{group_id}") font = QFont() font.setBold(True) font.setPointSize(12) group_item.setFont(0, font) group_item.setForeground(0, QColor("#8b949e")) for uin in group.contacts: if uin in self.contacts: contact = self.contacts[uin] contact_item = QTreeWidgetItem() name = contact.nickname if contact.nickname != uin else f"User {uin}" if contact.unread_messages > 0: name = f"● {name}" contact_item.setForeground(0, QColor("#c9d1d9")) item_font = QFont() item_font.setBold(True) item_font.setPointSize(12) contact_item.setFont(0, item_font) else: contact_item.setForeground(0, QColor("#8b949e")) item_font = QFont() item_font.setPointSize(12) contact_item.setFont(0, item_font) contact_item.setText(0, name) contact_item.setData(0, Qt.ItemDataRole.UserRole, uin) # FIX: Use the corrected status functions status_text = create_status_text(contact.status) status_color = get_status_color(contact.status) contact_item.setText(1, status_text) contact_item.setForeground(1, QColor(status_color)) if contact.unread_messages > 0: contact_item.setText(2, str(contact.unread_messages)) contact_item.setForeground(2, QColor("#7c3aed")) badge_font = QFont() badge_font.setBold(True) contact_item.setFont(2, badge_font) group_item.addChild(contact_item) self.contact_tree.addTopLevelItem(group_item) group_item.setExpanded(True) def on_roster_loaded(self): """Handle roster loading""" print(f"Roster loaded: {len(self.contacts)} contacts") def update_contact_info(self, uin: str, info: dict): """Update contact info""" if uin in self.contacts: contact = self.contacts[uin] if 'nick' in info and info['nick']: contact.nickname = info['nick'] elif 'firstName' in info: first = info.get('firstName', '') last = info.get('lastName', '') if first or last: contact.nickname = f"{first} {last}".strip() self.build_contact_tree() def on_message(self, uin: str, msg: str): """Handle incoming message""" print(f"Message from {uin}") if uin not in self.contacts: contact = Contact(uin=uin, nickname=f"User {uin}", status=S_ONLINE) self.contacts[uin] = contact if 0 not in self.groups: self.groups[0] = ContactGroup(id=0, name="Contacts") self.groups[0].contacts.append(uin) self.build_contact_tree() else: self.contacts[uin].status = S_ONLINE chat_msg = ChatMessage( text=msg, sender=uin, timestamp=datetime.now(), is_outgoing=False ) if uin not in self.chats: self.chats[uin] = [] self.chats[uin].append(chat_msg) if self.current_chat_uin == uin: self.display_message(chat_msg) else: if uin in self.contacts: self.contacts[uin].unread_messages += 1 self.build_contact_tree() def on_message_ack(self, uin: str, msg_id: int): """Handle message acknowledgement""" pass def on_contact_status_change(self, uin: str, status: int): """Handle status change""" # FIX: Log the actual status being set print(f"Status change for {uin}: {status} -> {create_status_text(status)}") if uin in self.contacts: self.contacts[uin].status = status self.build_contact_tree() if self.current_chat_uin == uin: status_text = create_status_text(status) status_color = get_status_color(status) self.chat_status.setText(status_text) self.chat_status.setStyleSheet(f"color: {status_color}; font-size: 12px;") def on_contact_offline(self, uin: str): """Handle contact offline""" if uin in self.contacts: self.contacts[uin].status = 0xFFFFFFFF self.build_contact_tree() def filter_contacts(self, text: str): """Filter contacts""" for i in range(self.contact_tree.topLevelItemCount()): group_item = self.contact_tree.topLevelItem(i) visible = 0 for j in range(group_item.childCount()): child = group_item.child(j) uin = child.data(0, Qt.ItemDataRole.UserRole) contact = self.contacts.get(uin) if contact: name = contact.nickname or contact.uin matches = text.lower() in name.lower() or text in uin child.setHidden(not matches) if matches: visible += 1 group_item.setHidden(visible == 0) def contact_selected(self, item: QTreeWidgetItem, column: int): """Handle contact selection""" uin = item.data(0, Qt.ItemDataRole.UserRole) if uin and not uin.startswith("group_"): self.open_chat(uin) def show_contact_context_menu(self, pos): """Context menu""" item = self.contact_tree.itemAt(pos) if not item: return uin = item.data(0, Qt.ItemDataRole.UserRole) if not uin or uin.startswith("group_"): return menu = QMenu(self) menu.addAction("Open Chat", lambda: self.open_chat(uin)) menu.addSeparator() menu.addAction("Remove Contact", lambda: self.remove_contact(uin)) menu.exec(self.contact_tree.viewport().mapToGlobal(pos)) def remove_contact(self, uin: str): """Remove contact""" reply = QMessageBox.question( self, "Remove", f"Remove {uin}?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: if self.worker and self.worker.client.logged_in: try: self.worker.client.remove_contact(int(uin)) if uin in self.contacts: del self.contacts[uin] for group in self.groups.values(): if uin in group.contacts: group.contacts.remove(uin) self.build_contact_tree() except Exception as e: QMessageBox.critical(self, "Error", str(e)) def open_chat(self, uin: str): """Open chat with contact""" self.current_chat_uin = uin self.chat_stack.setCurrentIndex(1) contact = self.contacts.get(uin, Contact(uin=uin, status=S_ONLINE)) name = contact.nickname if contact.nickname != uin else f"User {uin}" self.chat_title.setText(name) # FIX: Use corrected status functions status_text = create_status_text(contact.status) status_color = get_status_color(contact.status) self.chat_status.setText(status_text) self.chat_status.setStyleSheet(f"color: {status_color}; font-size: 12px;") self.messages_area.clear() if uin in self.chats: for msg in self.chats[uin]: self.display_message(msg) if uin in self.contacts: self.contacts[uin].unread_messages = 0 self.build_contact_tree() self.message_input.setFocus() def display_message(self, msg: ChatMessage): """Display message in chat""" time_str = msg.timestamp.strftime("%H:%M") if msg.is_outgoing: bg_color = "#238636" text_color = "#ffffff" align = "right" else: bg_color = "#21262d" text_color = "#c9d1d9" align = "left" html = f"""
{msg.text}
{time_str}
""" self.messages_area.append(html) scrollbar = self.messages_area.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) def send_message(self): """Send message""" if not self.current_chat_uin or not self.message_input.text(): return uin = self.current_chat_uin text = self.message_input.text() msg = ChatMessage( text=text, sender=str(self.worker.uin) if self.worker else "me", timestamp=datetime.now(), is_outgoing=True ) if uin not in self.chats: self.chats[uin] = [] self.chats[uin].append(msg) self.display_message(msg) if self.worker: success = self.worker.send_message_safe(uin, text) if not success: QMessageBox.warning(self, "Error", "Failed to send message.") self.message_input.clear() self.message_input.setFocus() def change_status(self): """Change user status""" if self.worker and self.worker.client.logged_in: status = self.status_combo.currentData() try: self.worker.client.set_status(status) except Exception as e: print(f"Status change error: {e}") def show_add_contact_dialog(self): """Add contact dialog""" dialog = QDialog(self) dialog.setWindowTitle("Add Contact") dialog.setFixedSize(350, 200) layout = QVBoxLayout(dialog) layout.setContentsMargins(30, 30, 30, 30) title = QLabel("Add New Contact") title.setStyleSheet("font-size: 18px; font-weight: bold; color: #c9d1d9;") layout.addWidget(title) uin_input = QLineEdit() uin_input.setPlaceholderText("Enter UIN number") uin_input.setMinimumHeight(40) uin_input.setStyleSheet(""" QLineEdit { background-color: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 10px 12px; color: #c9d1d9; font-size: 14px; margin: 10px 0; } QLineEdit:focus { border-color: #7c3aed; color: #c9d1d9; } QLineEdit::placeholder { color: #484f58; } """) layout.addWidget(uin_input) add_btn = QPushButton("Add Contact") add_btn.setMinimumHeight(40) add_btn.clicked.connect(dialog.accept) layout.addWidget(add_btn) if dialog.exec() == QDialog.DialogCode.Accepted: uin = uin_input.text().strip() if uin and uin.isdigit(): if self.worker and self.worker.client.logged_in: try: self.worker.client.add_contact(int(uin)) QMessageBox.information(self, "Success", f"Contact {uin} added!") except Exception as e: QMessageBox.critical(self, "Error", f"Failed: {e}") def closeEvent(self, event): """Handle close""" print("Closing...") if self.worker: self.worker.stop() self.worker.wait(2000) event.accept() # ================ Main ================ def main(): import os os.environ["QT_LOGGING_RULES"] = "*.debug=false" app = QApplication(sys.argv) app.setApplicationName("Modern ICQ") app.setStyle("Fusion") palette = QPalette() palette.setColor(QPalette.ColorRole.Window, QColor("#0d1117")) palette.setColor(QPalette.ColorRole.WindowText, QColor("#c9d1d9")) palette.setColor(QPalette.ColorRole.Base, QColor("#0d1117")) palette.setColor(QPalette.ColorRole.Text, QColor("#c9d1d9")) app.setPalette(palette) window = ModernICQClient() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()