diff --git a/jaklis.py b/jaklis.py index e99519c..ac03340 100755 --- a/jaklis.py +++ b/jaklis.py @@ -5,31 +5,39 @@ import sys import os import string import random -from os.path import join, dirname -from shutil import copyfile from dotenv import load_dotenv from duniterpy.key import SigningKey +from pathlib import Path +from lib.gva import GvaApi +from lib.cesium import CesiumPlus -__version__ = "0.1.0" +__version__ = "0.1.1" -MY_PATH = os.path.realpath(sys.argv[0]).replace("jaklis.py", "") +MY_PATH = Path(__file__).resolve().parent -# Get environment variables -if not os.path.isfile(MY_PATH + ".env"): - copyfile(MY_PATH + ".env.template", MY_PATH + ".env") -dotenv_path = join(dirname(__file__), MY_PATH + ".env") -load_dotenv(dotenv_path) +# Set file paths +dotenv_file = MY_PATH / ".env" +dotenv_template = MY_PATH / ".env.template" + +# Check and create dotenv file +if not dotenv_file.is_file(): + dotenv_file.write_text(dotenv_template.read_text()) + +# Load environment variables +load_dotenv(dotenv_file) # Set global values (default parameters) regarding environment variables node = os.getenv("DUNITER") + "/gva" or "https://g1v1.p2p.legal/gva" pod = os.getenv("ESNODE") or "https://g1.data.e-is.pro" destPubkey = False -# Parse arguments +# define parser parser = argparse.ArgumentParser( description="CLI Client for Cesium+ and Ḡchange", epilog="current node: '" + node + "', current pod: '" + pod + "'.", ) + +# load global arguments parser.add_argument( "-v", "--version", @@ -41,175 +49,231 @@ parser.add_argument( "-n", "--node", help="Address of the Cesium+, Gchange, or Duniter node to use" ) -# Create subparsers for different commands +# Define commands with arguments +commands = { + "read": { + "help": "Read messages", + "arguments": { + ("n", "number"): { + "type": int, + "default": 3, + "help": "Display the last NUMBER messages", + }, + ("j", "json"): {"action": "store_true", "help": "Output in JSON format"}, + ("o", "outbox"): {"action": "store_true", "help": "Read sent messages"}, + }, + }, + "send": { + "help": "Send a message", + "arguments": { + ("d", "destinataire"): { + "required": True, + "help": "Recipient of the message", + }, + ("t", "titre"): {"help": "Title of the message to send"}, + ("m", "message"): {"help": "Message to send"}, + ("f", "fichier"): {"help": "Send the message from the 'FILE'"}, + ("o", "outbox"): { + "action": "store_true", + "help": "Send the message to the outbox", + }, + }, + }, + "delete": { + "help": "Delete a message", + "arguments": { + ("i", "id"): { + "action": "append", + "nargs": "+", + "required": True, + "help": "ID(s) of the message(s) to delete", + }, + ("o", "outbox"): { + "action": "store_true", + "help": "Delete a sent message", + }, + }, + }, + "get": { + "help": "View a Cesium+ profile", + "arguments": { + ("p", "profile"): {"help": "Profile name"}, + ("a", "avatar"): { + "action": "store_true", + "help": "Also retrieve the avatar in raw base64 format", + }, + }, + }, + "page": { + "help": "View a Cesium+ page", + "arguments": { + ("p", "page"): {"help": "Page name"}, + ("a", "avatar"): { + "action": "store_true", + "help": "Also retrieve the page's avatar in raw base64 format", + }, + }, + }, + "set": { + "help": "Configure your Cesium+ profile", + "arguments": { + ("n", "name"): {"help": "Profile name"}, + ("d", "description"): {"help": "Profile description"}, + ("v", "ville"): {"help": "Profile city"}, + ("a", "adresse"): {"help": "Profile address"}, + ("pos", "position"): { + "nargs": 2, + "help": "Geographical coordinates (lat + lon)", + }, + ("s", "site"): {"help": "Profile website"}, + ("A", "avatar"): {"help": "Path to profile avatar in PNG"}, + }, + }, + "erase": {"help": "Erase your Cesium+ profile", "arguments": {}}, + "stars": { + "help": "View a profile's stars / Rate a profile (option -s RATING)", + "arguments": { + ("p", "profile"): {"help": "Target profile"}, + ("n", "number"): {"type": int, "help": "Number of stars"}, + }, + }, + "unstars": { + "help": "Remove a star", + "arguments": { + ("p", "profile"): {"help": "Profile to unstar"}, + }, + }, + "getoffer": { + "help": "Get information about a Ḡchange listing", + "arguments": { + ("i", "id"): {"help": "Target listing to retrieve"}, + }, + }, + "setoffer": { + "help": "Create a Ḡchange listing", + "arguments": { + ("t", "title"): {"help": "Title of the listing to create"}, + ("d", "description"): {"help": "Description of the listing to create"}, + ("c", "category"): {"help": "Category of the listing to create"}, + ("l", "location"): { + "nargs": 2, + "help": "Location of the listing to create (lat + lon)", + }, + ("p", "picture"): {"help": "Image of the listing to create"}, + ("ci", "city"): {"help": "City of the listing to create"}, + ("pr", "price"): {"help": "Price of the listing to create"}, + }, + }, + "deleteoffer": { + "help": "Delete a Ḡchange listing", + "arguments": { + ("i", "id"): {"help": "Target listing to delete"}, + }, + }, + "pay": { + "help": "Pay in Ḡ1", + "arguments": { + ("p", "pubkey"): {"help": "Payment recipient"}, + ("a", "amount"): {"type": float, "help": "Transaction amount"}, + ("c", "comment"): { + "default": "", + "help": "Transaction comment", + "nargs": "*", + }, + ("m", "mempool"): { + "action": "store_true", + "help": "Use mempool sources", + }, + ("v", "verbose"): { + "action": "store_true", + "help": "Display the JSON result of the transaction", + }, + }, + }, + "history": { + "help": "View Ḡ1 account transaction history", + "arguments": { + ("p", "pubkey"): {"help": "Public key of the target account"}, + ("n", "number"): { + "type": int, + "default": 10, + "help": "Display the last NUMBER transactions", + }, + ("j", "json"): { + "action": "store_true", + "help": "Display the result in JSON format", + }, + ("nocolors"): { + "action": "store_true", + "help": "Display the result in black and white", + }, + }, + }, + "balance": { + "help": "View Ḡ1 account balance", + "arguments": { + ("p", "pubkey"): {"help": "Public key of the target account"}, + ("m", "mempool"): { + "action": "store_true", + "help": "Use mempool sources", + }, + }, + }, + "id": { + "help": "View public key/username identity", + "arguments": { + ("p", "pubkey"): {"help": "Public key of the target account"}, + ("u", "username"): {"help": "Username of the target account"}, + }, + }, + "idBalance": { + "help": "View public key/username identity and balance", + "arguments": { + ("p", "pubkey"): {"help": "Public key of the target account"}, + }, + }, + "currentUd": { + "help": "Display the current Universal Dividend amount", + "arguments": { + ("p", "pubkey"): {"help": "Public key of the target account"}, + }, + }, + "listWallets": { + "help": "List all G1 wallets", + "arguments": { + ("b", "balance"): {"action": "store_true", "help": "Display balances"}, + ("mbr"): { + "action": "store_true", + "help": "Display raw list of member pubkeys", + }, + ("non_mbr"): { + "action": "store_true", + "help": "Display raw list of nonmember identity pubkeys", + }, + ("larf"): { + "action": "store_true", + "help": "Display raw list of nonmember pubkeys", + }, + ("brut"): { + "action": "store_true", + "help": "Display raw list of all pubkeys", + }, + ("p", "pubkey"): {"help": "Useless but needed"}, + }, + }, + "geolocProfiles": {"help": "Get JSON of all geolocated accounts", "arguments": {}}, +} + +# Process commands and arguments subparsers = parser.add_subparsers(title="jaklis Commands", dest="cmd") -read_cmd = subparsers.add_parser("read", help="Read messages") -send_cmd = subparsers.add_parser("send", help="Send a message") -delete_cmd = subparsers.add_parser("delete", help="Delete a message") -getProfile_cmd = subparsers.add_parser("get", help="View a Cesium+ profile") -getPage_cmd = subparsers.add_parser("page", help="View a Cesium+ page") -setProfile_cmd = subparsers.add_parser("set", help="Configure your Cesium+ profile") -eraseProfile_cmd = subparsers.add_parser("erase", help="Erase your Cesium+ profile") -stars_cmd = subparsers.add_parser( - "stars", help="View a profile's stars / Rate a profile (option -s RATING)" -) -unstars_cmd = subparsers.add_parser("unstars", help="Remove a star") -getoffer_cmd = subparsers.add_parser( - "getoffer", help="Get information about a Ḡchange listing" -) -setoffer_cmd = subparsers.add_parser("setoffer", help="Create a Ḡchange listing") -deleteoffer_cmd = subparsers.add_parser("deleteoffer", help="Delete a Ḡchange listing") -pay_cmd = subparsers.add_parser("pay", help="Pay in Ḡ1") -history_cmd = subparsers.add_parser( - "history", help="View Ḡ1 account transaction history" -) -balance_cmd = subparsers.add_parser("balance", help="View Ḡ1 account balance") -id_cmd = subparsers.add_parser("id", help="View public key/username identity") -id_balance_cmd = subparsers.add_parser( - "idBalance", help="View public key/username identity and balance" -) -currentUd = subparsers.add_parser( - "currentUd", help="Display the current Universal Dividend amount" -) -listWallets = subparsers.add_parser("listWallets", help="List all G1 wallets") -geolocProfiles = subparsers.add_parser( - "geolocProfiles", help="Get JSON of all geolocated accounts" -) - -# Messaging management commands -read_cmd.add_argument( - "-n", "--number", type=int, default=3, help="Display the last NUMBER messages" -) -read_cmd.add_argument("-j", "--json", action="store_true", help="Output in JSON format") -read_cmd.add_argument("-o", "--outbox", action="store_true", help="Read sent messages") - -send_cmd.add_argument( - "-d", "--destinataire", required=True, help="Recipient of the message" -) -send_cmd.add_argument("-t", "--titre", help="Title of the message to send") -send_cmd.add_argument("-m", "--message", help="Message to send") -send_cmd.add_argument("-f", "--fichier", help="Send the message from the 'FILE'") -send_cmd.add_argument( - "-o", "--outbox", action="store_true", help="Send the message to the outbox" -) - -delete_cmd.add_argument( - "-i", - "--id", - action="append", - nargs="+", - required=True, - help="ID(s) of the message(s) to delete", -) -delete_cmd.add_argument( - "-o", "--outbox", action="store_true", help="Delete a sent message" -) - -# Profile management commands -setProfile_cmd.add_argument("-n", "--name", help="Profile name") -setProfile_cmd.add_argument("-d", "--description", help="Profile description") -setProfile_cmd.add_argument("-v", "--ville", help="Profile city") -setProfile_cmd.add_argument("-a", "--adresse", help="Profile address") -setProfile_cmd.add_argument( - "-pos", "--position", nargs=2, help="Geographical coordinates (lat + lon)" -) -setProfile_cmd.add_argument("-s", "--site", help="Profile website") -setProfile_cmd.add_argument("-A", "--avatar", help="Path to profile avatar in PNG") - -getProfile_cmd.add_argument("-p", "--profile", help="Profile name") -getProfile_cmd.add_argument( - "-a", - "--avatar", - action="store_true", - help="Also retrieve the avatar in raw base64 format", -) - -getPage_cmd.add_argument("-p", "--page", help="Page name") -getPage_cmd.add_argument( - "-a", - "--avatar", - action="store_true", - help="Also retrieve the page's avatar in raw base64 format", -) - -# Likes management commands -stars_cmd.add_argument("-p", "--profile", help="Target profile") -stars_cmd.add_argument("-n", "--number", type=int, help="Number of stars") -unstars_cmd.add_argument("-p", "--profile", help="Profile to unstar") - -# Offers management commands -getoffer_cmd.add_argument("-i", "--id", help="Target listing to retrieve") -setoffer_cmd.add_argument("-t", "--title", help="Title of the listing to create") -setoffer_cmd.add_argument( - "-d", "--description", help="Description of the listing to create" -) -setoffer_cmd.add_argument("-c", "--category", help="Category of the listing to create") -setoffer_cmd.add_argument( - "-l", - "--localisation", - nargs=2, - help="Location of the listing to create (lat + lon)", -) -setoffer_cmd.add_argument("-p", "--picture", help="Image of the listing to create") -setoffer_cmd.add_argument("-ci", "--city", help="City of the listing to create") -setoffer_cmd.add_argument("-pr", "--price", help="Price of the listing to create") -deleteoffer_cmd.add_argument("-i", "--id", help="Target listing to delete") - -# GVA usage commands -pay_cmd.add_argument("-p", "--pubkey", help="Payment recipient") -pay_cmd.add_argument("-a", "--amount", type=float, help="Transaction amount") -pay_cmd.add_argument( - "-c", "--comment", default="", help="Transaction comment", nargs="*" -) -pay_cmd.add_argument("-m", "--mempool", action="store_true", help="Use mempool sources") -pay_cmd.add_argument( - "-v", - "--verbose", - action="store_true", - help="Display the JSON result of the transaction", -) - -history_cmd.add_argument("-p", "--pubkey", help="Public key of the target account") -history_cmd.add_argument( - "-n", - "--number", - type=int, - default=10, - help="Display the last NUMBER transactions", -) -history_cmd.add_argument( - "-j", "--json", action="store_true", help="Display the result in JSON format" -) -history_cmd.add_argument( - "--nocolors", action="store_true", help="Display the result in black and white" -) - -balance_cmd.add_argument("-p", "--pubkey", help="Public key of the target account") -balance_cmd.add_argument( - "-m", "--mempool", action="store_true", help="Use mempool sources" -) -id_cmd.add_argument("-p", "--pubkey", help="Public key of the target account") -id_cmd.add_argument("-u", "--username", help="Username of the target account") -id_balance_cmd.add_argument("-p", "--pubkey", help="Public key of the target account") -currentUd.add_argument("-p", "--pubkey", help="Public key of the target account") -listWallets.add_argument( - "-b", "--balance", action="store_true", help="Display balances" -) -listWallets.add_argument( - "--mbr", action="store_true", help="Display raw list of member pubkeys" -) -listWallets.add_argument( - "--non_mbr", - action="store_true", - help="Display raw list of non-member identity pubkeys", -) -listWallets.add_argument( - "--larf", action="store_true", help="Display raw list of non-member pubkeys" -) -listWallets.add_argument( - "--brut", action="store_true", help="Display raw list of all pubkeys" -) -listWallets.add_argument("-p", "--pubkey", help="Useless but needed") +for cmd, cmd_info in commands.items(): + cmd_parser = subparsers.add_parser(cmd, help=cmd_info["help"]) + for args, kwargs in cmd_info["arguments"].items(): + if isinstance(args, str): + cmd_parser.add_argument("--" + args, **kwargs) + else: + short_arg, long_arg = args + cmd_parser.add_argument("-" + short_arg, "--" + long_arg, **kwargs) args = parser.parse_args() cmd = args.cmd @@ -220,7 +284,7 @@ if args.version: if not cmd: parser.print_help() - sys.exit(1) + sys.exit(0) def createTmpDunikey(): @@ -239,73 +303,50 @@ def createTmpDunikey(): return keyPath -# Check if a dunikey is needed -try: - pubkey = args.pubkey -except: - pubkey = False -try: - profile = args.profile -except: - profile = False - -if cmd in ( - "history", - "balance", - "get", - "page", - "id", - "idBalance", - "listWallets", -) and (pubkey or profile): - noNeedDunikey = True - keyPath = False +def get_arg_value(args, arg): try: - dunikey = args.pubkey - except: - dunikey = args.profile -else: - noNeedDunikey = False + return getattr(args, arg) + except AttributeError: + return False + + +def get_dunikey(args): if args.key: - dunikey = args.key - keyPath = False - else: - dunikey = os.getenv("DUNIKEY") - if not dunikey: - keyPath = createTmpDunikey() - dunikey = keyPath - else: - keyPath = False + return args.key + dunikey = os.getenv("DUNIKEY") + if not dunikey: + keyPath = createTmpDunikey() + dunikey = keyPath if not os.path.isfile(dunikey): HOME = os.getenv("HOME") dunikey = HOME + dunikey if not os.path.isfile(dunikey): sys.stderr.write("The keyfile {0} is not found.\n".format(dunikey)) sys.exit(1) + return dunikey -# Construct the CesiumPlus object -if cmd in ( - "read", - "send", - "delete", - "set", - "get", + +pubkey = get_arg_value(args, "pubkey") +profile = get_arg_value(args, "profile") + +noNeedDunikey = cmd in ( + "history", + "balance", "page", - "erase", - "stars", - "unstars", - "getoffer", - "setoffer", - "deleteoffer", - "geolocProfiles", -): - from lib.cesium import CesiumPlus + "id", + "idBalance", + "listWallets", +) and (pubkey or profile) - if args.node: - pod = args.node +if noNeedDunikey: + dunikey = pubkey if pubkey else profile +else: + dunikey = get_dunikey(args) - cesium = CesiumPlus(dunikey, pod, noNeedDunikey) +keyPath = False if dunikey else createTmpDunikey() + +def handle_cesium_commands(args, cmd, cesium): # Messaging if cmd == "read": cesium.read(args.number, args.outbox, args.json) @@ -374,27 +415,11 @@ if cmd in ( ) elif cmd == "deleteoffer": cesium.deleteOffer(args.id) + else: + raise ValueError(f"Unknown command: {cmd}") -# Construct the GvaApi object -elif cmd in ( - "pay", - "history", - "balance", - "id", - "idBalance", - "currentUd", - "listWallets", -): - from lib.gva import GvaApi - - if args.node: - node = args.node - - if args.pubkey: - destPubkey = args.pubkey - - gva = GvaApi(dunikey, node, destPubkey, noNeedDunikey) +def handle_gva_commands(args, cmd, gva): if cmd == "pay": gva.pay(args.amount, args.comment, args.mempool, args.verbose) elif cmd == "history": @@ -409,6 +434,52 @@ elif cmd in ( gva.currentUd() elif cmd == "listWallets": gva.listWallets(args.brut, args.mbr, args.non_mbr, args.larf) + else: + raise ValueError(f"Unknown command: {cmd}") + + +# Construct the CesiumPlus object +if cmd in ( + "read", + "send", + "delete", + "set", + "get", + "page", + "erase", + "stars", + "unstars", + "getoffer", + "setoffer", + "deleteoffer", + "geolocProfiles", +): + if args.node: + pod = args.node + + cesium = CesiumPlus(dunikey, pod, noNeedDunikey) + handle_cesium_commands(args, cmd, cesium) + +# Construct the GvaApi object +elif cmd in ( + "pay", + "history", + "balance", + "id", + "idBalance", + "currentUd", + "listWallets", +): + if args.node: + node = args.node + + if args.pubkey: + destPubkey = args.pubkey + + gva = GvaApi(dunikey, node, destPubkey, noNeedDunikey) + handle_gva_commands(args, cmd, gva) +else: + raise ValueError(f"Unknown command: {cmd}") if keyPath: os.remove(keyPath)