diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..9b4821d --- /dev/null +++ b/cli.py @@ -0,0 +1,237 @@ +import os +import sys +import time +import requests +from typing import Optional +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization + + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "wikideck"))); + +from Mine.Block import Block +from Mine.Transaction import Transaction + +WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/'); + + +rsa_private_key: Optional[rsa.RSAPrivateKey] = None; +msg_array: list[str] = [""]; +private_key_file: str = "private_key.pem"; + + + +def main(): + + while True: + + if (msg_array[0]): + print(f"Last Action: {msg_array[0]}"); + + print("\n--- Wikideck CLI ---"); + print("1. Create RSA Keypair"); + print("2. Save RSA Keypair"); + print("3. Load RSA Keypair"); + print("4. Mine a Block"); + print("5. Generate a Transaction"); + print("6. View Deck"); + print("0. Exit"); + print("\n-------------------"); + + try: + choice: int = int(input("Select an option: ").strip()); + except ValueError: + msg_array[0] = "Invalid Input. Please Choose a Number Between 0-6."; + clear_console(); + continue; + + + if choice == 0: + break; + elif choice == 1: + opt_generate_private_key(65537, 2048); + elif choice == 2: + opt_save_private_key(); + elif choice == 3: + opt_load_private_key(); + elif choice == 4: + opt_mine_block(); + elif choice == 5: + opt_generate_transaction(); + elif choice == 6: + opt_view_deck(); + else: + msg_array[0] = "Invalid Input. Please Choose a Number Between 0-6."; + + clear_console(); + +def opt_generate_private_key(exponent, size): + global rsa_private_key; + rsa_private_key = rsa.generate_private_key( + public_exponent = exponent, + key_size = size, + ) + msg_array[0] = "RSA Keypair Generated"; + +def opt_save_private_key(): + if rsa_private_key is None: + msg_array[0] = "No RSA Keypair to Save. Please Generate one first."; + return None; + else: + with open(private_key_file, "wb") as file: + pem = rsa_private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + file.write(pem); + msg_array[0] = f"RSA Private Key Saved to {private_key_file}"; + +def opt_load_private_key(): + global rsa_private_key; + if not os.path.exists(private_key_file): + msg_array[0] = f"No saved key found ({private_key_file} missing)"; + return None; + + with open(private_key_file, "rb") as file: + rsa_private_key = serialization.load_pem_private_key( + file.read(), + password=None, + ) + msg_array[0] = f"RSA Private Key Loaded From {private_key_file}"; + +def opt_mine_block(): + + global rsa_private_key; + + + if rsa_private_key is None: + msg_array[0] = "You must load or generate an RSA keypair first."; + return; + + try: + response = requests.get(f"{WIKIDECK_URL}/blocks"); + if response.status_code != 200: + msg_array[0] = f"Failed to fetch block to mine: {response.status_code}"; + return; + + block_to_mine = Block(data = response.json()); + block_to_mine.mine(rsa_private_key); + + response = requests.post(f"{WIKIDECK_URL}/blocks", json=block_to_mine.as_dict()); + if response.status_code == 200: + msg_array[0] = "Block successfully mined and submitted"; + else: + msg_array[0] = f"Failed to submit mined block: {response.status_code}"; + + except Exception as e: + msg_array[0] = f"Error during mining: {str(e)}"; + +def opt_generate_transaction(): + global rsa_private_key; + + if rsa_private_key is None: + msg_array[0] = "You must load or generate an RSA keypair first."; + return; + + print("\nPaste the recipient's RSA Public Key (PEM format). End with an Empty Line (Press enter twice): "); + lines = []; + + while True: + line = input(); + if not line.strip(): + break; + lines.append(line); + + receiver_pem = "\n".join(lines); + + try: + receiver_key = serialization.load_pem_public_key(receiver_pem.encode("utf-8")); + except Exception: + msg_array[0] = "Invalid public key format"; + return; + + try: + + cards = fetch_cards_json(rsa_private_key); + + if not cards: + msg_array[0] = "You have no cards to send"; + return; + + print("\nYour cards:"); + + for i, card in enumerate(cards): + print(f"{i+1}. Card ID: {card['cardId']}, Page ID: {card['pageId']}"); + + + try: + index: int = int(input("Select a card number to send: ").strip()) - 1; + + if not (0 <= index < len(cards)): + raise ValueError; + except ValueError: + msg_array[0] = "Invalid card selection"; + return; + + selected_card = cards[index]; + + tx = Transaction( + cardId = selected_card['cardId'], + sender = rsa_private_key.public_key(), + receiver=receiver_key, + authorPrivateKey=rsa_private_key + ); + + response = requests.post(f"{WIKIDECK_URL}/transactions", json=tx.as_dict()); + + if response.status_code != 200: + msg_array[0] = f"Transaction failed: {response.status_code}"; + else: + msg_array[0] = "Transaction submitted successfully"; + + except Exception as e: + msg_array[0] = f"Error creating transaction: {str(e)}"; + +def opt_view_deck(): + + global rsa_private_key; + if rsa_private_key is None: + msg_array[0] = "You must load or generate an RSA keypair first."; + return; + + try: + cards = fetch_cards_json(rsa); + + + if len(cards) <= 0: + msg_array[0] = "You have no cards to view"; + return; + + for i, card in enumerate(cards): + print(f"{i+1}. Card ID: {card['cardId']}, Page ID: {card['pageId']}"); + + except Exception as e: + msg_array[0] = f"Error fetching cards: {str(e)}"; + +def fetch_cards_json(rsa): + pem = rsa.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode("utf-8"); + + response = requests.get(f"{WIKIDECK_URL}/cards", params={"publicKey": pem}); + + if response.status_code != 200: + msg_array[0] = f"Failed to fetch cards: {response.status_code}"; + return; + + cards = response.json(); + return cards; + + +def clear_console(): + os.system('cls' if os.name == 'nt' else 'clear'); + +if __name__ == "__main__": + main();