From 3e4130458a9bbb7b04aaacc20cb4ca478285bb0a Mon Sep 17 00:00:00 2001 From: ningwang-arch Date: Tue, 3 Aug 2021 19:18:10 +0800 Subject: [PATCH] tui v-1.0.0 added --- completer_generator.py | 33 +++++++++++++ connect_node.py | 57 +++++++++------------- del_func.py | 28 +++++++++-- json2vmess.py | 13 ++---- parser_create.py | 55 ---------------------- port_path.py | 39 ++++++++++++++++ requirements.txt | 4 ++ settings.py | 75 +++++++++++++++++++++++++++++ update_sub.py | 23 ++++++++- v_cli.py | 104 +++++++++++++++++++---------------------- 10 files changed, 269 insertions(+), 162 deletions(-) create mode 100644 completer_generator.py delete mode 100644 parser_create.py create mode 100644 port_path.py create mode 100644 requirements.txt diff --git a/completer_generator.py b/completer_generator.py new file mode 100644 index 0000000..192e59d --- /dev/null +++ b/completer_generator.py @@ -0,0 +1,33 @@ +import os +import json +from prompt_toolkit.completion import NestedCompleter +from settings import CONFIG_DIR, nested_dict_base + + +def get_sub_node_set(): + if not os.path.exists(CONFIG_DIR+'groups.json') or not os.path.exists(CONFIG_DIR+'connections.json'): + return None + with open(CONFIG_DIR+'groups.json') as f: + groups_info = json.load(f) + with open(CONFIG_DIR+'connections.json', encoding='utf-8') as f: + conn = json.load(f) + res = {} + for item in groups_info: + sub_name = groups_info[item]['displayName'].replace(' ', '%20') + connections = [conn[i]['displayName'].replace(' ', '%20') + for i in groups_info[item]['connections']] + + res[sub_name] = set(connections) + return res + + +def generate_completer(): + res = get_sub_node_set() + nested_dict_base['update'] = set(res.keys()) + nested_dict_base['connect'] = res + nested_dict_base['info'] = res + nested_dict_base['delete'] = res + + completer = NestedCompleter.from_nested_dict(nested_dict_base) + + return completer diff --git a/connect_node.py b/connect_node.py index 0cc0943..9ae1c24 100644 --- a/connect_node.py +++ b/connect_node.py @@ -1,7 +1,8 @@ import json import os -from settings import LAST_CONNECT, CONFIG_DIR, PLATFORM +from settings import LAST_CONNECT, CONFIG_DIR, PLATFORM, clean_blocks, get_node_str, load_default_config from fill_config import config +import click config_path = CONFIG_DIR+'config.json' log_path = CONFIG_DIR+'connect.log' @@ -21,11 +22,12 @@ def print_node(): info = {} for item in sub.keys(): connections = sub[item]['connections'] + cnt = 1 for i in range(len(connections)): connections[i] = str(cnt)+'. '+conn[connections[i]]['displayName'] cnt += 1 info[sub[item]['displayName']] = connections - print(json.dumps(info, indent=4, ensure_ascii=False)) + click.echo_via_pager(json.dumps(info, indent=4, ensure_ascii=False)) def disconnect(): @@ -64,35 +66,7 @@ def current(): print("Current connect: "+node_dict[node]['displayName']) -def convet_num_to_nodestr(choice): - sub_path = CONFIG_DIR+'groups.json' - con_path = CONFIG_DIR+'connections.json' - if not os.path.exists(con_path): - print('No node, please update the subscription and try again') - return "" - conn = {} - sub = {} - with open(sub_path, 'r', encoding='utf-8') as f: - sub = json.load(f) - with open(con_path, 'r', encoding='utf-8') as f: - conn = json.load(f) - # format {"sub_name":[node_name_list]} - info = {} - values = [] - for item in sub.keys(): - connections = sub[item]['connections'] - for i in range(len(connections)): - values.append(conn[connections[i]]['displayName']) - if choice > len(values): - print('Bad choice!') - return "" - node_name = values[choice-1] - for item in conn.keys(): - if conn[item]['displayName'] == node_name: - return item - - -def connect(choice, path="/usr/bin/v2ray", http_port=8889, socks_port=11223): +def connect_by_nodestr(node_str, path="/usr/bin/v2ray", http_port=8889, socks_port=11223): path = path.replace("\\", '/') if (("/" in path) and (not os.path.exists(path))): @@ -110,7 +84,6 @@ def connect(choice, path="/usr/bin/v2ray", http_port=8889, socks_port=11223): if result != 0: print('Port occupied or no executable program') return - node_str = convet_num_to_nodestr(choice) node_name = '' connect_info = {"node": node_str, "path": path, @@ -120,6 +93,7 @@ def connect(choice, path="/usr/bin/v2ray", http_port=8889, socks_port=11223): if node_str == "": print('Invalid choice') return + with open(CONFIG_DIR+'connections.json') as f: node_name = json.load(f)[node_str]['displayName'] pass @@ -141,7 +115,6 @@ def connect_default(): info = json.load(f) path = info['path'] path = path.replace("\\", '/') - if (("/" in path) and (not os.path.exists(path))): print("No such file!") return @@ -149,10 +122,22 @@ def connect_default(): os.system("exec %s -config %s > %s 2>&1 &" % (path, config_path, log_path)) elif PLATFORM == 'win32': - filepath, fullflname = os.path.split(path) - os.chdir(filepath) os.system("start /b %s -config %s > %s 2>&1 &" % - (fullflname, config_path, log_path)) + (path, config_path, log_path)) print("Connect successfully") else: print('No default config file') + + +def connect(blocks: list): + clean_blocks(blocks) + if not blocks: + connect_default() + elif len(blocks) < 2 or len(blocks) > 2: + print('Error format,should be `connect group nodeName` ') + return + else: + conf = load_default_config() + node_str = get_node_str(blocks[len(blocks)-1]) + connect_by_nodestr( + node_str, conf['path'], conf['http_port'], conf['socks_port']) diff --git a/del_func.py b/del_func.py index 6d4c1b1..8c31b86 100644 --- a/del_func.py +++ b/del_func.py @@ -1,7 +1,6 @@ import json import os -from settings import CONFIG_DIR, CONNECTIONS_DIR -from connect_node import convet_num_to_nodestr +from settings import CONFIG_DIR, CONNECTIONS_DIR, clean_blocks, get_node_str def delete_outbounds(node_str: str): @@ -26,10 +25,9 @@ def delete_outbounds(node_str: str): return "" -def delete_node(choice: int): +def delete_node(node_str): sub_path = CONFIG_DIR+'groups.json' sub_info = {} - node_str = convet_num_to_nodestr(choice) if node_str == '': print('Delete failed') return @@ -69,3 +67,25 @@ def delete_sub(sub_name: str): return print('No such subscription') return + + +def delete_func(blocks: list): + blocks = clean_blocks(blocks) + if (not blocks) or (len(blocks) > 2): + print('Error format,should be `delete sub_name` or `delete sub_name node_name`') + return + elif len(blocks) == 1: + sub_name = blocks[0] + choice = input('Be sure to delete subscription %s ? [y/n] ' % sub_name) + if choice == 'y' or choice == 'Y': + delete_sub(sub_name) + else: + return + elif len(blocks) == 2: + node_name = blocks[len(blocks)-1].replace('%20', ' ') + choice = input('Be sure to delete node %s ? [y/n] ' % node_name) + if choice == 'y' or choice == 'Y': + delete_node(get_node_str(node_name)) + else: + return + pass diff --git a/json2vmess.py b/json2vmess.py index efd4039..8797442 100644 --- a/json2vmess.py +++ b/json2vmess.py @@ -1,15 +1,15 @@ -from settings import CONFIG_DIR, CONNECTIONS_DIR +from settings import CONFIG_DIR, CONNECTIONS_DIR, clean_blocks, get_node_str import json import base64 -from connect_node import convet_num_to_nodestr class UnknowProtocolException(Exception): pass -def show_info(choice): - node_str = convet_num_to_nodestr(choice) +def show_info(blocks: list): + blocks = clean_blocks(blocks) + node_str = get_node_str(blocks[len(blocks)-1]) info_list = convert(node_str) for info in info_list: print('Group %s\nName %s\nProtocol %s\nAddress %s\nPort %s\nLink %s' % ( @@ -140,8 +140,3 @@ def outbounds2vmess(outbound): # print(json.dumps(vobj)) vobj.append(vobj_tmp) return vobj - - -if __name__ == '__main__': - # get_group('zuwltsbpadjy') - show_info(98) diff --git a/parser_create.py b/parser_create.py deleted file mode 100644 index e2b21c2..0000000 --- a/parser_create.py +++ /dev/null @@ -1,55 +0,0 @@ -import argparse - - -def create_parser(): - parser = argparse.ArgumentParser( - description="A python-based v2ray command line client") - - parser.add_argument('--disconnect', - action="store_true", - help="Disconnect v2ray link and close v2ray task") - - parser.add_argument('-u', '--update', - nargs='?', - action="store", - help="Update subscription") - - parser.add_argument('-c', '--connect', - nargs='?', - action="store", - help="Start v2ray and link to the specified link. If there is no choice parameter, the last link will be used by default") - - parser.add_argument('--http_port', - action='store', - help="Proxy http traffic on the specified port, if the parameter is not added, the default proxy port 8889") - - parser.add_argument('--socks_port', - action='store', - help="Proxy socks traffic on the specified port, if the parameter is not added, the default proxy port 11223") - - parser.add_argument('--list_all', - action="store_true", - help="List all nodes") - - parser.add_argument('--current', - action="store_true", - help="Display the node currently in use") - parser.add_argument('--path', '-p', - action="store", - help="v2ray executable file path, the system path is called by default") - - parser.add_argument('--delete_node', - action='store', - help='Delete node') - - parser.add_argument('--delete_sub', - action='store', - help='Delete a subscription') - parser.add_argument('--show_info', - action='store', - help='Display information about a specific node') - - return parser - - -create_parser() diff --git a/port_path.py b/port_path.py new file mode 100644 index 0000000..c36c1df --- /dev/null +++ b/port_path.py @@ -0,0 +1,39 @@ +import json +from settings import LAST_CONNECT, clean_blocks, load_default_config + + +# port +def port_about(blocks: list): + conf = load_default_config() + blocks = clean_blocks(blocks) + if (not len(blocks)) or len(blocks) > 3 or len(blocks) < 1: + print( + 'Error Format,shoould be `port show port` or `port set port [port]`') + return + if blocks[0] == 'show': + print('Http port : %s\nSocks port : %s' % + (conf['http_port'], conf['socks_port'])) + elif blocks[0] == 'set': + http_port = int(input('Please input http port : ')) + socks_port = int(input('Please input socks port : ')) + conf['http_port'] = http_port + conf['socks_port'] = socks_port + with open(LAST_CONNECT, 'w', encoding='utf-8') as f: + f.write(json.dumps(conf, ensure_ascii=False, indent=4)) + + +# path +def path_about(blocks: list): + conf = load_default_config() + blocks = clean_blocks(blocks) + if (not len(blocks)) or len(blocks) > 2 or len(blocks) < 1: + print( + 'Error Format,shoould be `path show` or `path set [path]`') + return + if blocks[0] == 'show': + print('Path : %s' % conf['path']) + elif blocks[0] == 'set': + path = input('Please input v2ray path') + conf['path'] = path + with open(LAST_CONNECT, 'w', encoding='utf-8') as f: + f.write(json.dumps(conf, ensure_ascii=False, indent=4)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..777ecd1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +click==8.0.1 +prompt-toolkit==3.0.19 +urllib3==1.26.6 +wcwidth==0.2.5 diff --git a/settings.py b/settings.py index d482760..86322dd 100644 --- a/settings.py +++ b/settings.py @@ -18,6 +18,20 @@ def get_home_path() -> str: CONNECTIONS_DIR = CONFIG_DIR+"connections/" LAST_CONNECT = CONFIG_DIR+"lastconnect.json" + +def get_default_config(): + if not os.path.exists(CONFIG_DIR): + os.makedirs(CONFIG_DIR) + last_dict = {} + if os.path.exists(CONFIG_DIR+'lastconnect.json'): + with open(CONFIG_DIR+'lastconnect.json', 'r', encoding='utf-8') as f: + last_dict = json.load(f) + else: + last_dict = {'path': '/usr/bin/v2ray', + 'http_port': 8889, 'socks_port': 11223} + return last_dict + + TPL = {} TPL["outbounds"] = """ [ @@ -223,14 +237,75 @@ def get_home_path() -> str: """ +nested_dict_base = { + 'all': None, + 'connect': {}, + 'current': None, + 'delete': {}, + 'disconnect': None, + 'exit': None, + 'info': {}, + 'path': { + 'set': None, + 'show': None + }, + 'port': { + 'set': None, + 'show': None + }, + 'update': {}, +} + def load_TPL(stype): s = TPL[stype] return json.loads(s) +def default_config(): + if os.path.exists(LAST_CONNECT): + return + with open(LAST_CONNECT, 'w', encoding='utf-8') as f: + f.write(json.dumps({'node': '', 'http_port': '8889', 'socks_port': '11223', + 'path': '/usr/bin/v2ray'}, ensure_ascii=False, indent=4)) + + +def get_node_str(node_name): + with open(CONFIG_DIR+'connections.json', 'r', encoding='utf-8') as f: + conn = json.load(f) + for item in conn: + if conn[item]['displayName'].replace(' ', '%20') == node_name: + return item + + +def load_default_config(): + if not os.path.exists(LAST_CONNECT): + return + with open(LAST_CONNECT, 'r', encoding='utf-8') as f: + return json.load(f) + + +def clean_blocks(blocks: list): + for item in blocks.copy(): + if item == '': + blocks.remove(item) + return blocks + + +def trim(s): + import re + if s.startswith(' ') or s.endswith(' '): + return re.sub(r"^(\s+)|(\s+)$", "", s) + return s + + # 设置节点过滤规则,可自定义 def check_link(vmess: json): if vmess['net'] == "tcp": return False return True + + +if __name__ == '__main__': + + print(load_default_config()) diff --git a/update_sub.py b/update_sub.py index 9c3fdaf..798a62a 100644 --- a/update_sub.py +++ b/update_sub.py @@ -6,7 +6,7 @@ import string from vm2obs import convert -from settings import CONFIG_DIR, CONNECTIONS_DIR +from settings import CONFIG_DIR, CONNECTIONS_DIR, clean_blocks sub_path = CONFIG_DIR + 'groups.json' conn_path = CONFIG_DIR+'connections.json' @@ -117,6 +117,14 @@ def update_from_url(url, sub_name=''): load_last_conn_node(node=update_lastconnection(node_name=node_name)) +def get_sub_url(sub_name): + with open(sub_path, 'r', encoding='utf-8') as f: + info = json.load(f) + for item in info: + if info[item]['displayName'] == sub_name.replace('%20', ' '): + return info[item]['subscriptionOption']['address'] + + def update_from_sub(): con = {} with open(sub_path, 'r', encoding='utf-8') as f: @@ -127,3 +135,16 @@ def update_from_sub(): for item in con.keys(): url = con[item]['subscriptionOption']['address'] update_from_url(url) + + +def update(blocks: list): + blocks = clean_blocks(blocks) + if len(blocks) > 1: + print('Error format,should be `update sub_name` or `update [url]`') + if len(blocks) == 0: + update_from_sub() + elif blocks[0].startswith('https://') or blocks[0].startswith('http://'): + sub_name = input('Please input sub_name : ') + update_from_url(blocks[0], sub_name) + else: + update_from_url(get_sub_url(blocks[0])) diff --git a/v_cli.py b/v_cli.py index 2361524..2b0a6e4 100755 --- a/v_cli.py +++ b/v_cli.py @@ -1,63 +1,53 @@ #!/usr/bin/python3 # -*- encoding: utf-8 -*- -from parser_create import create_parser -import connect_node import sys -import json -import os -import update_sub -from settings import CONNECTIONS_DIR,CONFIG_DIR -import del_func +from settings import default_config, trim +import connect_node import json2vmess - - -def get_default_config(): - if not os.path.exists(CONFIG_DIR): - os.makedirs(CONFIG_DIR) - last_dict={} - if os.path.exists(CONFIG_DIR+'lastconnect.json'): - with open(CONFIG_DIR+'lastconnect.json', 'r',encoding='utf-8') as f: - last_dict = json.load(f) - else: - last_dict = {'path': '/usr/bin/v2ray', - 'http_port': 8889, 'socks_port': 11223} - return last_dict - +import del_func +import port_path +import update_sub +from prompt_toolkit import prompt +from completer_generator import generate_completer +from prompt_toolkit.styles import Style if __name__ == '__main__': - default_config = get_default_config() - parser = create_parser() - option = parser.parse_args() - argv_list = sys.argv - if len(argv_list) <= 1: - connect_node.connect_default() - elif option.disconnect: - connect_node.disconnect() - elif option.list_all: - connect_node.print_node() - elif option.current: - connect_node.current() - elif option.delete_node: - del_func.delete_node(int(option.delete_node)) - elif option.delete_sub: - del_func.delete_sub(option.delete_sub) - elif option.show_info: - json2vmess.show_info(int(option.show_info)) - elif (('--update' in argv_list) or ('-u' in argv_list)): - if option.update is not None: - print('Please input subscription name: ', end='') - sub_name = input() - update_sub.update_from_url(option.update, sub_name) - elif option.update is None: - update_sub.update_from_sub() - elif (('--connect' in argv_list) or ('-c' in argv_list)): - if option.connect is not None: - http_port = int( - option.http_port) if option.http_port is not None else default_config['http_port'] - socks_port = int( - option.socks_port) if option.socks_port is not None else default_config['socks_port'] - path = option.path if option.path is not None else default_config['path'] - connect_node.connect(int(option.connect), - path, http_port, socks_port) - elif option.connect is None: - connect_node.connect_default() + # completer = generate_completer() + # print(completer) + default_config() + style = Style.from_dict({ + # User input (default text). + '': '#ffffff', + # Prompt. + 'pound': '#caa9fa', + }) + message = [ + ('class:pound', '>>> '), + ] + while True: + user_input = prompt(message, style=style, + completer=generate_completer()) + # print(user_input) + blocks = trim(user_input).split(' ') + + if blocks[0] == 'all': + connect_node.print_node() + elif blocks[0] == 'info': + json2vmess.show_info(blocks[1:]) + elif blocks[0] == 'disconnect': + connect_node.disconnect() + elif blocks[0] == 'connect': + connect_node.connect(blocks[1:]) + elif blocks[0] == 'delete': + del_func.delete_func(blocks[1:]) + elif blocks[0] == 'port': + port_path.port_about(blocks[1:]) + elif blocks[0] == 'path': + port_path.path_about(blocks[1:]) + elif blocks[0] == 'current': + connect_node.current() + elif blocks[0] == 'update': + update_sub.update(blocks[1:]) + elif blocks[0] == 'exit': + print('Bye!') + sys.exit(0)