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
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from secure_sms.controller import AppController
|
from secure_sms.application.controller import AppController
|
||||||
from secure_sms.ui import SecureSmsApp
|
from secure_sms.ui import SecureSmsApp
|
||||||
|
|
||||||
controller = AppController()
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from secure_sms.database import Database
|
from secure_sms.infrastructure.database import Database
|
||||||
from secure_sms.gsm import GSMGateway
|
from secure_sms.infrastructure.gsm import IMessageGateway, GSMGateway
|
||||||
from secure_sms.services import SecureMessagingService
|
from secure_sms.application.services import SecureMessagingService
|
||||||
|
|
||||||
|
|
||||||
class AppController:
|
class AppController:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.db = Database()
|
self.db = Database()
|
||||||
self.service = SecureMessagingService(self.db)
|
self.service = SecureMessagingService(self.db)
|
||||||
self.gsm: Optional[GSMGateway] = None
|
self.gsm: Optional[IMessageGateway] = None
|
||||||
self.ui = 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):
|
def bind_ui(self, ui):
|
||||||
self.ui = ui
|
self.ui = ui
|
||||||
|
|
@ -74,40 +104,22 @@ class AppController:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return False, str(exc)
|
return False, str(exc)
|
||||||
|
|
||||||
if self.gsm and self.gsm.is_connected:
|
msg_id = self.service.store_outgoing_message(phone, text, mode, "queued")
|
||||||
sent = self.gsm.send_frames(phone, frames)
|
self._outbox.put({"phone": phone, "frames": frames, "msg_id": msg_id})
|
||||||
state = "sent" if sent else "failed"
|
self._notify_ui(phone)
|
||||||
else:
|
return True, "queued"
|
||||||
sent = True
|
|
||||||
state = "simulated"
|
|
||||||
|
|
||||||
if sent:
|
|
||||||
self.service.store_outgoing_message(phone, text, mode, state)
|
|
||||||
self._notify_ui(phone)
|
|
||||||
return True, state
|
|
||||||
return False, "failed"
|
|
||||||
|
|
||||||
def request_secure(self, phone: str) -> tuple[bool, str]:
|
def request_secure(self, phone: str) -> tuple[bool, str]:
|
||||||
frames = self.service.request_secure_channel(phone)
|
frames = self.service.request_secure_channel(phone)
|
||||||
if self.gsm and self.gsm.is_connected:
|
self._outbox.put({"phone": phone, "frames": frames, "msg_id": None})
|
||||||
ok = self.gsm.send_frames(phone, frames)
|
|
||||||
status = "sent" if ok else "failed"
|
|
||||||
else:
|
|
||||||
ok = True
|
|
||||||
status = "simulated"
|
|
||||||
self._notify_ui(phone)
|
self._notify_ui(phone)
|
||||||
return ok, status
|
return True, "queued"
|
||||||
|
|
||||||
def switch_to_normal(self, phone: str) -> tuple[bool, str]:
|
def switch_to_normal(self, phone: str) -> tuple[bool, str]:
|
||||||
frames = self.service.request_normal_mode(phone)
|
frames = self.service.request_normal_mode(phone)
|
||||||
if self.gsm and self.gsm.is_connected:
|
self._outbox.put({"phone": phone, "frames": frames, "msg_id": None})
|
||||||
ok = self.gsm.send_frames(phone, frames)
|
|
||||||
status = "sent" if ok else "failed"
|
|
||||||
else:
|
|
||||||
ok = True
|
|
||||||
status = "simulated"
|
|
||||||
self._notify_ui(phone)
|
self._notify_ui(phone)
|
||||||
return ok, status
|
return True, "queued"
|
||||||
|
|
||||||
def get_admin_snapshot(self) -> dict:
|
def get_admin_snapshot(self) -> dict:
|
||||||
snapshot = self.service.get_admin_snapshot()
|
snapshot = self.service.get_admin_snapshot()
|
||||||
|
|
@ -2,9 +2,9 @@ import platform
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from secure_sms.database import Database, utc_now
|
from secure_sms.infrastructure.database import Database, utc_now
|
||||||
from secure_sms.models import ContactDetails, ContactSummary, MessageView, PendingPacketView, SecureEventView
|
from secure_sms.core.models import ContactDetails, ContactSummary, MessageView, PendingPacketView, SecureEventView
|
||||||
from secure_sms.protocol import (
|
from secure_sms.core.protocol import (
|
||||||
build_control_frames,
|
build_control_frames,
|
||||||
build_message_frames,
|
build_message_frames,
|
||||||
decode_control_payload,
|
decode_control_payload,
|
||||||
|
|
@ -12,7 +12,7 @@ from secure_sms.protocol import (
|
||||||
encode_plain_body,
|
encode_plain_body,
|
||||||
parse_frame,
|
parse_frame,
|
||||||
)
|
)
|
||||||
from secure_sms.security import ECCCryptoService, PasswordManager, StorageCipher
|
from secure_sms.core.security import ECCCryptoService, PasswordManager, StorageCipher
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_CONTACT_LABEL = "مخاطب ناشناس"
|
SYSTEM_CONTACT_LABEL = "مخاطب ناشناس"
|
||||||
|
|
@ -183,8 +183,8 @@ class SecureMessagingService:
|
||||||
encoded_payload = encode_plain_body(text)
|
encoded_payload = encode_plain_body(text)
|
||||||
return build_message_frames("N", encoded_payload), "normal"
|
return build_message_frames("N", encoded_payload), "normal"
|
||||||
|
|
||||||
def store_outgoing_message(self, phone: str, text: str, mode: str, transport_state: str):
|
def store_outgoing_message(self, phone: str, text: str, mode: str, transport_state: str) -> int:
|
||||||
self.db.add_message(
|
return self.db.add_message(
|
||||||
phone=phone,
|
phone=phone,
|
||||||
direction="out",
|
direction="out",
|
||||||
body_enc=self._enc(text),
|
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 dataclasses import dataclass
|
||||||
from typing import Optional
|
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"
|
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 pathlib import Path
|
||||||
from typing import Optional
|
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"
|
DB_FILE = "secure_sms_v2.db"
|
||||||
|
|
@ -279,6 +279,14 @@ class Database:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return int(cursor.lastrowid)
|
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]:
|
def list_message_rows(self, phone: str) -> list[sqlite3.Row]:
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import abc
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
@ -6,7 +7,22 @@ from typing import Callable, Optional
|
||||||
import serial
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
port: str,
|
port: str,
|
||||||
314
secure_sms/ui.py
314
secure_sms/ui.py
|
|
@ -12,30 +12,32 @@ except ImportError:
|
||||||
get_display = None
|
get_display = None
|
||||||
|
|
||||||
|
|
||||||
ctk.set_appearance_mode("light")
|
ctk.set_appearance_mode("dark")
|
||||||
ctk.set_default_color_theme("blue")
|
ctk.set_default_color_theme("blue")
|
||||||
|
|
||||||
|
|
||||||
PRIMARY = "#175B4B"
|
PRIMARY = "#2AABEE"
|
||||||
PRIMARY_DARK = "#0E4236"
|
PRIMARY_DARK = "#229ED9"
|
||||||
PRIMARY_SOFT = "#DFF1E8"
|
PRIMARY_SOFT = "#1C3A4F"
|
||||||
ACCENT = "#E8A04D"
|
ACCENT = "#2AABEE"
|
||||||
ACCENT_DARK = "#C97E2D"
|
ACCENT_DARK = "#229ED9"
|
||||||
BACKGROUND = "#F5EFE7"
|
BACKGROUND = "#0E1621"
|
||||||
CARD = "#FFFDFC"
|
CARD = "#17212B"
|
||||||
SURFACE = "#FBF7F2"
|
SURFACE = "#17212B"
|
||||||
INPUT_BG = "#FFFCF8"
|
INPUT_BG = "#242F3D"
|
||||||
TEXT = "#16312A"
|
TEXT = "#FFFFFF"
|
||||||
MUTED = "#6B7A77"
|
MUTED = "#6C7883"
|
||||||
DANGER = "#B6465F"
|
DANGER = "#E05D57"
|
||||||
WARNING = "#9A6C3C"
|
WARNING = "#E0A356"
|
||||||
BORDER = "#E5DCCE"
|
BORDER = "#232E3C"
|
||||||
KEYBOARD_BG = "#D4DCE2"
|
KEYBOARD_BG = "#17212B"
|
||||||
KEY_FACE = "#FFFFFF"
|
KEY_FACE = "#242F3D"
|
||||||
KEY_MUTED = "#BCC1C9"
|
KEY_MUTED = "#1C2733"
|
||||||
KEY_TEXT = "#000000"
|
KEY_TEXT = "#FFFFFF"
|
||||||
SIDEBAR = "#1B5A4A"
|
SIDEBAR = "#17212B"
|
||||||
SIDEBAR_SOFT = "#245E4E"
|
SIDEBAR_SOFT = "#242F3D"
|
||||||
|
BUBBLE_OUT = "#2B5278"
|
||||||
|
BUBBLE_IN = "#182533"
|
||||||
FONT_BODY = "Tahoma" if os.name == "nt" else "DejaVu Sans"
|
FONT_BODY = "Tahoma" if os.name == "nt" else "DejaVu Sans"
|
||||||
FONT_TITLE = "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]")
|
RTL_PATTERN = re.compile(r"[\u0600-\u06FF]")
|
||||||
|
|
@ -560,7 +562,7 @@ class SecureSmsApp(ctk.CTk):
|
||||||
|
|
||||||
def _show_lock_screen(self):
|
def _show_lock_screen(self):
|
||||||
self._clear_root()
|
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(
|
frame.place(
|
||||||
relx=0.5,
|
relx=0.5,
|
||||||
rely=0.5,
|
rely=0.5,
|
||||||
|
|
@ -574,55 +576,69 @@ class SecureSmsApp(ctk.CTk):
|
||||||
if not self.controller.is_bootstrapped()
|
if not self.controller.is_bootstrapped()
|
||||||
else 'برای ورود، رمز اصلی برنامه را وارد کن.'
|
else 'برای ورود، رمز اصلی برنامه را وارد کن.'
|
||||||
)
|
)
|
||||||
|
RTLLabel(
|
||||||
|
frame,
|
||||||
|
text='📨 صبا',
|
||||||
|
font=ctk.CTkFont(family=FONT_TITLE, size=32, weight="bold"),
|
||||||
|
text_color=PRIMARY,
|
||||||
|
).pack(pady=(24, 4))
|
||||||
RTLLabel(
|
RTLLabel(
|
||||||
frame,
|
frame,
|
||||||
text=title,
|
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,
|
text_color=TEXT,
|
||||||
).pack(pady=(30, 10))
|
).pack(pady=(4, 6))
|
||||||
RTLLabel(
|
RTLLabel(
|
||||||
frame,
|
frame,
|
||||||
text=subtitle,
|
text=subtitle,
|
||||||
wraplength=min(self.window_width - 110, 420),
|
wraplength=min(self.window_width - 110, 420),
|
||||||
justify="right",
|
justify="right",
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
font=ctk.CTkFont(family=FONT_BODY, size=14),
|
||||||
text_color=MUTED,
|
text_color=MUTED,
|
||||||
).pack(padx=28)
|
).pack(padx=28)
|
||||||
self.password_entry = RTLEntry(
|
self.password_entry = RTLEntry(
|
||||||
frame,
|
frame,
|
||||||
placeholder_text='رمز اصلی',
|
placeholder_text='رمز اصلی',
|
||||||
show="*",
|
show="*",
|
||||||
height=48,
|
height=44,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=17),
|
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
|
self.confirm_entry = None
|
||||||
if not self.controller.is_bootstrapped():
|
if not self.controller.is_bootstrapped():
|
||||||
self.confirm_entry = RTLEntry(
|
self.confirm_entry = RTLEntry(
|
||||||
frame,
|
frame,
|
||||||
placeholder_text='تکرار رمز',
|
placeholder_text='تکرار رمز',
|
||||||
show="*",
|
show="*",
|
||||||
height=48,
|
height=44,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=17),
|
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(
|
self.lock_message = RTLLabel(
|
||||||
frame,
|
frame,
|
||||||
text="",
|
text="",
|
||||||
text_color=DANGER,
|
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 'ورود'
|
action_text = 'شروع برنامه' if not self.controller.is_bootstrapped() else 'ورود'
|
||||||
RTLButton(
|
RTLButton(
|
||||||
frame,
|
frame,
|
||||||
text=action_text,
|
text=action_text,
|
||||||
height=48,
|
height=44,
|
||||||
|
corner_radius=8,
|
||||||
fg_color=PRIMARY,
|
fg_color=PRIMARY,
|
||||||
hover_color=PRIMARY_DARK,
|
hover_color=PRIMARY_DARK,
|
||||||
|
text_color="#FFFFFF",
|
||||||
command=self._submit_lock_screen,
|
command=self._submit_lock_screen,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
|
||||||
).pack(fill="x", padx=42, pady=(8, 12))
|
).pack(fill="x", padx=36, pady=(4, 16))
|
||||||
self.password_entry.bind("<Return>", lambda _event: self._submit_lock_screen())
|
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)
|
self._register_text_input(self.password_entry, title='رمز اصلی', layout="en", submit=self._submit_lock_screen)
|
||||||
if self.confirm_entry:
|
if self.confirm_entry:
|
||||||
|
|
@ -713,66 +729,71 @@ class SecureSmsApp(ctk.CTk):
|
||||||
main_row = 1 if self.is_portrait else 0
|
main_row = 1 if self.is_portrait else 0
|
||||||
main_column = 0 if self.is_portrait else 1
|
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(row=0, column=0, sticky="nsew")
|
||||||
self.sidebar.grid_columnconfigure(0, weight=1)
|
self.sidebar.grid_columnconfigure(0, weight=1)
|
||||||
self.sidebar.grid_rowconfigure(5, 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(
|
RTLLabel(
|
||||||
self.sidebar,
|
sidebar_header,
|
||||||
text='صبا',
|
text='صبا',
|
||||||
text_color="white",
|
text_color=TEXT,
|
||||||
font=ctk.CTkFont(family=FONT_TITLE, size=title_size, weight="bold"),
|
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(
|
RTLLabel(
|
||||||
self.sidebar,
|
sidebar_header,
|
||||||
text='پیام\u200cرسان امن و ساده برای کاربر غیر فنی',
|
text='پیام\u200cرسان امن',
|
||||||
text_color="#D5E8E1",
|
text_color=MUTED,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=subtitle_size),
|
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.connection_badge = RTLLabel(
|
||||||
self.sidebar,
|
self.sidebar,
|
||||||
text="",
|
text="",
|
||||||
corner_radius=999,
|
corner_radius=6,
|
||||||
fg_color="#2E7D62",
|
fg_color="#1C3A4F",
|
||||||
text_color="white",
|
text_color=MUTED,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=13, weight="bold"),
|
||||||
padx=14,
|
padx=10,
|
||||||
pady=8,
|
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 = 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)
|
top_actions.grid_columnconfigure((0, 1), weight=1)
|
||||||
RTLButton(
|
RTLButton(
|
||||||
top_actions,
|
top_actions,
|
||||||
text='مخاطب جدید',
|
text='گفتگوی جدید',
|
||||||
command=self._open_contact_dialog,
|
command=self._open_contact_dialog,
|
||||||
fg_color=ACCENT,
|
fg_color=PRIMARY,
|
||||||
text_color="#3A2514",
|
text_color="#FFFFFF",
|
||||||
hover_color=ACCENT_DARK,
|
hover_color=PRIMARY_DARK,
|
||||||
|
corner_radius=8,
|
||||||
height=action_height,
|
height=action_height,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
||||||
).grid(row=0, column=0, padx=6, sticky="ew")
|
).grid(row=0, column=0, padx=4, sticky="ew")
|
||||||
RTLButton(
|
RTLButton(
|
||||||
top_actions,
|
top_actions,
|
||||||
text='تنظیمات',
|
text='⚙ تنظیمات',
|
||||||
command=self._open_settings_panel,
|
command=self._open_settings_panel,
|
||||||
fg_color="#F4EFE9",
|
fg_color=INPUT_BG,
|
||||||
text_color="#15302B",
|
text_color=TEXT,
|
||||||
hover_color="#ECE1D5",
|
hover_color="#2D3A49",
|
||||||
|
corner_radius=8,
|
||||||
height=action_height,
|
height=action_height,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
||||||
).grid(row=0, column=1, padx=6, sticky="ew")
|
).grid(row=0, column=1, padx=4, sticky="ew")
|
||||||
|
|
||||||
self.contact_form_card = ctk.CTkFrame(
|
self.contact_form_card = ctk.CTkFrame(
|
||||||
self.sidebar,
|
self.sidebar,
|
||||||
fg_color=SIDEBAR_SOFT,
|
fg_color=SIDEBAR_SOFT,
|
||||||
corner_radius=18,
|
corner_radius=10,
|
||||||
border_width=1,
|
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(row=4, column=0, padx=14, pady=(0, 10), sticky="ew")
|
||||||
self.contact_form_card.grid_columnconfigure(0, weight=1)
|
self.contact_form_card.grid_columnconfigure(0, weight=1)
|
||||||
|
|
@ -809,20 +830,22 @@ class SecureSmsApp(ctk.CTk):
|
||||||
RTLButton(
|
RTLButton(
|
||||||
contact_actions,
|
contact_actions,
|
||||||
text='ذخیره',
|
text='ذخیره',
|
||||||
fg_color=ACCENT,
|
fg_color=PRIMARY,
|
||||||
text_color="#3A2514",
|
text_color="#FFFFFF",
|
||||||
hover_color=ACCENT_DARK,
|
hover_color=PRIMARY_DARK,
|
||||||
command=self._save_contact_inline,
|
command=self._save_contact_inline,
|
||||||
|
corner_radius=8,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||||
height=40,
|
height=40,
|
||||||
).grid(row=0, column=0, padx=4, sticky="ew")
|
).grid(row=0, column=0, padx=4, sticky="ew")
|
||||||
RTLButton(
|
RTLButton(
|
||||||
contact_actions,
|
contact_actions,
|
||||||
text='بستن',
|
text='بستن',
|
||||||
fg_color="#F4EFE9",
|
fg_color=INPUT_BG,
|
||||||
text_color=TEXT,
|
text_color=TEXT,
|
||||||
hover_color="#ECE1D5",
|
hover_color="#2D3A49",
|
||||||
command=self._hide_contact_form,
|
command=self._hide_contact_form,
|
||||||
|
corner_radius=8,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||||
height=40,
|
height=40,
|
||||||
).grid(row=0, column=1, padx=4, sticky="ew")
|
).grid(row=0, column=1, padx=4, sticky="ew")
|
||||||
|
|
@ -839,46 +862,46 @@ class SecureSmsApp(ctk.CTk):
|
||||||
self.contacts_frame = RTLScrollableFrame(
|
self.contacts_frame = RTLScrollableFrame(
|
||||||
self.sidebar,
|
self.sidebar,
|
||||||
height=160 if self.is_portrait else 320,
|
height=160 if self.is_portrait else 320,
|
||||||
label_text='مخاطب\u200cها',
|
label_text='گفتگو\u200cها',
|
||||||
label_font=ctk.CTkFont(family=FONT_BODY, size=17 if self.is_portrait else 18, weight="bold"),
|
label_font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||||
fg_color="transparent",
|
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 = 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(row=main_row, column=main_column, sticky="nsew")
|
||||||
self.main_panel.grid_rowconfigure(1, weight=1)
|
self.main_panel.grid_rowconfigure(1, weight=1)
|
||||||
self.main_panel.grid_columnconfigure(0, 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 = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=0, border_width=0)
|
||||||
self.header_card.grid(row=0, column=0, padx=outer_pad, pady=(outer_pad, inner_pad), sticky="ew")
|
self.header_card.grid(row=0, column=0, sticky="ew")
|
||||||
self.header_card.grid_columnconfigure(0, weight=1)
|
self.header_card.grid_columnconfigure(0, weight=1)
|
||||||
self.header_card.grid_columnconfigure(1, weight=0)
|
self.header_card.grid_columnconfigure(1, weight=0)
|
||||||
self.chat_title = RTLLabel(
|
self.chat_title = RTLLabel(
|
||||||
self.header_card,
|
self.header_card,
|
||||||
text='یک مخاطب را انتخاب کن',
|
text='یک مخاطب را انتخاب کن',
|
||||||
text_color=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.chat_subtitle = RTLLabel(
|
||||||
self.header_card,
|
self.header_card,
|
||||||
text='در اینجا فقط دو حالت داری: عادی یا امن',
|
text='در اینجا فقط دو حالت داری: عادی یا امن',
|
||||||
text_color=MUTED,
|
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.mode_badge = RTLLabel(
|
||||||
self.header_card,
|
self.header_card,
|
||||||
text='عادی',
|
text='عادی',
|
||||||
fg_color=PRIMARY_SOFT,
|
fg_color=PRIMARY_SOFT,
|
||||||
text_color=PRIMARY,
|
text_color=PRIMARY,
|
||||||
corner_radius=999,
|
corner_radius=6,
|
||||||
padx=20,
|
padx=14,
|
||||||
pady=10,
|
pady=6,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
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 = ctk.CTkFrame(self.main_panel, fg_color="transparent")
|
||||||
content.grid(row=1, column=0, padx=outer_pad, pady=(0, inner_pad), sticky="nsew")
|
content.grid(row=1, column=0, padx=outer_pad, pady=(0, inner_pad), sticky="nsew")
|
||||||
|
|
@ -900,7 +923,7 @@ class SecureSmsApp(ctk.CTk):
|
||||||
else:
|
else:
|
||||||
self.chat_container.grid(row=0, column=0, sticky="nsew", padx=(0, inner_pad))
|
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(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_card.grid_columnconfigure(0, weight=1)
|
||||||
self.profile_title_label = RTLLabel(
|
self.profile_title_label = RTLLabel(
|
||||||
|
|
@ -937,54 +960,60 @@ class SecureSmsApp(ctk.CTk):
|
||||||
text='فعال\u200cسازی ارتباط امن',
|
text='فعال\u200cسازی ارتباط امن',
|
||||||
fg_color=PRIMARY,
|
fg_color=PRIMARY,
|
||||||
hover_color=PRIMARY_DARK,
|
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,
|
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.normal_button = RTLButton(
|
||||||
self.profile_card,
|
self.profile_card,
|
||||||
text='بازگشت به حالت عادی',
|
text='بازگشت به حالت عادی',
|
||||||
fg_color="#F4EFE9",
|
fg_color=INPUT_BG,
|
||||||
text_color=TEXT,
|
text_color=TEXT,
|
||||||
hover_color="#ECE1D5",
|
hover_color="#2D3A49",
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"),
|
corner_radius=8,
|
||||||
|
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||||
command=self._switch_to_normal,
|
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()
|
self._configure_profile_card_layout()
|
||||||
|
|
||||||
composer = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=24, border_width=1, border_color=BORDER)
|
composer = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=0, border_width=0)
|
||||||
composer.grid(row=2, column=0, padx=outer_pad, pady=(0, outer_pad), sticky="ew")
|
composer.grid(row=2, column=0, sticky="ew")
|
||||||
composer.grid_columnconfigure(0, weight=1)
|
composer.grid_columnconfigure(0, weight=1)
|
||||||
self.message_entry = RTLTextbox(
|
self.message_entry = RTLTextbox(
|
||||||
composer,
|
composer,
|
||||||
height=72 if self.is_portrait else 82,
|
height=52 if self.is_portrait else 62,
|
||||||
fg_color=INPUT_BG,
|
fg_color=INPUT_BG,
|
||||||
|
text_color=TEXT,
|
||||||
border_width=0,
|
border_width=0,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
corner_radius=10,
|
||||||
|
font=ctk.CTkFont(family=FONT_BODY, size=15),
|
||||||
wrap="word",
|
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 = 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(
|
self.send_state_label = RTLLabel(
|
||||||
actions,
|
actions,
|
||||||
text="",
|
text="",
|
||||||
text_color=MUTED,
|
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(
|
RTLButton(
|
||||||
actions,
|
actions,
|
||||||
text='ارسال',
|
text='➤',
|
||||||
fg_color=ACCENT,
|
fg_color=PRIMARY,
|
||||||
text_color="#3A2514",
|
text_color="#FFFFFF",
|
||||||
hover_color=ACCENT_DARK,
|
hover_color=PRIMARY_DARK,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=20, weight="bold"),
|
||||||
width=110 if self.is_portrait else 126,
|
width=48,
|
||||||
height=48,
|
height=48,
|
||||||
|
corner_radius=24,
|
||||||
command=self._send_message,
|
command=self._send_message,
|
||||||
).pack()
|
).pack()
|
||||||
self.message_entry.bind("<Control-Return>", lambda _event: self._send_message())
|
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.overlay_frame = ctk.CTkFrame(
|
||||||
self.main_panel,
|
self.main_panel,
|
||||||
fg_color=CARD,
|
fg_color=CARD,
|
||||||
corner_radius=28,
|
corner_radius=12,
|
||||||
border_width=1,
|
border_width=1,
|
||||||
border_color=BORDER,
|
border_color=BORDER,
|
||||||
)
|
)
|
||||||
|
|
@ -1015,9 +1044,9 @@ class SecureSmsApp(ctk.CTk):
|
||||||
def _refresh_connection_badge(self):
|
def _refresh_connection_badge(self):
|
||||||
modem = self.controller.modem_status()
|
modem = self.controller.modem_status()
|
||||||
if modem["connected"]:
|
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:
|
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):
|
def _refresh_contacts(self):
|
||||||
for widget in self.contacts_frame.winfo_children():
|
for widget in self.contacts_frame.winfo_children():
|
||||||
|
|
@ -1037,15 +1066,15 @@ class SecureSmsApp(ctk.CTk):
|
||||||
self.contacts_frame,
|
self.contacts_frame,
|
||||||
text=f"{contact.name}\n{contact.phone}\n{contact.last_message_preview or 'آماده گفتگو'}",
|
text=f"{contact.name}\n{contact.phone}\n{contact.last_message_preview or 'آماده گفتگو'}",
|
||||||
anchor="e",
|
anchor="e",
|
||||||
height=76 if self.is_portrait else 88,
|
height=72 if self.is_portrait else 80,
|
||||||
corner_radius=20,
|
corner_radius=8,
|
||||||
command=lambda phone=contact.phone: self._select_contact(phone),
|
command=lambda phone=contact.phone: self._select_contact(phone),
|
||||||
fg_color="#F7EFE5" if selected else "#2A6956",
|
fg_color="#2B5278" if selected else "transparent",
|
||||||
hover_color="#F0E3D5" if selected else "#32745F",
|
hover_color="#2B5278",
|
||||||
text_color=TEXT if selected else "white",
|
text_color=TEXT,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15),
|
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):
|
def _select_contact(self, phone: str):
|
||||||
self.current_contact_phone = phone
|
self.current_contact_phone = phone
|
||||||
|
|
@ -1093,51 +1122,52 @@ class SecureSmsApp(ctk.CTk):
|
||||||
self.chat_container,
|
self.chat_container,
|
||||||
text='هنوز پیامی ثبت نشده است.\nاز نوار پایین برای نوشتن پیام استفاده کن.',
|
text='هنوز پیامی ثبت نشده است.\nاز نوار پایین برای نوشتن پیام استفاده کن.',
|
||||||
text_color=MUTED,
|
text_color=MUTED,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15)
|
font=ctk.CTkFont(family=FONT_BODY, size=14)
|
||||||
).pack(pady=40)
|
).pack(pady=40)
|
||||||
return
|
return
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if message.direction == "system":
|
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(
|
RTLLabel(
|
||||||
self.chat_container,
|
sys_frame,
|
||||||
text=message.body,
|
text=message.body,
|
||||||
text_color="#8A5C2E",
|
text_color=MUTED,
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=13),
|
font=ctk.CTkFont(family=FONT_BODY, size=12),
|
||||||
wraplength=int(self.window_width * 0.7)
|
wraplength=int(self.window_width * 0.6)
|
||||||
).pack(pady=12, anchor="center")
|
).pack(padx=12, pady=6)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
is_out = message.direction == "out"
|
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"
|
anchor = "e" if is_out else "w"
|
||||||
|
|
||||||
bubble = ctk.CTkFrame(
|
bubble = ctk.CTkFrame(
|
||||||
self.chat_container,
|
self.chat_container,
|
||||||
fg_color=bubble_color,
|
fg_color=bubble_color,
|
||||||
corner_radius=16,
|
corner_radius=12,
|
||||||
border_width=1 if not is_out else 0,
|
border_width=0,
|
||||||
border_color=BORDER
|
|
||||||
)
|
)
|
||||||
bubble.pack(anchor=anchor, padx=12, pady=6, fill="none")
|
bubble.pack(anchor=anchor, padx=8, pady=3, fill="none")
|
||||||
|
|
||||||
RTLLabel(
|
RTLLabel(
|
||||||
bubble,
|
bubble,
|
||||||
text=message.body,
|
text=message.body,
|
||||||
text_color="#16312A",
|
text_color="#FFFFFF",
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=16),
|
font=ctk.CTkFont(family=FONT_BODY, size=15),
|
||||||
wraplength=max(200, int(self.window_width * 0.55)),
|
wraplength=max(180, int(self.window_width * 0.5)),
|
||||||
justify="right"
|
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
|
badge_text = f"🛡️ {message.created_at}" if message.mode == "secure" else message.created_at
|
||||||
RTLLabel(
|
RTLLabel(
|
||||||
bubble,
|
bubble,
|
||||||
text=badge_text,
|
text=badge_text,
|
||||||
text_color=MUTED,
|
text_color="#7A8E9C",
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=11),
|
font=ctk.CTkFont(family=FONT_BODY, size=10),
|
||||||
justify="right"
|
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:
|
try:
|
||||||
self.after(50, lambda: self.chat_container._parent_canvas.yview_moveto(1.0))
|
self.after(50, lambda: self.chat_container._parent_canvas.yview_moveto(1.0))
|
||||||
|
|
@ -1255,9 +1285,9 @@ class SecureSmsApp(ctk.CTk):
|
||||||
RTLButton(
|
RTLButton(
|
||||||
body,
|
body,
|
||||||
text='بازگشت به گفتگو',
|
text='بازگشت به گفتگو',
|
||||||
fg_color="#F1E7DB",
|
fg_color=INPUT_BG,
|
||||||
text_color=TEXT,
|
text_color=TEXT,
|
||||||
hover_color="#E8D8C7",
|
hover_color="#2D3A49",
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||||
height=40,
|
height=40,
|
||||||
command=self._hide_overlay,
|
command=self._hide_overlay,
|
||||||
|
|
@ -1286,9 +1316,9 @@ class SecureSmsApp(ctk.CTk):
|
||||||
text='بستن',
|
text='بستن',
|
||||||
width=86,
|
width=86,
|
||||||
height=36,
|
height=36,
|
||||||
fg_color="#F1E7DB",
|
fg_color=INPUT_BG,
|
||||||
text_color=TEXT,
|
text_color=TEXT,
|
||||||
hover_color="#E8D8C7",
|
hover_color="#2D3A49",
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"),
|
||||||
command=self._hide_overlay,
|
command=self._hide_overlay,
|
||||||
).grid(row=0, column=1, padx=(8, 0), sticky="e")
|
).grid(row=0, column=1, padx=(8, 0), sticky="e")
|
||||||
|
|
@ -1353,9 +1383,9 @@ class SecureSmsApp(ctk.CTk):
|
||||||
RTLButton(
|
RTLButton(
|
||||||
body,
|
body,
|
||||||
text='بازگشت به تنظیمات',
|
text='بازگشت به تنظیمات',
|
||||||
fg_color="#F1E7DB",
|
fg_color=INPUT_BG,
|
||||||
text_color=TEXT,
|
text_color=TEXT,
|
||||||
hover_color="#E8D8C7",
|
hover_color="#2D3A49",
|
||||||
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"),
|
||||||
height=40,
|
height=40,
|
||||||
command=self._open_settings_panel,
|
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