This site is developed to XHTML and CSS2 W3C standards.
If you see this paragraph, your browser does not support those standards and you
need to upgrade. Visit WaSP
for a variety of options.
Paste #708
Posted by: modern icq
Posted on: 2026-06-29 20:48:04
Age: 15 hrs ago
Views: 11
#!/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"""
<div style="margin: 8px 0; text-align: {align};">
<div style="display: inline-block; max-width: 70%;
background-color: {bg_color};
color: {text_color};
padding: 10px 15px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0,0,0,0.3);">
<div style="margin-bottom: 4px; line-height: 1.4;">{msg.text}</div>
<div style="font-size: 10px; color: {text_color}99; text-align: right;">
{time_str}
</div>
</div>
</div>
"""
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()
Download raw |
Create new paste