feat(cli): add interactive and utility command-line interfaces

- Implement two different CLIs: an interactive menu and a utility CLI
- CLIs support key generation, mining, transactions, and card viewing
Co-authored-by: deusbalatro <github.sequence146@passinbox.com>
Co-committed-by: deusbalatro <github.sequence146@passinbox.com>
This commit is contained in:
Ed is The Standard Text Editor 2025-06-13 09:15:24 +00:00 committed by Ed is The Standard Text Editor
parent 652ff32a4e
commit 8061fdba9b
2 changed files with 448 additions and 0 deletions

293
cli.py Normal file
View File

@ -0,0 +1,293 @@
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 = "privatekey.pem";
public_key_file: str = "publickey.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 Private Key");
print("3. Load RSA Keypair");
print("4. Save Public Key")
print("5. Mine a Block");
print("6. Generate a Transaction");
print("7. 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_keypair(65537, 2048);
elif choice == 2:
opt_save_private_key(rsa_private_key);
elif choice == 3:
opt_load_private_key();
elif choice == 4:
opt_save_public_key(rsa_private_key);
elif choice == 5:
opt_mine_block();
elif choice == 6:
opt_generate_transaction();
elif choice == 7:
opt_view_deck();
else:
msg_array[0] = "Invalid Input. Please Choose a Number Between 0-6.";
clear_console();
def opt_generate_keypair(exponent=65537, size=2048):
global rsa_private_key;
rsa_private_key = rsa.generate_private_key(
public_exponent = exponent,
key_size = size,
)
msg_array[0] = "RSA Keypair Generated";
return rsa_private_key;
def opt_save_private_key(rsa, filename=private_key_file):
if rsa is None:
msg_array[0] = "No RSA Keypair to extract private key. Please generate one first.";
return None;
else:
with open(filename, "wb") as file:
pem = rsa.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 {filename}";
def opt_save_public_key(rsa, filename=public_key_file):
if rsa is None:
msg_array[0] = "No RSA Keypair to extract public key. Please generate one first.";
return None;
public_key = rsa.public_key();
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
with open(filename, "wb") as file:
file.write(public_pem);
msg_array[0] = f"RSA Public Key Saved to {filename}";
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 get_private_key_from_file(path):
if not os.path.exists(path):
msg_array[0] = f"No key file found ({path} missing)";
return None;
with open(path, "rb") as file:
key = serialization.load_pem_private_key(
file.read(),
password=None,
);
msg_array[0] = f"RSA Private Key Gotten From {path}"; #inaccessible
return key;
def opt_mine_block(path = None):
global rsa_private_key;
if (path):
rsa_private_key = get_private_key_from_file(path);
if not (rsa_private_key):
return None;
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(path = None, recv = None):
global rsa_private_key;
if (path):
rsa_private_key = get_private_key_from_file(path);
if not (rsa_private_key):
return None;
if rsa_private_key is None:
msg_array[0] = "You must load or generate an RSA keypair first.";
return;
if not (recv):
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;
else:
if not os.path.isfile(recv):
msg_array[0] = f"Reciever public key file not found {recv}";
return None;
with open(recv, "rb") as recv_file:
receiver_key = serialization.load_pem_public_key(recv_file.read());
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_private_key);
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();

155
wikideck_cli.py Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
import argparse
import glob
import os
from cli import opt_generate_keypair
from cli import opt_save_private_key
from cli import opt_load_private_key
from cli import opt_save_public_key
from cli import opt_mine_block
from cli import opt_generate_transaction
from cli import opt_view_deck
from cli import msg_array
from cli import get_private_key_from_file
from cli import fetch_cards_json
DEFAULT_FILENAME: str = "privatekey";
def resolve_path(path: str | None) -> str | None:
if not path or path.strip() == "":
return None;
path = path.strip();
if not path.endswith(".pem"):
path += ".pem";
if os.path.exists(path) and os.path.isfile(path):
return path;
return None;
def resolve_path_for_key_creation(filename:str | None) -> str:
default_name = "privatekey.pem";
if not filename or filename.strip() == "":
return default_name;
filename = filename.strip();
if os.path.isdir(filename):
return os.path.join(filename, default_name);
if not filename.endswith(".pem"):
filename += ".pem";
dir_path = os.path.dirname(filename);
if dir_path and not os.path.exists(dir_path):
os.makedirs(dir_path);
return filename;
def get_first_pem_file():
pem_files = glob.glob("*.pem");
if pem_files:
first_pem = min(pem_files, key=os.path.getmtime);
return first_pem;
else:
return None;
def handle_createkey(args):
rsa_key = opt_generate_keypair();
filepath = resolve_path_for_key_creation(args.output);
opt_save_private_key(rsa_key, filepath);
print(msg_array[0]);
public_path = f"pub_{filepath}"
opt_save_public_key(rsa_key, public_path);
def handle_mine(args):
file = resolve_path(args.use);
if not file:
file = get_first_pem_file();
if not file:
msg_array[0] = "No valid private key found";
return;
opt_mine_block(file);
def handle_tx(args):
file = resolve_path(args.use);
if not file:
file = get_first_pem_file();
if not file:
msg_array[0] = "No valid private key found";
return;
recv = resolve_path(args.receiver);
if args.receiver and not recv:
msg_array[0] = "No valid public key found (receiver)";
return;
opt_generate_transaction(file, recv);
def handle_deck(args):
file = resolve_path(args.use);
if not file:
file = get_first_pem_file();
if not file:
msg_array[0] = "No valid private key found";
return;
rsa = get_private_key_from_file(file);
if rsa is None:
return;
cards = fetch_cards_json(rsa);
if len(cards) < 1:
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']}");
def build_parser():
parser = argparse.ArgumentParser(description="Wikideck CLI");
subparsers = parser.add_subparsers(dest="command", required=True);
#createkey
p_create = subparsers.add_parser("createkey",aliases=["ck"], help="Create an RSA keypair and save it to the current directory");
p_create.add_argument("-O", "--output", help="Optional custom filename (.pem)");
p_create.set_defaults(func=handle_createkey);
#mine
p_mine = subparsers.add_parser("mine", help="Mine a block, using the first ./*.pem by default.");
p_mine.add_argument("-u", "--use", help="Use specified keypair (path to .pem file)");
p_mine.set_defaults(func=handle_mine);
#transaction
p_transaction = subparsers.add_parser("transaction", aliases=["tx"], help="Generate a transaction, using the first ./*.pem by default.");
p_transaction.add_argument("-u", "--use", help="Use specified keypair (path to .pem file)");
p_transaction.add_argument("-to", "--receiver", help="Send to specified public key (path to .pem file)");
p_transaction.set_defaults(func=handle_tx);
#deck
p_deck = subparsers.add_parser("deck", help="Show all cards tied to the private key, using the first ./*.pem by default.");
p_deck.add_argument("-u", "--use", help="Use specified keypair (path to .pem file)");
p_deck.set_defaults(func=handle_deck);
return parser;
def main():
parser = build_parser();
args = parser.parse_args();
args.func(args);
print(msg_array[0]);
if __name__ == "__main__":
main();