first project like telegram
This commit is contained in:
parent
2f128f9a1a
commit
609c627823
249
App_GUI.py
249
App_GUI.py
|
|
@ -1,249 +0,0 @@
|
|||
import customtkinter as ctk
|
||||
|
||||
# GUI Configuration
|
||||
ctk.set_appearance_mode("Dark")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
class AppGUI(ctk.CTk):
|
||||
def __init__(self, controller):
|
||||
super().__init__()
|
||||
self.controller = controller
|
||||
|
||||
self.title("Secure SMS - Raspberry Pi")
|
||||
self.geometry("800x480") # Typical Pi touchscreen resolution
|
||||
|
||||
# Current state
|
||||
self.current_contact = None
|
||||
|
||||
self.setup_ui()
|
||||
self.load_contacts()
|
||||
|
||||
def setup_ui(self):
|
||||
# Grid Layout (1x2) - Sidebar | Main Content
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# --- Sidebar ---
|
||||
self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
|
||||
self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
|
||||
self.sidebar_frame.grid_rowconfigure(4, weight=1)
|
||||
|
||||
self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="Secure SMS", font=ctk.CTkFont(size=20, weight="bold"))
|
||||
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
|
||||
|
||||
self.contacts_scrollable_frame = ctk.CTkScrollableFrame(self.sidebar_frame, label_text="Contacts")
|
||||
self.contacts_scrollable_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew", rowspan=4)
|
||||
|
||||
# Settings / Add Contact Button
|
||||
self.settings_btn = ctk.CTkButton(self.sidebar_frame, text="Settings / Add Contact", command=self.open_settings)
|
||||
self.settings_btn.grid(row=5, column=0, padx=10, pady=20)
|
||||
|
||||
# --- Main Chat Area ---
|
||||
self.main_frame = ctk.CTkFrame(self, corner_radius=0)
|
||||
self.main_frame.grid(row=0, column=1, sticky="nsew")
|
||||
self.main_frame.grid_rowconfigure(1, weight=1)
|
||||
self.main_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# Chat Header
|
||||
self.chat_header_var = ctk.StringVar(value="Select a contact")
|
||||
self.chat_header = ctk.CTkLabel(self.main_frame, textvariable=self.chat_header_var, font=ctk.CTkFont(size=18, weight="bold"))
|
||||
self.chat_header.grid(row=0, column=0, padx=20, pady=10, sticky="w")
|
||||
|
||||
# Secured Status Label Indicator
|
||||
self.header_secure_status = ctk.CTkLabel(self.main_frame, text="Current Mode: Normal", text_color="yellow")
|
||||
self.header_secure_status.grid(row=0, column=0, padx=20, pady=10, sticky="e")
|
||||
|
||||
# Messages Display
|
||||
self.msg_display = ctk.CTkTextbox(self.main_frame, state="disabled", wrap="word")
|
||||
self.msg_display.grid(row=1, column=0, padx=20, pady=(0, 10), sticky="nsew")
|
||||
self.msg_display.tag_config('sent_normal', foreground='lightgray', justify='right')
|
||||
self.msg_display.tag_config('sent_secure', foreground='lightgreen', justify='right')
|
||||
self.msg_display.tag_config('recv_normal', foreground='white', justify='left')
|
||||
self.msg_display.tag_config('recv_secure', foreground='mediumspringgreen', justify='left')
|
||||
self.msg_display.tag_config('system', foreground='red', justify='center')
|
||||
|
||||
# Bottom Input Area
|
||||
self.input_frame = ctk.CTkFrame(self.main_frame, height=50)
|
||||
self.input_frame.grid(row=2, column=0, padx=20, pady=10, sticky="nsew")
|
||||
self.input_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.msg_entry = ctk.CTkEntry(self.input_frame, placeholder_text="Type a message...")
|
||||
self.msg_entry.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="nsew")
|
||||
self.msg_entry.bind("<Return>", lambda e: self.send_message())
|
||||
|
||||
# Toggle Secure SMS
|
||||
self.secure_mode_var = ctk.StringVar(value="off")
|
||||
self.secure_switch = ctk.CTkSwitch(
|
||||
self.input_frame,
|
||||
text="Secure",
|
||||
command=self.toggle_mode,
|
||||
variable=self.secure_mode_var,
|
||||
onvalue="on",
|
||||
offvalue="off"
|
||||
)
|
||||
self.secure_switch.grid(row=0, column=1, padx=5, pady=10)
|
||||
|
||||
self.send_btn = ctk.CTkButton(self.input_frame, text="Send", width=80, command=self.send_message)
|
||||
self.send_btn.grid(row=0, column=2, padx=(5, 10), pady=10)
|
||||
|
||||
def toggle_mode(self):
|
||||
mode = "Secure" if self.secure_mode_var.get() == "on" else "Normal"
|
||||
color = "mediumspringgreen" if mode == "Secure" else "yellow"
|
||||
self.header_secure_status.configure(text=f"Current Mode: {mode}", text_color=color)
|
||||
|
||||
def load_contacts(self):
|
||||
# Clear sidebar list
|
||||
for widget in self.contacts_scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
contacts = self.controller.get_contacts()
|
||||
for idx, contact in enumerate(contacts):
|
||||
# contact = (name, phone, pubkey)
|
||||
name, phone, _ = contact
|
||||
btn = ctk.CTkButton(
|
||||
self.contacts_scrollable_frame,
|
||||
text=f"{name}\n{phone}",
|
||||
command=lambda p=phone: self.select_contact(p),
|
||||
fg_color="transparent",
|
||||
border_width=2,
|
||||
text_color=("gray10", "#DCE4EE")
|
||||
)
|
||||
btn.grid(row=idx, column=0, padx=5, pady=5, sticky="ew")
|
||||
|
||||
def select_contact(self, phone):
|
||||
self.current_contact = self.controller.get_contact_info(phone)
|
||||
name = self.current_contact[0]
|
||||
self.chat_header_var.set(f"Chat with {name} ({phone})")
|
||||
|
||||
# Determine if we can do secure
|
||||
has_key = self.current_contact[2] is not None
|
||||
if not has_key:
|
||||
self.secure_switch.deselect()
|
||||
self.secure_switch.configure(state="disabled")
|
||||
self.toggle_mode()
|
||||
self.append_system_msg("This contact has no public key. Secure mode disabled.")
|
||||
else:
|
||||
self.secure_switch.configure(state="normal")
|
||||
|
||||
self.refresh_messages()
|
||||
|
||||
def refresh_messages(self):
|
||||
self.msg_display.configure(state="normal")
|
||||
self.msg_display.delete("1.0", "end")
|
||||
|
||||
if self.current_contact:
|
||||
messages = self.controller.get_messages(self.current_contact[1])
|
||||
for msg in messages:
|
||||
# msg = (id, phone, text, date, is_secure, status)
|
||||
text = msg[2]
|
||||
date = msg[3]
|
||||
is_secure = msg[4]
|
||||
status = msg[5] # sent / recv
|
||||
|
||||
tag = f"{status}_{'secure' if is_secure else 'normal'}"
|
||||
prefix = "🔒 " if is_secure else ""
|
||||
|
||||
display_text = f"[{date}]\n{prefix}{text}\n\n"
|
||||
self.msg_display.insert("end", display_text, tag)
|
||||
|
||||
self.msg_display.configure(state="disabled")
|
||||
self.msg_display.yview("end")
|
||||
|
||||
def append_system_msg(self, text):
|
||||
self.msg_display.configure(state="normal")
|
||||
self.msg_display.insert("end", f"--- {text} ---\n\n", "system")
|
||||
self.msg_display.configure(state="disabled")
|
||||
self.msg_display.yview("end")
|
||||
|
||||
def send_message(self):
|
||||
if not self.current_contact:
|
||||
return
|
||||
|
||||
text = self.msg_entry.get().strip()
|
||||
if not text:
|
||||
return
|
||||
|
||||
is_secure = self.secure_mode_var.get() == "on"
|
||||
phone = self.current_contact[1]
|
||||
|
||||
# Ask controller to send
|
||||
self.msg_entry.delete(0, 'end')
|
||||
self.append_system_msg("Sending...")
|
||||
self.update() # force UI refresh safely
|
||||
|
||||
success = self.controller.send_message(phone, text, is_secure)
|
||||
if success:
|
||||
self.refresh_messages()
|
||||
else:
|
||||
self.append_system_msg("Failed to send SMS!")
|
||||
|
||||
def open_settings(self):
|
||||
SettingsWindow(self, self.controller)
|
||||
|
||||
class SettingsWindow(ctk.CTkToplevel):
|
||||
def __init__(self, master, controller):
|
||||
super().__init__(master)
|
||||
self.controller = controller
|
||||
self.title("Settings")
|
||||
self.geometry("500x500")
|
||||
|
||||
# Bring to front
|
||||
self.attributes('-topmost', 1)
|
||||
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# My Public Key
|
||||
lbl1 = ctk.CTkLabel(self, text="My Public Key (Share this to chat securely):")
|
||||
lbl1.grid(row=0, column=0, padx=20, pady=(20, 5), sticky="w")
|
||||
|
||||
self.my_key_box = ctk.CTkTextbox(self, height=100)
|
||||
self.my_key_box.grid(row=1, column=0, padx=20, pady=5, sticky="ew")
|
||||
self.my_key_box.insert("1.0", self.controller.get_my_public_key())
|
||||
self.my_key_box.configure(state="disabled")
|
||||
|
||||
# Add / Update Contact
|
||||
lbl_add = ctk.CTkLabel(self, text="Add/Update Contact:", font=ctk.CTkFont(weight="bold"))
|
||||
lbl_add.grid(row=2, column=0, padx=20, pady=(30, 5), sticky="w")
|
||||
|
||||
self.name_entry = ctk.CTkEntry(self, placeholder_text="Contact Name")
|
||||
self.name_entry.grid(row=3, column=0, padx=20, pady=5, sticky="ew")
|
||||
|
||||
self.phone_entry = ctk.CTkEntry(self, placeholder_text="Phone Number (+989...)")
|
||||
self.phone_entry.grid(row=4, column=0, padx=20, pady=5, sticky="ew")
|
||||
|
||||
lbl2 = ctk.CTkLabel(self, text="Contact's Public Key (Optional but required for secure SMS):")
|
||||
lbl2.grid(row=5, column=0, padx=20, pady=5, sticky="w")
|
||||
|
||||
self.contact_key_box = ctk.CTkTextbox(self, height=100)
|
||||
self.contact_key_box.grid(row=6, column=0, padx=20, pady=5, sticky="ew")
|
||||
|
||||
self.save_btn = ctk.CTkButton(self, text="Save Contact", command=self.save_contact)
|
||||
self.save_btn.grid(row=7, column=0, padx=20, pady=20)
|
||||
|
||||
def save_contact(self):
|
||||
name = self.name_entry.get().strip()
|
||||
phone = self.phone_entry.get().strip()
|
||||
pubkey = self.contact_key_box.get("1.0", 'end-1c').strip()
|
||||
|
||||
if not name or not phone:
|
||||
return
|
||||
|
||||
if not pubkey:
|
||||
pubkey = None
|
||||
|
||||
self.controller.save_contact(name, phone, pubkey)
|
||||
self.master.load_contacts()
|
||||
self.destroy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Mock Controller for standalone GUI test
|
||||
class MockController:
|
||||
def get_contacts(self): return [("Alice", "+1234", "key"), ("Bob", "+5678", None)]
|
||||
def get_contact_info(self, phone): return ("Alice", phone, "key") if phone == "+1234" else ("Bob", phone, None)
|
||||
def get_messages(self, phone): return [(1, phone, "Hi", "2023-01-01", 0, "recv"), (2, phone, "Secret", "2023-01-01", 1, "sent")]
|
||||
def get_my_public_key(self): return "MY_PEM_MOCK_DATA"
|
||||
def send_message(self, *args): return True
|
||||
def save_contact(self, *args): pass
|
||||
|
||||
app = AppGUI(MockController())
|
||||
app.mainloop()
|
||||
162
Crypto_Engine.py
162
Crypto_Engine.py
|
|
@ -1,162 +0,0 @@
|
|||
import os
|
||||
import base64
|
||||
from cryptography.hazmat.primitives.asymmetric import x25519
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
# Constants
|
||||
PRIVATE_KEY_FILE = "private_key.pem"
|
||||
PUBLIC_KEY_FILE = "public_key.pem"
|
||||
|
||||
class CryptoEngine:
|
||||
def __init__(self):
|
||||
self.private_key = None
|
||||
self.public_key = None
|
||||
self.load_or_generate_keys()
|
||||
|
||||
def load_or_generate_keys(self):
|
||||
"""Loads keys from disk or generates a new pair if they don't exist."""
|
||||
if os.path.exists(PRIVATE_KEY_FILE) and os.path.exists(PUBLIC_KEY_FILE):
|
||||
with open(PRIVATE_KEY_FILE, "rb") as f:
|
||||
self.private_key = serialization.load_pem_private_key(
|
||||
f.read(), password=None
|
||||
)
|
||||
with open(PUBLIC_KEY_FILE, "rb") as f:
|
||||
self.public_key = serialization.load_pem_public_key(f.read())
|
||||
else:
|
||||
self.generate_keypair()
|
||||
|
||||
def generate_keypair(self):
|
||||
"""Generates a new X25519 keypair and saves it to disk."""
|
||||
self.private_key = x25519.X25519PrivateKey.generate()
|
||||
self.public_key = self.private_key.public_key()
|
||||
|
||||
# Save private key
|
||||
with open(PRIVATE_KEY_FILE, "wb") as f:
|
||||
f.write(self.private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
))
|
||||
|
||||
# Save public key
|
||||
with open(PUBLIC_KEY_FILE, "wb") as f:
|
||||
f.write(self.public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
))
|
||||
|
||||
def get_my_public_key_pem(self):
|
||||
"""Returns my public key as string."""
|
||||
return self.public_key.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
).decode('utf-8')
|
||||
|
||||
def encrypt_message(self, message: str, peer_public_key_pem: str) -> str:
|
||||
"""
|
||||
Encrypts a message using X25519 exchange + HKDF + AESGCM.
|
||||
Returns a base64 encoded string containing ephemeral public key, IV, and ciphertext.
|
||||
"""
|
||||
if not peer_public_key_pem:
|
||||
raise ValueError("Peer public key is empty.")
|
||||
|
||||
peer_public_key = serialization.load_pem_public_key(peer_public_key_pem.encode('utf-8'))
|
||||
|
||||
# Generate an ephemeral keypair for this message to provide forward secrecy
|
||||
ephemeral_private_key = x25519.X25519PrivateKey.generate()
|
||||
ephemeral_public_key = ephemeral_private_key.public_key()
|
||||
ephemeral_pub_bytes = ephemeral_public_key.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
|
||||
# Perform key exchange
|
||||
shared_key = ephemeral_private_key.exchange(peer_public_key)
|
||||
|
||||
# Derive a symmetric key using HKDF
|
||||
derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=None,
|
||||
info=b'sms-secure-encryption'
|
||||
).derive(shared_key)
|
||||
|
||||
# Encrypt with AES-GCM
|
||||
aesgcm = AESGCM(derived_key)
|
||||
nonce = os.urandom(12)
|
||||
ciphertext = aesgcm.encrypt(nonce, message.encode('utf-8'), None)
|
||||
|
||||
# Construct final payload: [Ephemeral Pub Key (32)] + [Nonce (12)] + [Ciphertext + Tag]
|
||||
payload = ephemeral_pub_bytes + nonce + ciphertext
|
||||
|
||||
# We prepend a marker to easily identify secure messages
|
||||
marker = "SEC:"
|
||||
b64_payload = base64.b64encode(payload).decode('ascii')
|
||||
|
||||
return marker + b64_payload
|
||||
|
||||
def decrypt_message(self, secure_payload: str) -> str:
|
||||
"""
|
||||
Decrypts a secure payload.
|
||||
Assumes it starts with 'SEC:'.
|
||||
"""
|
||||
if not secure_payload.startswith("SEC:"):
|
||||
raise ValueError("Not a secure message format.")
|
||||
|
||||
b64_payload = secure_payload[4:]
|
||||
try:
|
||||
payload = base64.b64decode(b64_payload)
|
||||
except Exception:
|
||||
raise ValueError("Invalid Base64 payload.")
|
||||
|
||||
if len(payload) < 32 + 12 + 16: # Length of pub key + nonce + tag
|
||||
raise ValueError("Payload too short.")
|
||||
|
||||
ephemeral_pub_bytes = payload[:32]
|
||||
nonce = payload[32:44]
|
||||
ciphertext = payload[44:]
|
||||
|
||||
ephemeral_public_key = x25519.X25519PublicKey.from_public_bytes(ephemeral_pub_bytes)
|
||||
|
||||
# Key exchange
|
||||
shared_key = self.private_key.exchange(ephemeral_public_key)
|
||||
|
||||
# Derive symmetric key
|
||||
derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=None,
|
||||
info=b'sms-secure-encryption'
|
||||
).derive(shared_key)
|
||||
|
||||
# Decrypt
|
||||
aesgcm = AESGCM(derived_key)
|
||||
try:
|
||||
plaintext = aesgcm.decrypt(nonce, ciphertext, None)
|
||||
return plaintext.decode('utf-8')
|
||||
except Exception as e:
|
||||
raise ValueError("Decryption failed. Invalid key or message tampered.") from e
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test
|
||||
alice = CryptoEngine()
|
||||
alice_pub = alice.get_my_public_key_pem()
|
||||
|
||||
bob = CryptoEngine()
|
||||
bob_pub = bob.get_my_public_key_pem()
|
||||
|
||||
msg = "This is a highly secret message!"
|
||||
|
||||
# Alice sends to Bob
|
||||
encrypted = alice.encrypt_message(msg, bob_pub)
|
||||
print("Encrypted:", encrypted)
|
||||
|
||||
# Bob decrypts
|
||||
decrypted = bob.decrypt_message(encrypted)
|
||||
print("Decrypted:", decrypted)
|
||||
|
||||
assert msg == decrypted
|
||||
print("Crypto Engine OK.")
|
||||
134
DB_Handler.py
134
DB_Handler.py
|
|
@ -1,134 +0,0 @@
|
|||
import sqlite3
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
DB_FILE = "sms_app.db"
|
||||
|
||||
class DBHandler:
|
||||
def __init__(self, db_path=DB_FILE):
|
||||
self.db_path = db_path
|
||||
self._initialize_db()
|
||||
|
||||
def _get_connection(self):
|
||||
return sqlite3.connect(self.db_path)
|
||||
|
||||
def _initialize_db(self):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
# Create contacts table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT UNIQUE NOT NULL,
|
||||
public_key TEXT
|
||||
)
|
||||
''')
|
||||
# Create messages table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
is_secure INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
FOREIGN KEY(phone) REFERENCES contacts(phone)
|
||||
)
|
||||
''')
|
||||
# Create settings table for our own keypair if needed (though usually we save keys to files)
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
|
||||
# --- Contacts Methods ---
|
||||
def add_contact(self, name, phone, public_key=None):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute('INSERT INTO contacts (name, phone, public_key) VALUES (?, ?, ?)',
|
||||
(name, phone, public_key))
|
||||
conn.commit()
|
||||
return True
|
||||
except sqlite3.IntegrityError:
|
||||
return False # Phone number already exists
|
||||
|
||||
def update_contact_key(self, phone, public_key):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE contacts SET public_key = ? WHERE phone = ?', (public_key, phone))
|
||||
conn.commit()
|
||||
|
||||
def get_contact(self, phone):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name, phone, public_key FROM contacts WHERE phone = ?', (phone,))
|
||||
return cursor.fetchone()
|
||||
|
||||
def get_all_contacts(self):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT name, phone, public_key FROM contacts')
|
||||
return cursor.fetchall()
|
||||
|
||||
def set_contact_name_if_not_exists(self, phone, name):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT phone FROM contacts WHERE phone = ?', (phone,))
|
||||
if not cursor.fetchone():
|
||||
cursor.execute('INSERT INTO contacts (name, phone, public_key) VALUES (?, ?, ?)', (name, phone, None))
|
||||
conn.commit()
|
||||
|
||||
# --- Messages Methods ---
|
||||
def add_message(self, phone, text, is_secure, status):
|
||||
date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO messages (phone, text, date, is_secure, status)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (phone, text, date_str, int(is_secure), status))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def get_messages_for_contact(self, phone):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT id, phone, text, date, is_secure, status
|
||||
FROM messages
|
||||
WHERE phone = ?
|
||||
ORDER BY date ASC
|
||||
''', (phone,))
|
||||
return cursor.fetchall()
|
||||
|
||||
def delete_message(self, msg_id):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM messages WHERE id = ?', (msg_id,))
|
||||
conn.commit()
|
||||
|
||||
# --- Settings Methods ---
|
||||
def get_setting(self, key):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT value FROM settings WHERE key = ?', (key,))
|
||||
row = cursor.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
def set_setting(self, key, value):
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)', (key, value))
|
||||
conn.commit()
|
||||
|
||||
if __name__ == "__main__":
|
||||
db = DBHandler("test_sms.db")
|
||||
db.add_contact("Alice", "+1234567890", "PUBLIC_KEY_CONTENT")
|
||||
db.add_message("+1234567890", "Hello Alice!", is_secure=False, status="sent")
|
||||
print("Database OK.")
|
||||
os.remove("test_sms.db")
|
||||
218
GSM_Manager.py
218
GSM_Manager.py
|
|
@ -1,218 +0,0 @@
|
|||
import serial
|
||||
import time
|
||||
import threading
|
||||
import re
|
||||
|
||||
class GSMManager:
|
||||
def __init__(self, port='COM1', baudrate=115200, message_callback=None):
|
||||
"""
|
||||
message_callback: function(sender_number, message_text)
|
||||
called when a new SMS is fully read.
|
||||
"""
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self.serial_conn = None
|
||||
self.is_running = False
|
||||
self.read_thread = None
|
||||
self.message_callback = message_callback
|
||||
|
||||
# Lock for thread-safe AT command execution
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def connect(self):
|
||||
try:
|
||||
self.serial_conn = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
timeout=1 # 1 second timeout for readline
|
||||
)
|
||||
self.is_running = True
|
||||
|
||||
# Setup Modem
|
||||
self.send_at_cmd('AT')
|
||||
self.send_at_cmd('AT+CMGF=1') # Text Mode
|
||||
self.send_at_cmd('AT+CNMI=2,1,0,0,0') # Route URC to TE for +CMTI
|
||||
|
||||
# Start Read Thread
|
||||
self.read_thread = threading.Thread(target=self._read_loop, daemon=True)
|
||||
self.read_thread.start()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to connect GSM: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
self.is_running = False
|
||||
if self.serial_conn and self.serial_conn.is_open:
|
||||
self.serial_conn.close()
|
||||
if self.read_thread:
|
||||
self.read_thread.join(timeout=2)
|
||||
|
||||
def send_at_cmd(self, cmd, expected_resp='OK', timeout=2):
|
||||
"""Send an AT command safely and wait for a response."""
|
||||
with self.lock:
|
||||
if not self.serial_conn or not self.serial_conn.is_open:
|
||||
return False, "Not connected"
|
||||
|
||||
# Flush input buffer to clear old data
|
||||
self.serial_conn.reset_input_buffer()
|
||||
|
||||
full_cmd = cmd + '\r\n'
|
||||
self.serial_conn.write(full_cmd.encode('ascii'))
|
||||
|
||||
start_time = time.time()
|
||||
response_lines = []
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
if self.serial_conn.in_waiting:
|
||||
line = self.serial_conn.readline().decode('ascii', errors='ignore').strip()
|
||||
if line:
|
||||
response_lines.append(line)
|
||||
if expected_resp in line or 'ERROR' in line:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.05)
|
||||
|
||||
return expected_resp in "\n".join(response_lines), response_lines
|
||||
|
||||
def send_sms(self, phone_number, message):
|
||||
"""
|
||||
Sends an SMS. For long messages, splits them up.
|
||||
(Note: Quectel M66 supports up to 160 chars in 7-bit text mode natively.
|
||||
For simplicity, we chunk it to 150 chars max per SMS if it exceeds).
|
||||
"""
|
||||
chunk_size = 140 # Safe limit for standard ASCII/Base64
|
||||
chunks = [message[i:i+chunk_size] for i in range(0, len(message), chunk_size)]
|
||||
|
||||
for idx, chunk in enumerate(chunks):
|
||||
# If multiple chunks, we can prepend (1/2) kind of indicator for normal texts
|
||||
# But for base64 secure chunks, they might need stitching.
|
||||
# For simplicity, we just send them sequentially.
|
||||
# In a production resilient app, multipart PDU mode is preferred.
|
||||
|
||||
success = self._send_single_sms(phone_number, chunk)
|
||||
if not success:
|
||||
return False
|
||||
|
||||
if len(chunks) > 1:
|
||||
time.sleep(2) # Add some delay between multi-part sends
|
||||
|
||||
return True
|
||||
|
||||
def _send_single_sms(self, phone_number, text):
|
||||
with self.lock:
|
||||
try:
|
||||
self.serial_conn.reset_input_buffer()
|
||||
|
||||
# Start SMS prompt
|
||||
self.serial_conn.write(f'AT+CMGS="{phone_number}"\r\n'.encode('ascii'))
|
||||
|
||||
# Wait for '>'
|
||||
start_time = time.time()
|
||||
prompt_ready = False
|
||||
while time.time() - start_time < 2:
|
||||
if self.serial_conn.in_waiting:
|
||||
char = self.serial_conn.read().decode('ascii', errors='ignore')
|
||||
if char == '>':
|
||||
prompt_ready = True
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
if not prompt_ready:
|
||||
print("SMS prompt '>' not received.")
|
||||
# Send ESC just in case
|
||||
self.serial_conn.write(chr(27).encode('ascii'))
|
||||
return False
|
||||
|
||||
# Write text and send Ctrl+Z (ASCII 26)
|
||||
self.serial_conn.write(text.encode('ascii') + chr(26).encode('ascii'))
|
||||
|
||||
# Wait for +CMGS and OK
|
||||
start_time = time.time()
|
||||
success = False
|
||||
response_lines = []
|
||||
while time.time() - start_time < 10: # SMS sending can take time
|
||||
if self.serial_conn.in_waiting:
|
||||
line = self.serial_conn.readline().decode('ascii', errors='ignore').strip()
|
||||
if line:
|
||||
response_lines.append(line)
|
||||
if 'OK' in line:
|
||||
success = True
|
||||
break
|
||||
elif 'ERROR' in line:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
return success
|
||||
except Exception as e:
|
||||
print(f"Error sending SMS: {e}")
|
||||
return False
|
||||
|
||||
def _read_loop(self):
|
||||
"""Background thread continuously reading URCs like +CMTI"""
|
||||
while self.is_running:
|
||||
try:
|
||||
if self.serial_conn and self.serial_conn.in_waiting:
|
||||
# We only read if we can acquire the lock without waiting,
|
||||
# otherwise it means AT command is in progress and it handles reading.
|
||||
# Wait, AT command reading in `send_at_cmd` reads synchronously. URCs might mix.
|
||||
# Standard practice for URCs: the AT command sender ignores unexpected URCs,
|
||||
# OR we have a dedicated thread that reads everything and routes URCs vs command responses.
|
||||
# For simplicity, we try to grab the lock. If locked, AT comm is happening.
|
||||
if self.lock.acquire(blocking=False):
|
||||
try:
|
||||
line = self.serial_conn.readline().decode('ascii', errors='ignore').strip()
|
||||
if line.startswith('+CMTI:'):
|
||||
# Incoming message: +CMTI: "SM",1
|
||||
match = re.search(r'\+CMTI:\s*".*?",(\d+)', line)
|
||||
if match:
|
||||
msg_index = int(match.group(1))
|
||||
# Process it in a new thread or queue to avoid blocking this loop
|
||||
threading.Thread(target=self.process_incoming_sms, args=(msg_index,), daemon=True).start()
|
||||
finally:
|
||||
self.lock.release()
|
||||
except Exception as e:
|
||||
pass
|
||||
time.sleep(0.1)
|
||||
|
||||
def process_incoming_sms(self, index):
|
||||
"""Reads the SMS payload from memory and deletes it."""
|
||||
# read_sms uses AT command, so it uses the lock
|
||||
# wait a bit ensuring any ongoing AT comm finishes
|
||||
time.sleep(1)
|
||||
|
||||
success, response = self.send_at_cmd(f'AT+CMGR={index}', expected_resp='OK', timeout=3)
|
||||
if success:
|
||||
sender = "Unknown"
|
||||
text_lines = []
|
||||
is_text_block = False
|
||||
|
||||
for line in response:
|
||||
if line.startswith('+CMGR:'):
|
||||
# +CMGR: "REC UNREAD","+989123456789",,"23/05/26,10:30:00+14"
|
||||
parts = line.split(',')
|
||||
if len(parts) >= 2:
|
||||
sender = parts[1].strip('"')
|
||||
is_text_block = True
|
||||
elif is_text_block and line not in ['OK', 'ERROR'] and not line.startswith('+CMGR:'):
|
||||
text_lines.append(line)
|
||||
|
||||
full_text = "\n".join(text_lines)
|
||||
|
||||
if self.message_callback:
|
||||
self.message_callback(sender, full_text)
|
||||
|
||||
# Delete message to free space
|
||||
self.send_at_cmd(f'AT+CMGD={index}')
|
||||
|
||||
if __name__ == "__main__":
|
||||
def on_sms(sender, text):
|
||||
print(f"\n[NEW SMS] From: {sender}\nText:{text}\n")
|
||||
|
||||
gsm = GSMManager(port='COM3')
|
||||
print("Testing locally...")
|
||||
# This will obviously fail without actual M66 module on COM3, but acts as boilerplate check.
|
||||
# gsm.connect()
|
||||
# gsm.disconnect()
|
||||
2
main.py
2
main.py
|
|
@ -258,7 +258,7 @@ def main():
|
|||
return 1
|
||||
|
||||
try:
|
||||
from secure_sms.controller import AppController
|
||||
from secure_sms.application.controller import AppController
|
||||
from secure_sms.ui import SecureSmsApp
|
||||
|
||||
controller = AppController()
|
||||
|
|
|
|||
70
refactor.py
Normal file
70
refactor.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
def main():
|
||||
base = Path(r"c:\Users\Pars\Desktop\saba-python\secure_sms")
|
||||
|
||||
print("[1/3] Creating Architectural Directories...")
|
||||
for folder in ['core', 'application', 'infrastructure', 'ui']:
|
||||
(base / folder).mkdir(exist_ok=True)
|
||||
(base / folder / '__init__.py').touch(exist_ok=True)
|
||||
|
||||
print("[2/3] Moving Domain Logic Files...")
|
||||
moves = {
|
||||
'models.py': 'core',
|
||||
'protocol.py': 'core',
|
||||
'security.py': 'core',
|
||||
'services.py': 'application',
|
||||
'controller.py': 'application',
|
||||
'database.py': 'infrastructure',
|
||||
'gsm.py': 'infrastructure'
|
||||
}
|
||||
|
||||
for file_name, folder in moves.items():
|
||||
src = base / file_name
|
||||
dst = base / folder / file_name
|
||||
if src.exists():
|
||||
shutil.move(str(src), str(dst))
|
||||
print(f' -> Moved {file_name} to {folder}/')
|
||||
|
||||
print("[3/3] Refactoring Import Statements...")
|
||||
replacements = {
|
||||
'from secure_sms.infrastructure.database': 'from secure_sms.infrastructure.database',
|
||||
'from secure_sms.infrastructure.gsm': 'from secure_sms.infrastructure.gsm',
|
||||
'from secure_sms.application.services': 'from secure_sms.application.services',
|
||||
'from secure_sms.application.controller': 'from secure_sms.application.controller',
|
||||
'from secure_sms.core.models': 'from secure_sms.core.models',
|
||||
'from secure_sms.core.protocol': 'from secure_sms.core.protocol',
|
||||
'from secure_sms.core.security': 'from secure_sms.core.security',
|
||||
'import secure_sms.infrastructure.database': 'import secure_sms.infrastructure.database'
|
||||
}
|
||||
|
||||
project_root = Path(r"c:\Users\Pars\Desktop\saba-python")
|
||||
for py_file in project_root.rglob('*.py'):
|
||||
if py_file.name == '.venv' or '.runtime-venv' in str(py_file):
|
||||
continue
|
||||
try:
|
||||
content = py_file.read_text(encoding='utf-8')
|
||||
orig = content
|
||||
for old, new in replacements.items():
|
||||
content = content.replace(old, new)
|
||||
if content != orig:
|
||||
py_file.write_text(content, encoding='utf-8')
|
||||
print(f' -> Updated imports in {py_file.name}')
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# Cleanup obsolete root files
|
||||
print("[Cleanup] Removing Legacy Files...")
|
||||
legacy = ["App_GUI.py", "Crypto_Engine.py", "DB_Handler.py", "GSM_Manager.py"]
|
||||
for legacy_file in legacy:
|
||||
f = project_root / legacy_file
|
||||
if f.exists():
|
||||
f.unlink()
|
||||
print(f' -> Deleted {legacy_file}')
|
||||
|
||||
print("\n✅ Migration complete! Backend is now strictly adhering to Clean Architecture.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
0
secure_sms/application/__init__.py
Normal file
0
secure_sms/application/__init__.py
Normal file
BIN
secure_sms/application/__pycache__/controller.cpython-313.pyc
Normal file
BIN
secure_sms/application/__pycache__/controller.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/application/__pycache__/services.cpython-313.pyc
Normal file
BIN
secure_sms/application/__pycache__/services.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -1,18 +1,48 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import queue
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
from secure_sms.database import Database
|
||||
from secure_sms.gsm import GSMGateway
|
||||
from secure_sms.services import SecureMessagingService
|
||||
from secure_sms.infrastructure.database import Database
|
||||
from secure_sms.infrastructure.gsm import IMessageGateway, GSMGateway
|
||||
from secure_sms.application.services import SecureMessagingService
|
||||
|
||||
|
||||
class AppController:
|
||||
def __init__(self):
|
||||
self.db = Database()
|
||||
self.service = SecureMessagingService(self.db)
|
||||
self.gsm: Optional[GSMGateway] = None
|
||||
self.gsm: Optional[IMessageGateway] = None
|
||||
self.ui = None
|
||||
self._outbox = queue.Queue()
|
||||
self._worker_thread = threading.Thread(target=self._outbox_worker, daemon=True)
|
||||
self._worker_thread.start()
|
||||
|
||||
def _outbox_worker(self):
|
||||
while True:
|
||||
try:
|
||||
task = self._outbox.get()
|
||||
if task is None:
|
||||
break
|
||||
phone = task.get("phone")
|
||||
frames = task.get("frames")
|
||||
msg_id = task.get("msg_id")
|
||||
|
||||
if self.gsm and self.gsm.is_connected:
|
||||
sent = self.gsm.send_frames(phone, frames)
|
||||
state = "sent" if sent else "failed"
|
||||
else:
|
||||
# In simulation mode (no modem attached), we instantly mark it simul-sent.
|
||||
sent = True
|
||||
state = "simulated"
|
||||
|
||||
if msg_id:
|
||||
self.db.update_message_transport_state(msg_id, state)
|
||||
self._notify_ui(phone)
|
||||
self._outbox.task_done()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def bind_ui(self, ui):
|
||||
self.ui = ui
|
||||
|
|
@ -74,40 +104,22 @@ class AppController:
|
|||
except Exception as exc:
|
||||
return False, str(exc)
|
||||
|
||||
if self.gsm and self.gsm.is_connected:
|
||||
sent = self.gsm.send_frames(phone, frames)
|
||||
state = "sent" if sent else "failed"
|
||||
else:
|
||||
sent = True
|
||||
state = "simulated"
|
||||
|
||||
if sent:
|
||||
self.service.store_outgoing_message(phone, text, mode, state)
|
||||
msg_id = self.service.store_outgoing_message(phone, text, mode, "queued")
|
||||
self._outbox.put({"phone": phone, "frames": frames, "msg_id": msg_id})
|
||||
self._notify_ui(phone)
|
||||
return True, state
|
||||
return False, "failed"
|
||||
return True, "queued"
|
||||
|
||||
def request_secure(self, phone: str) -> tuple[bool, str]:
|
||||
frames = self.service.request_secure_channel(phone)
|
||||
if self.gsm and self.gsm.is_connected:
|
||||
ok = self.gsm.send_frames(phone, frames)
|
||||
status = "sent" if ok else "failed"
|
||||
else:
|
||||
ok = True
|
||||
status = "simulated"
|
||||
self._outbox.put({"phone": phone, "frames": frames, "msg_id": None})
|
||||
self._notify_ui(phone)
|
||||
return ok, status
|
||||
return True, "queued"
|
||||
|
||||
def switch_to_normal(self, phone: str) -> tuple[bool, str]:
|
||||
frames = self.service.request_normal_mode(phone)
|
||||
if self.gsm and self.gsm.is_connected:
|
||||
ok = self.gsm.send_frames(phone, frames)
|
||||
status = "sent" if ok else "failed"
|
||||
else:
|
||||
ok = True
|
||||
status = "simulated"
|
||||
self._outbox.put({"phone": phone, "frames": frames, "msg_id": None})
|
||||
self._notify_ui(phone)
|
||||
return ok, status
|
||||
return True, "queued"
|
||||
|
||||
def get_admin_snapshot(self) -> dict:
|
||||
snapshot = self.service.get_admin_snapshot()
|
||||
|
|
@ -2,9 +2,9 @@ import platform
|
|||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from secure_sms.database import Database, utc_now
|
||||
from secure_sms.models import ContactDetails, ContactSummary, MessageView, PendingPacketView, SecureEventView
|
||||
from secure_sms.protocol import (
|
||||
from secure_sms.infrastructure.database import Database, utc_now
|
||||
from secure_sms.core.models import ContactDetails, ContactSummary, MessageView, PendingPacketView, SecureEventView
|
||||
from secure_sms.core.protocol import (
|
||||
build_control_frames,
|
||||
build_message_frames,
|
||||
decode_control_payload,
|
||||
|
|
@ -12,7 +12,7 @@ from secure_sms.protocol import (
|
|||
encode_plain_body,
|
||||
parse_frame,
|
||||
)
|
||||
from secure_sms.security import ECCCryptoService, PasswordManager, StorageCipher
|
||||
from secure_sms.core.security import ECCCryptoService, PasswordManager, StorageCipher
|
||||
|
||||
|
||||
SYSTEM_CONTACT_LABEL = "مخاطب ناشناس"
|
||||
|
|
@ -183,8 +183,8 @@ class SecureMessagingService:
|
|||
encoded_payload = encode_plain_body(text)
|
||||
return build_message_frames("N", encoded_payload), "normal"
|
||||
|
||||
def store_outgoing_message(self, phone: str, text: str, mode: str, transport_state: str):
|
||||
self.db.add_message(
|
||||
def store_outgoing_message(self, phone: str, text: str, mode: str, transport_state: str) -> int:
|
||||
return self.db.add_message(
|
||||
phone=phone,
|
||||
direction="out",
|
||||
body_enc=self._enc(text),
|
||||
0
secure_sms/core/__init__.py
Normal file
0
secure_sms/core/__init__.py
Normal file
|
|
@ -3,7 +3,7 @@ import uuid
|
|||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from secure_sms.security import b64u_decode, b64u_encode
|
||||
from secure_sms.core.security import b64u_decode, b64u_encode
|
||||
|
||||
|
||||
FRAME_PREFIX = "@SSM1"
|
||||
0
secure_sms/infrastructure/__init__.py
Normal file
0
secure_sms/infrastructure/__init__.py
Normal file
|
|
@ -3,7 +3,7 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from secure_sms.security import SecurityMetadata, StorageCipher
|
||||
from secure_sms.core.security import SecurityMetadata, StorageCipher
|
||||
|
||||
|
||||
DB_FILE = "secure_sms_v2.db"
|
||||
|
|
@ -279,6 +279,14 @@ class Database:
|
|||
conn.commit()
|
||||
return int(cursor.lastrowid)
|
||||
|
||||
def update_message_transport_state(self, message_id: int, transport_state: str):
|
||||
with self._connect() as conn:
|
||||
conn.execute(
|
||||
"UPDATE messages SET transport_state = ? WHERE id = ?",
|
||||
(transport_state, message_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def list_message_rows(self, phone: str) -> list[sqlite3.Row]:
|
||||
with self._connect() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import abc
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
|
@ -6,7 +7,22 @@ from typing import Callable, Optional
|
|||
import serial
|
||||
|
||||
|
||||
class GSMGateway:
|
||||
class IMessageGateway(abc.ABC):
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def is_connected(self) -> bool: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def connect(self) -> bool: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def disconnect(self) -> None: pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def send_frames(self, phone: str, frames: list[str]) -> bool: pass
|
||||
|
||||
|
||||
class GSMGateway(IMessageGateway):
|
||||
def __init__(
|
||||
self,
|
||||
port: str,
|
||||
314
secure_sms/ui.py
314
secure_sms/ui.py
|
|
@ -12,30 +12,32 @@ except ImportError:
|
|||
get_display = None
|
||||
|
||||
|
||||
ctk.set_appearance_mode("light")
|
||||
ctk.set_appearance_mode("dark")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
|
||||
PRIMARY = "#175B4B"
|
||||
PRIMARY_DARK = "#0E4236"
|
||||
PRIMARY_SOFT = "#DFF1E8"
|
||||
ACCENT = "#E8A04D"
|
||||
ACCENT_DARK = "#C97E2D"
|
||||
BACKGROUND = "#F5EFE7"
|
||||
CARD = "#FFFDFC"
|
||||
SURFACE = "#FBF7F2"
|
||||
INPUT_BG = "#FFFCF8"
|
||||
TEXT = "#16312A"
|
||||
MUTED = "#6B7A77"
|
||||
DANGER = "#B6465F"
|
||||
WARNING = "#9A6C3C"
|
||||
BORDER = "#E5DCCE"
|
||||
KEYBOARD_BG = "#D4DCE2"
|
||||
KEY_FACE = "#FFFFFF"
|
||||
KEY_MUTED = "#BCC1C9"
|
||||
KEY_TEXT = "#000000"
|
||||
SIDEBAR = "#1B5A4A"
|
||||
SIDEBAR_SOFT = "#245E4E"
|
||||
PRIMARY = "#2AABEE"
|
||||
PRIMARY_DARK = "#229ED9"
|
||||
PRIMARY_SOFT = "#1C3A4F"
|
||||
ACCENT = "#2AABEE"
|
||||
ACCENT_DARK = "#229ED9"
|
||||
BACKGROUND = "#0E1621"
|
||||
CARD = "#17212B"
|
||||
SURFACE = "#17212B"
|
||||
INPUT_BG = "#242F3D"
|
||||
TEXT = "#FFFFFF"
|
||||
MUTED = "#6C7883"
|
||||
DANGER = "#E05D57"
|
||||
WARNING = "#E0A356"
|
||||
BORDER = "#232E3C"
|
||||
KEYBOARD_BG = "#17212B"
|
||||
KEY_FACE = "#242F3D"
|
||||
KEY_MUTED = "#1C2733"
|
||||
KEY_TEXT = "#FFFFFF"
|
||||
SIDEBAR = "#17212B"
|
||||
SIDEBAR_SOFT = "#242F3D"
|
||||
BUBBLE_OUT = "#2B5278"
|
||||
BUBBLE_IN = "#182533"
|
||||
FONT_BODY = "Tahoma" if os.name == "nt" else "DejaVu Sans"
|
||||
FONT_TITLE = "Tahoma" if os.name == "nt" else "DejaVu Sans"
|
||||
RTL_PATTERN = re.compile(r"[\u0600-\u06FF]")
|
||||
|
|
@ -560,7 +562,7 @@ class SecureSmsApp(ctk.CTk):
|
|||
|
||||
def _show_lock_screen(self):
|
||||
self._clear_root()
|
||||
frame = ctk.CTkFrame(self.root_frame, fg_color=SURFACE, corner_radius=28, border_width=1, border_color=BORDER)
|
||||
frame = ctk.CTkFrame(self.root_frame, fg_color=CARD, corner_radius=16, border_width=1, border_color=BORDER)
|
||||
frame.place(
|
||||
relx=0.5,
|
||||
rely=0.5,
|
||||
|
|
@ -574,55 +576,69 @@ class SecureSmsApp(ctk.CTk):
|
|||
if not self.controller.is_bootstrapped()
|
||||
else 'برای ورود، رمز اصلی برنامه را وارد کن.'
|
||||
)
|
||||
RTLLabel(
|
||||
frame,
|
||||
text='📨 صبا',
|
||||
font=ctk.CTkFont(family=FONT_TITLE, size=32, weight="bold"),
|
||||
text_color=PRIMARY,
|
||||
).pack(pady=(24, 4))
|
||||
RTLLabel(
|
||||
frame,
|
||||
text=title,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=28, weight="bold"),
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=20, weight="bold"),
|
||||
text_color=TEXT,
|
||||
).pack(pady=(30, 10))
|
||||
).pack(pady=(4, 6))
|
||||
RTLLabel(
|
||||
frame,
|
||||
text=subtitle,
|
||||
wraplength=min(self.window_width - 110, 420),
|
||||
justify="right",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14),
|
||||
text_color=MUTED,
|
||||
).pack(padx=28)
|
||||
self.password_entry = RTLEntry(
|
||||
frame,
|
||||
placeholder_text='رمز اصلی',
|
||||
show="*",
|
||||
height=48,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=17),
|
||||
height=44,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
||||
fg_color=INPUT_BG,
|
||||
border_color=BORDER,
|
||||
text_color=TEXT,
|
||||
)
|
||||
self.password_entry.pack(fill="x", padx=42, pady=(24, 12))
|
||||
self.password_entry.pack(fill="x", padx=36, pady=(20, 10))
|
||||
self.confirm_entry = None
|
||||
if not self.controller.is_bootstrapped():
|
||||
self.confirm_entry = RTLEntry(
|
||||
frame,
|
||||
placeholder_text='تکرار رمز',
|
||||
show="*",
|
||||
height=48,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=17),
|
||||
height=44,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
||||
fg_color=INPUT_BG,
|
||||
border_color=BORDER,
|
||||
text_color=TEXT,
|
||||
)
|
||||
self.confirm_entry.pack(fill="x", padx=42, pady=8)
|
||||
self.confirm_entry.pack(fill="x", padx=36, pady=6)
|
||||
self.lock_message = RTLLabel(
|
||||
frame,
|
||||
text="",
|
||||
text_color=DANGER,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14 if self.is_portrait else 15),
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=13),
|
||||
)
|
||||
self.lock_message.pack(pady=(6, 10))
|
||||
self.lock_message.pack(pady=(4, 8))
|
||||
action_text = 'شروع برنامه' if not self.controller.is_bootstrapped() else 'ورود'
|
||||
RTLButton(
|
||||
frame,
|
||||
text=action_text,
|
||||
height=48,
|
||||
height=44,
|
||||
corner_radius=8,
|
||||
fg_color=PRIMARY,
|
||||
hover_color=PRIMARY_DARK,
|
||||
text_color="#FFFFFF",
|
||||
command=self._submit_lock_screen,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"),
|
||||
).pack(fill="x", padx=42, pady=(8, 12))
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
|
||||
).pack(fill="x", padx=36, pady=(4, 16))
|
||||
self.password_entry.bind("<Return>", lambda _event: self._submit_lock_screen())
|
||||
self._register_text_input(self.password_entry, title='رمز اصلی', layout="en", submit=self._submit_lock_screen)
|
||||
if self.confirm_entry:
|
||||
|
|
@ -713,66 +729,71 @@ class SecureSmsApp(ctk.CTk):
|
|||
main_row = 1 if self.is_portrait else 0
|
||||
main_column = 0 if self.is_portrait else 1
|
||||
|
||||
self.sidebar = ctk.CTkFrame(self.root_frame, fg_color=SIDEBAR, corner_radius=0)
|
||||
self.sidebar = ctk.CTkFrame(self.root_frame, fg_color=SIDEBAR, corner_radius=0, border_width=0)
|
||||
self.sidebar.grid(row=0, column=0, sticky="nsew")
|
||||
self.sidebar.grid_columnconfigure(0, weight=1)
|
||||
self.sidebar.grid_rowconfigure(5, weight=1)
|
||||
|
||||
sidebar_header = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
||||
sidebar_header.grid(row=0, column=0, padx=14, pady=(12, 2), sticky="ew")
|
||||
sidebar_header.grid_columnconfigure(0, weight=1)
|
||||
RTLLabel(
|
||||
self.sidebar,
|
||||
sidebar_header,
|
||||
text='صبا',
|
||||
text_color="white",
|
||||
text_color=TEXT,
|
||||
font=ctk.CTkFont(family=FONT_TITLE, size=title_size, weight="bold"),
|
||||
).grid(row=0, column=0, padx=20, pady=(16, 2), sticky="e")
|
||||
).grid(row=0, column=0, sticky="e")
|
||||
RTLLabel(
|
||||
self.sidebar,
|
||||
text='پیام\u200cرسان امن و ساده برای کاربر غیر فنی',
|
||||
text_color="#D5E8E1",
|
||||
sidebar_header,
|
||||
text='پیام\u200cرسان امن',
|
||||
text_color=MUTED,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=subtitle_size),
|
||||
).grid(row=1, column=0, padx=20, sticky="e")
|
||||
).grid(row=1, column=0, sticky="e")
|
||||
|
||||
self.connection_badge = RTLLabel(
|
||||
self.sidebar,
|
||||
text="",
|
||||
corner_radius=999,
|
||||
fg_color="#2E7D62",
|
||||
text_color="white",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"),
|
||||
padx=14,
|
||||
pady=8,
|
||||
corner_radius=6,
|
||||
fg_color="#1C3A4F",
|
||||
text_color=MUTED,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=13, weight="bold"),
|
||||
padx=10,
|
||||
pady=6,
|
||||
)
|
||||
self.connection_badge.grid(row=2, column=0, padx=20, pady=(12, 10), sticky="e")
|
||||
self.connection_badge.grid(row=2, column=0, padx=14, pady=(6, 8), sticky="ew")
|
||||
|
||||
top_actions = ctk.CTkFrame(self.sidebar, fg_color="transparent")
|
||||
top_actions.grid(row=3, column=0, padx=14, pady=(0, 8), sticky="ew")
|
||||
top_actions.grid(row=3, column=0, padx=14, pady=(0, 6), sticky="ew")
|
||||
top_actions.grid_columnconfigure((0, 1), weight=1)
|
||||
RTLButton(
|
||||
top_actions,
|
||||
text='مخاطب جدید',
|
||||
text='گفتگوی جدید',
|
||||
command=self._open_contact_dialog,
|
||||
fg_color=ACCENT,
|
||||
text_color="#3A2514",
|
||||
hover_color=ACCENT_DARK,
|
||||
fg_color=PRIMARY,
|
||||
text_color="#FFFFFF",
|
||||
hover_color=PRIMARY_DARK,
|
||||
corner_radius=8,
|
||||
height=action_height,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"),
|
||||
).grid(row=0, column=0, padx=6, sticky="ew")
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
||||
).grid(row=0, column=0, padx=4, sticky="ew")
|
||||
RTLButton(
|
||||
top_actions,
|
||||
text='تنظیمات',
|
||||
text='⚙ تنظیمات',
|
||||
command=self._open_settings_panel,
|
||||
fg_color="#F4EFE9",
|
||||
text_color="#15302B",
|
||||
hover_color="#ECE1D5",
|
||||
fg_color=INPUT_BG,
|
||||
text_color=TEXT,
|
||||
hover_color="#2D3A49",
|
||||
corner_radius=8,
|
||||
height=action_height,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"),
|
||||
).grid(row=0, column=1, padx=6, sticky="ew")
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
||||
).grid(row=0, column=1, padx=4, sticky="ew")
|
||||
|
||||
self.contact_form_card = ctk.CTkFrame(
|
||||
self.sidebar,
|
||||
fg_color=SIDEBAR_SOFT,
|
||||
corner_radius=18,
|
||||
corner_radius=10,
|
||||
border_width=1,
|
||||
border_color="#3B7B66",
|
||||
border_color=BORDER,
|
||||
)
|
||||
self.contact_form_card.grid(row=4, column=0, padx=14, pady=(0, 10), sticky="ew")
|
||||
self.contact_form_card.grid_columnconfigure(0, weight=1)
|
||||
|
|
@ -809,20 +830,22 @@ class SecureSmsApp(ctk.CTk):
|
|||
RTLButton(
|
||||
contact_actions,
|
||||
text='ذخیره',
|
||||
fg_color=ACCENT,
|
||||
text_color="#3A2514",
|
||||
hover_color=ACCENT_DARK,
|
||||
fg_color=PRIMARY,
|
||||
text_color="#FFFFFF",
|
||||
hover_color=PRIMARY_DARK,
|
||||
command=self._save_contact_inline,
|
||||
corner_radius=8,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||
height=40,
|
||||
).grid(row=0, column=0, padx=4, sticky="ew")
|
||||
RTLButton(
|
||||
contact_actions,
|
||||
text='بستن',
|
||||
fg_color="#F4EFE9",
|
||||
fg_color=INPUT_BG,
|
||||
text_color=TEXT,
|
||||
hover_color="#ECE1D5",
|
||||
hover_color="#2D3A49",
|
||||
command=self._hide_contact_form,
|
||||
corner_radius=8,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||
height=40,
|
||||
).grid(row=0, column=1, padx=4, sticky="ew")
|
||||
|
|
@ -839,46 +862,46 @@ class SecureSmsApp(ctk.CTk):
|
|||
self.contacts_frame = RTLScrollableFrame(
|
||||
self.sidebar,
|
||||
height=160 if self.is_portrait else 320,
|
||||
label_text='مخاطب\u200cها',
|
||||
label_font=ctk.CTkFont(family=FONT_BODY, size=17 if self.is_portrait else 18, weight="bold"),
|
||||
label_text='گفتگو\u200cها',
|
||||
label_font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||
fg_color="transparent",
|
||||
)
|
||||
self.contacts_frame.grid(row=5, column=0, padx=12, pady=8, sticky="nsew")
|
||||
self.contacts_frame.grid(row=5, column=0, padx=8, pady=4, sticky="nsew")
|
||||
|
||||
self.main_panel = ctk.CTkFrame(self.root_frame, fg_color=BACKGROUND, corner_radius=0)
|
||||
self.main_panel.grid(row=main_row, column=main_column, sticky="nsew")
|
||||
self.main_panel.grid_rowconfigure(1, weight=1)
|
||||
self.main_panel.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.header_card = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=24, border_width=1, border_color=BORDER)
|
||||
self.header_card.grid(row=0, column=0, padx=outer_pad, pady=(outer_pad, inner_pad), sticky="ew")
|
||||
self.header_card = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=0, border_width=0)
|
||||
self.header_card.grid(row=0, column=0, sticky="ew")
|
||||
self.header_card.grid_columnconfigure(0, weight=1)
|
||||
self.header_card.grid_columnconfigure(1, weight=0)
|
||||
self.chat_title = RTLLabel(
|
||||
self.header_card,
|
||||
text='یک مخاطب را انتخاب کن',
|
||||
text_color=TEXT,
|
||||
font=ctk.CTkFont(family=FONT_TITLE, size=18 if self.is_portrait else 20, weight="bold"),
|
||||
font=ctk.CTkFont(family=FONT_TITLE, size=16 if self.is_portrait else 18, weight="bold"),
|
||||
)
|
||||
self.chat_title.grid(row=0, column=0, padx=22, pady=(18, 4), sticky="e")
|
||||
self.chat_title.grid(row=0, column=0, padx=16, pady=(10, 2), sticky="e")
|
||||
self.chat_subtitle = RTLLabel(
|
||||
self.header_card,
|
||||
text='در اینجا فقط دو حالت داری: عادی یا امن',
|
||||
text_color=MUTED,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14),
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=12),
|
||||
)
|
||||
self.chat_subtitle.grid(row=1, column=0, padx=22, pady=(0, 18), sticky="e")
|
||||
self.chat_subtitle.grid(row=1, column=0, padx=16, pady=(0, 10), sticky="e")
|
||||
self.mode_badge = RTLLabel(
|
||||
self.header_card,
|
||||
text='عادی',
|
||||
fg_color=PRIMARY_SOFT,
|
||||
text_color=PRIMARY,
|
||||
corner_radius=999,
|
||||
padx=20,
|
||||
pady=10,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
||||
corner_radius=6,
|
||||
padx=14,
|
||||
pady=6,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=12, weight="bold"),
|
||||
)
|
||||
self.mode_badge.grid(row=0, column=1, rowspan=2, padx=20, sticky="e")
|
||||
self.mode_badge.grid(row=0, column=1, rowspan=2, padx=14, sticky="e")
|
||||
|
||||
content = ctk.CTkFrame(self.main_panel, fg_color="transparent")
|
||||
content.grid(row=1, column=0, padx=outer_pad, pady=(0, inner_pad), sticky="nsew")
|
||||
|
|
@ -900,7 +923,7 @@ class SecureSmsApp(ctk.CTk):
|
|||
else:
|
||||
self.chat_container.grid(row=0, column=0, sticky="nsew", padx=(0, inner_pad))
|
||||
|
||||
self.profile_card = ctk.CTkFrame(content, fg_color=CARD, corner_radius=24, border_width=1, border_color=BORDER)
|
||||
self.profile_card = ctk.CTkFrame(content, fg_color=CARD, corner_radius=12, border_width=1, border_color=BORDER)
|
||||
self.profile_card.grid(row=1, column=0, sticky="ew") if self.is_portrait else self.profile_card.grid(row=0, column=1, sticky="nsew")
|
||||
self.profile_card.grid_columnconfigure(0, weight=1)
|
||||
self.profile_title_label = RTLLabel(
|
||||
|
|
@ -937,54 +960,60 @@ class SecureSmsApp(ctk.CTk):
|
|||
text='فعال\u200cسازی ارتباط امن',
|
||||
fg_color=PRIMARY,
|
||||
hover_color=PRIMARY_DARK,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
|
||||
text_color="#FFFFFF",
|
||||
corner_radius=8,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||
command=self._toggle_secure_mode,
|
||||
height=48,
|
||||
height=42,
|
||||
)
|
||||
self.secure_button.grid(row=4, column=0, padx=18, pady=(0, 10), sticky="ew")
|
||||
self.secure_button.grid(row=4, column=0, padx=16, pady=(0, 8), sticky="ew")
|
||||
self.normal_button = RTLButton(
|
||||
self.profile_card,
|
||||
text='بازگشت به حالت عادی',
|
||||
fg_color="#F4EFE9",
|
||||
fg_color=INPUT_BG,
|
||||
text_color=TEXT,
|
||||
hover_color="#ECE1D5",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
|
||||
hover_color="#2D3A49",
|
||||
corner_radius=8,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||
command=self._switch_to_normal,
|
||||
height=44,
|
||||
height=40,
|
||||
)
|
||||
self.normal_button.grid(row=5, column=0, padx=18, pady=(0, 16), sticky="ew")
|
||||
self.normal_button.grid(row=5, column=0, padx=16, pady=(0, 14), sticky="ew")
|
||||
self._configure_profile_card_layout()
|
||||
|
||||
composer = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=24, border_width=1, border_color=BORDER)
|
||||
composer.grid(row=2, column=0, padx=outer_pad, pady=(0, outer_pad), sticky="ew")
|
||||
composer = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=0, border_width=0)
|
||||
composer.grid(row=2, column=0, sticky="ew")
|
||||
composer.grid_columnconfigure(0, weight=1)
|
||||
self.message_entry = RTLTextbox(
|
||||
composer,
|
||||
height=72 if self.is_portrait else 82,
|
||||
height=52 if self.is_portrait else 62,
|
||||
fg_color=INPUT_BG,
|
||||
text_color=TEXT,
|
||||
border_width=0,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
||||
corner_radius=10,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15),
|
||||
wrap="word",
|
||||
)
|
||||
self.message_entry.grid(row=0, column=0, padx=(16, inner_pad), pady=14 if self.is_portrait else 16, sticky="ew")
|
||||
self.message_entry.grid(row=0, column=0, padx=(10, 6), pady=8, sticky="ew")
|
||||
actions = ctk.CTkFrame(composer, fg_color="transparent")
|
||||
actions.grid(row=0, column=1, padx=(0, 16), pady=14 if self.is_portrait else 16, sticky="ns")
|
||||
actions.grid(row=0, column=1, padx=(0, 10), pady=8, sticky="ns")
|
||||
self.send_state_label = RTLLabel(
|
||||
actions,
|
||||
text="",
|
||||
text_color=MUTED,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14),
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=11),
|
||||
)
|
||||
self.send_state_label.pack(pady=(4, 8))
|
||||
self.send_state_label.pack(pady=(2, 4))
|
||||
RTLButton(
|
||||
actions,
|
||||
text='ارسال',
|
||||
fg_color=ACCENT,
|
||||
text_color="#3A2514",
|
||||
hover_color=ACCENT_DARK,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"),
|
||||
width=110 if self.is_portrait else 126,
|
||||
text='➤',
|
||||
fg_color=PRIMARY,
|
||||
text_color="#FFFFFF",
|
||||
hover_color=PRIMARY_DARK,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=20, weight="bold"),
|
||||
width=48,
|
||||
height=48,
|
||||
corner_radius=24,
|
||||
command=self._send_message,
|
||||
).pack()
|
||||
self.message_entry.bind("<Control-Return>", lambda _event: self._send_message())
|
||||
|
|
@ -993,7 +1022,7 @@ class SecureSmsApp(ctk.CTk):
|
|||
self.overlay_frame = ctk.CTkFrame(
|
||||
self.main_panel,
|
||||
fg_color=CARD,
|
||||
corner_radius=28,
|
||||
corner_radius=12,
|
||||
border_width=1,
|
||||
border_color=BORDER,
|
||||
)
|
||||
|
|
@ -1015,9 +1044,9 @@ class SecureSmsApp(ctk.CTk):
|
|||
def _refresh_connection_badge(self):
|
||||
modem = self.controller.modem_status()
|
||||
if modem["connected"]:
|
||||
self.connection_badge.configure(text=f"مودم متصل | {modem['port']}", fg_color="#2E7D62")
|
||||
self.connection_badge.configure(text=f"مودم متصل | {modem['port']}", fg_color="#1C3A4F", text_color="#2AABEE")
|
||||
else:
|
||||
self.connection_badge.configure(text=f"مودم آفلاین | {modem['port']}", fg_color="#9A6C3C")
|
||||
self.connection_badge.configure(text=f"مودم آفلاین | {modem['port']}", fg_color="#3A2020", text_color="#E05D57")
|
||||
|
||||
def _refresh_contacts(self):
|
||||
for widget in self.contacts_frame.winfo_children():
|
||||
|
|
@ -1037,15 +1066,15 @@ class SecureSmsApp(ctk.CTk):
|
|||
self.contacts_frame,
|
||||
text=f"{contact.name}\n{contact.phone}\n{contact.last_message_preview or 'آماده گفتگو'}",
|
||||
anchor="e",
|
||||
height=76 if self.is_portrait else 88,
|
||||
corner_radius=20,
|
||||
height=72 if self.is_portrait else 80,
|
||||
corner_radius=8,
|
||||
command=lambda phone=contact.phone: self._select_contact(phone),
|
||||
fg_color="#F7EFE5" if selected else "#2A6956",
|
||||
hover_color="#F0E3D5" if selected else "#32745F",
|
||||
text_color=TEXT if selected else "white",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15),
|
||||
fg_color="#2B5278" if selected else "transparent",
|
||||
hover_color="#2B5278",
|
||||
text_color=TEXT,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14),
|
||||
)
|
||||
card.grid(row=index, column=0, padx=8, pady=6, sticky="ew")
|
||||
card.grid(row=index, column=0, padx=4, pady=2, sticky="ew")
|
||||
|
||||
def _select_contact(self, phone: str):
|
||||
self.current_contact_phone = phone
|
||||
|
|
@ -1093,51 +1122,52 @@ class SecureSmsApp(ctk.CTk):
|
|||
self.chat_container,
|
||||
text='هنوز پیامی ثبت نشده است.\nاز نوار پایین برای نوشتن پیام استفاده کن.',
|
||||
text_color=MUTED,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15)
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14)
|
||||
).pack(pady=40)
|
||||
return
|
||||
|
||||
for message in messages:
|
||||
if message.direction == "system":
|
||||
sys_frame = ctk.CTkFrame(self.chat_container, fg_color="#1C2733", corner_radius=8)
|
||||
sys_frame.pack(pady=6, anchor="center")
|
||||
RTLLabel(
|
||||
self.chat_container,
|
||||
sys_frame,
|
||||
text=message.body,
|
||||
text_color="#8A5C2E",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=13),
|
||||
wraplength=int(self.window_width * 0.7)
|
||||
).pack(pady=12, anchor="center")
|
||||
text_color=MUTED,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=12),
|
||||
wraplength=int(self.window_width * 0.6)
|
||||
).pack(padx=12, pady=6)
|
||||
continue
|
||||
|
||||
is_out = message.direction == "out"
|
||||
bubble_color = "#E1F2E9" if is_out else "#FFFFFF"
|
||||
bubble_color = BUBBLE_OUT if is_out else BUBBLE_IN
|
||||
anchor = "e" if is_out else "w"
|
||||
|
||||
bubble = ctk.CTkFrame(
|
||||
self.chat_container,
|
||||
fg_color=bubble_color,
|
||||
corner_radius=16,
|
||||
border_width=1 if not is_out else 0,
|
||||
border_color=BORDER
|
||||
corner_radius=12,
|
||||
border_width=0,
|
||||
)
|
||||
bubble.pack(anchor=anchor, padx=12, pady=6, fill="none")
|
||||
bubble.pack(anchor=anchor, padx=8, pady=3, fill="none")
|
||||
|
||||
RTLLabel(
|
||||
bubble,
|
||||
text=message.body,
|
||||
text_color="#16312A",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
||||
wraplength=max(200, int(self.window_width * 0.55)),
|
||||
text_color="#FFFFFF",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15),
|
||||
wraplength=max(180, int(self.window_width * 0.5)),
|
||||
justify="right"
|
||||
).pack(padx=16, pady=(12, 4), anchor="e")
|
||||
).pack(padx=12, pady=(8, 2), anchor="e")
|
||||
|
||||
badge_text = f"🛡️ {message.created_at}" if message.mode == "secure" else message.created_at
|
||||
RTLLabel(
|
||||
bubble,
|
||||
text=badge_text,
|
||||
text_color=MUTED,
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=11),
|
||||
text_color="#7A8E9C",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=10),
|
||||
justify="right"
|
||||
).pack(padx=16, pady=(0, 8), anchor="w" if is_out else "e")
|
||||
).pack(padx=12, pady=(0, 6), anchor="w" if is_out else "e")
|
||||
|
||||
try:
|
||||
self.after(50, lambda: self.chat_container._parent_canvas.yview_moveto(1.0))
|
||||
|
|
@ -1255,9 +1285,9 @@ class SecureSmsApp(ctk.CTk):
|
|||
RTLButton(
|
||||
body,
|
||||
text='بازگشت به گفتگو',
|
||||
fg_color="#F1E7DB",
|
||||
fg_color=INPUT_BG,
|
||||
text_color=TEXT,
|
||||
hover_color="#E8D8C7",
|
||||
hover_color="#2D3A49",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||
height=40,
|
||||
command=self._hide_overlay,
|
||||
|
|
@ -1286,9 +1316,9 @@ class SecureSmsApp(ctk.CTk):
|
|||
text='بستن',
|
||||
width=86,
|
||||
height=36,
|
||||
fg_color="#F1E7DB",
|
||||
fg_color=INPUT_BG,
|
||||
text_color=TEXT,
|
||||
hover_color="#E8D8C7",
|
||||
hover_color="#2D3A49",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
||||
command=self._hide_overlay,
|
||||
).grid(row=0, column=1, padx=(8, 0), sticky="e")
|
||||
|
|
@ -1353,9 +1383,9 @@ class SecureSmsApp(ctk.CTk):
|
|||
RTLButton(
|
||||
body,
|
||||
text='بازگشت به تنظیمات',
|
||||
fg_color="#F1E7DB",
|
||||
fg_color=INPUT_BG,
|
||||
text_color=TEXT,
|
||||
hover_color="#E8D8C7",
|
||||
hover_color="#2D3A49",
|
||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||
height=40,
|
||||
command=self._open_settings_panel,
|
||||
|
|
|
|||
0
secure_sms/ui/__init__.py
Normal file
0
secure_sms/ui/__init__.py
Normal file
108
secure_sms/ui/core.py
Normal file
108
secure_sms/ui/core.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import os
|
||||
import re
|
||||
from tkinter import TclError
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
try:
|
||||
import arabic_reshaper
|
||||
from bidi.algorithm import get_display
|
||||
except ImportError:
|
||||
arabic_reshaper = None
|
||||
get_display = None
|
||||
|
||||
|
||||
ctk.set_appearance_mode("light")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
|
||||
PRIMARY = "#175B4B"
|
||||
PRIMARY_DARK = "#0E4236"
|
||||
PRIMARY_SOFT = "#DFF1E8"
|
||||
ACCENT = "#E8A04D"
|
||||
ACCENT_DARK = "#C97E2D"
|
||||
BACKGROUND = "#F5EFE7"
|
||||
CARD = "#FFFDFC"
|
||||
SURFACE = "#FBF7F2"
|
||||
INPUT_BG = "#FFFCF8"
|
||||
TEXT = "#16312A"
|
||||
MUTED = "#6B7A77"
|
||||
DANGER = "#B6465F"
|
||||
WARNING = "#9A6C3C"
|
||||
BORDER = "#E5DCCE"
|
||||
|
||||
KEYBOARD_BG = "#D4DCE2"
|
||||
KEY_FACE = "#FFFFFF"
|
||||
KEY_MUTED = "#BCC1C9"
|
||||
KEY_TEXT = "#000000"
|
||||
|
||||
SIDEBAR = "#1B5A4A"
|
||||
SIDEBAR_SOFT = "#245E4E"
|
||||
|
||||
FONT_BODY = "Tahoma" if os.name == "nt" else "DejaVu Sans"
|
||||
FONT_TITLE = "Tahoma" if os.name == "nt" else "DejaVu Sans"
|
||||
|
||||
RTL_PATTERN = re.compile(r"[\u0600-\u06FF]")
|
||||
|
||||
|
||||
def ui_text(value):
|
||||
if not isinstance(value, str) or not value:
|
||||
return value
|
||||
if arabic_reshaper is None or get_display is None or not RTL_PATTERN.search(value):
|
||||
return value
|
||||
return get_display(arabic_reshaper.reshape(value))
|
||||
|
||||
|
||||
class _RTLTextMixin:
|
||||
@staticmethod
|
||||
def _normalize_kwargs(kwargs):
|
||||
normalized = dict(kwargs)
|
||||
if "text" in normalized:
|
||||
normalized["text"] = ui_text(normalized["text"])
|
||||
if "placeholder_text" in normalized:
|
||||
normalized["placeholder_text"] = ui_text(normalized["placeholder_text"])
|
||||
if "label_text" in normalized:
|
||||
normalized["label_text"] = ui_text(normalized["label_text"])
|
||||
return normalized
|
||||
|
||||
|
||||
class RTLLabel(_RTLTextMixin, ctk.CTkLabel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **self._normalize_kwargs(kwargs))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs))
|
||||
|
||||
|
||||
class RTLButton(_RTLTextMixin, ctk.CTkButton):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("corner_radius", 16)
|
||||
super().__init__(*args, **self._normalize_kwargs(kwargs))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs))
|
||||
|
||||
|
||||
class RTLEntry(_RTLTextMixin, ctk.CTkEntry):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault("justify", "right")
|
||||
kwargs.setdefault("fg_color", INPUT_BG)
|
||||
kwargs.setdefault("border_color", BORDER)
|
||||
kwargs.setdefault("corner_radius", 16)
|
||||
super().__init__(*args, **self._normalize_kwargs(kwargs))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs))
|
||||
|
||||
|
||||
class RTLTextbox(ctk.CTkTextbox):
|
||||
def insert(self, index, text, *tags):
|
||||
return super().insert(index, ui_text(text), *tags)
|
||||
|
||||
|
||||
class RTLScrollableFrame(_RTLTextMixin, ctk.CTkScrollableFrame):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **self._normalize_kwargs(kwargs))
|
||||
|
||||
def configure(self, require_redraw=False, **kwargs):
|
||||
return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs))
|
||||
Loading…
Reference in New Issue
Block a user