diff --git a/.gitignore b/.gitignore index eb33d99..c550d1e 100644 --- a/.gitignore +++ b/.gitignore @@ -164,4 +164,6 @@ cython_debug/ *.token *.yml *.old -gkeepapi/* \ No newline at end of file +gkeepapi/* +templates/* +SysTray.py \ No newline at end of file diff --git a/MoveLowPrioritytoShoppingList.py b/MoveLowPrioritytoShoppingList.py index 8a35bb9..871f0b4 100644 --- a/MoveLowPrioritytoShoppingList.py +++ b/MoveLowPrioritytoShoppingList.py @@ -6,9 +6,32 @@ """ import os import gkeepapi -import json - +import keyring +import maskpass +import re +try: + import simplejson as json +except ImportError: + import json from time import perf_counter as timer, sleep +if os.name == 'nt': + from infi.systray import SysTrayIcon +else: + pass + +try: + if os.name == 'nt': + from infi.systray import SysTrayIcon +except ImportError: + pass +# Define constants at the top of your file +GOOGLE_KEEP_MASTER_TOKEN = 'Google Keep Master Token' +# Define the base directory for your application +BASE_DIR = os.path.abspath(os.getcwd()) +CONFIG_FILE = os.path.join(BASE_DIR, 'config.json') +# Adjust the path for keep_notes.json +KEEP_NOTES_PATH = os.path.join(BASE_DIR, 'keep_notes.json') + def first_run() -> bool: @@ -21,7 +44,7 @@ def first_run() -> bool: Returns: bool: 'True' if first run, 'False' if not. """ - return not os.path.isfile('config.json') # The 'not' is there to flip the return value of isfile + return not os.path.isfile(CONFIG_FILE) # The 'not' is there to flip the return value of isfile def load_settings() -> dict: @@ -31,10 +54,82 @@ def load_settings() -> dict: Returns: dict: Dictionary of settings """ - with open('config.json', 'r') as openfile: - # Reading from json file - settings = json.load(openfile) - return settings + try: + with open(CONFIG_FILE, 'r') as openfile: + # Reading the settings from json file + settings = json.load(openfile) + # The Google Master Token is stored on the system keyring and extracted from there + settings['master_token'] = keyring.get_password( + GOOGLE_KEEP_MASTER_TOKEN, settings['username']) + return settings + except FileNotFoundError: + print(f"{CONFIG_FILE} not found.") + return {} + except Exception as e: + print(f"An error occurred: {e}") + return {} + + +def check_username(username: str) -> None: + """ + Check if the username is a valid email address. + + Args: + username (str): Email address of user + + Returns: + None + """ + assert username != "", f"Username is empty" + email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + assert re.match( + email_regex, username), f"Invalid email address: {'username'}" + return None + + +def check_token(token: str) -> None: + """ + Checks to ensure token is not empty or too short. + * I'm just guessing about the minimum length of a token though. + * If you run into issues, feel free to change it and send a pull request. + + Args: + token (str): Google Keep Master Token + + Returns: + None + """ + assert token != "", f"Master token is empty, got: {token}" + assert len( + token) > 100, f"Master token is too short, got: {len(token)} characters" + + return None + + +def check_list_names(keep: object, primary_list: list, low_priority_list: list) -> None: + """ + Check that the primary and low priority list names are not empty or non-existent in the keep object. + + Args: + keep (obj): Google Keep object + primary_list (list): Names of primary lists + low_priority_list (list): Names of low priority lists + + Returns: + None + """ + assert primary_list != [], f"Primary list name is empty" + for list_name in primary_list: + assert list_name != "", f"Invalid Primary list name: {list_name}" + assert low_priority_list != [], f"Low priority list name is empty" + for list_name in low_priority_list: + assert list_name != "", f"Invalid Low priority list name: {list_name}" + # Check if the list names exist in the keep object + for list_name in primary_list: + assert keep.list(list_name) is not None, f"Primary list does not exist: {list_name}" + for list_name in low_priority_list: + assert keep.list(list_name) is not None, f"Low priority list does not exist: {list_name}" + return None def check_low_priority_items(keep: object, low_priority_list: str) -> list: @@ -59,6 +154,43 @@ def check_low_priority_items(keep: object, low_priority_list: str) -> list: return items_to_move +def check_num_sets(num_sets: int) -> None: + """ + Check that the number of sets is greater than 0. + + Args: + num_sets (int): Number of sets + + Returns: + None + """ + assert num_sets > 0, f"Number of sets must be greater than 0, got: {num_sets}" + return None + + +def check_settings(keep: object, config: dict) -> None: + """ + Check that the settings file is not broken. + + Args: + keep (obj): Google Keep object + config (dict): Dictionary of settings + + Returns: + bool: 'True' if settings are valid, 'False' if not + """ + assert config != {}, f"{CONFIG_FILE} is empty" # Check that config is not empty + assert config['first_run_flag'] == "True", f"{CONFIG_FILE} maybe corrupted" + check_token(config['master_token']) + check_username(config["username"]) + check_num_sets(config['num_sets']) + # Check to see that there are no empty elements or empty strings in the primary and low prioritylist + check_list_names(keep, config['primary_list'], config['low_priority_list']) + print(f'Loaded settings. Username: {config["username"]}') + + return None + + def move_items_to_primary_list(keep: object, primary_list: str, items_to_move: list) -> None: """ Move ticked items from low priority list to primary list. @@ -75,35 +207,115 @@ def move_items_to_primary_list(keep: object, primary_list: str, items_to_move: l if primary_list in note.title: for item in items_to_move: # Add the item to the top of the primary list unticked - note.add(item.text, False, gkeepapi.node.NewListItemPlacementValue.Top) + note.add(item.text, False, + gkeepapi.node.NewListItemPlacementValue.Top) + return None + +def delete_ticked_items_from_primary_list(keep: object, primary_list: str) -> None: + """ + Delete ticked items from primary list. + + Args: + keep (obj): Google Keep object + primary_list (str): Name of primary list + + Returns: + None + """ + for note in keep.all(): + if primary_list in note.title: + for item in note: + if item.checked: + item.delete() + return None +def loop(keep: object, config: dict) -> None: + """ + Synchronize the changes to the Google Keep server and continuously move low priority items to the primary list. + + Args: + keep (object): The object representing the Google Keep instance. + config (dict): The dictionary containing the configuration settings. + + Returns: + None + """ + while True: + # Syc the changes to the Google Keep server + keep.sync() + items_to_move = check_low_priority_items( + keep, config['low_priority_list']) + + # if no items to move, return to check for low priority items + if items_to_move == []: + # print('No items to move') + pass + else: + move_items_to_primary_list( + keep, config['primary_list'], items_to_move) + print( + f'Moved {len(items_to_move)} items to {config["primary_list"]}') + items_to_move = [] + # Dump Keep Notes to disk for caching + with open("keep_notes.json", "w") as outfile: + json.dump(keep.dump(), outfile) + # Rate restriction to prevent API ban from Google + sleep(0.5) + def main(): # start_time = timer() - keep = gkeepapi.Keep() if first_run(): print("First run") username = input("Google Keep Username: ") - master_token = input( + check_username(username) + master_token = maskpass.askpass( "Google Keep Master Token (Use the included DockerFile to get one): ") - primary_list = input('Name of Primary List: ') - low_priority_list = input('Name of Low Priority List: ') - config = { - "first_run_flag": "True", - "username": username, - "master_token": master_token, - "primary_list": primary_list, - "low_priority_list": low_priority_list - } - json_object = json.dumps(config, indent=4) - - # Writing config.json - with open("config.json", "w") as outfile: - outfile.write(json_object) - + check_token(master_token) # Load all Keep Notes - keep.resume(username, master_token) + try: + keep = gkeepapi.Keep() + keep.resume(username, master_token) + except Exception as e: + print(f"Username or master token is invalid: {e}") + exit(-1) + # If the login above is successful, write the master token to system keyring + keyring.set_password("Google Keep Master Token", + username, master_token) + num_sets = int( + input('Number of Sets of Lists (1 Set contains two Lists ): ')) + check_num_sets(num_sets) + + primary_lists = [] + low_priority_lists = [] + for i in range(int(num_sets)): + primary_list = input(f'Name of Primary List {i+1}: ') + low_priority_list = input(f'Name of Low Priority List {i+1}: ') + primary_lists.append(primary_list) + low_priority_lists.append(low_priority_list) + check_list_names(primary_lists, low_priority_lists) + # Master token is stored on the system keyring so deliberately empty + config = { + "first_run_flag": "True", + "username": username, + "master_token": "", + "num_sets": num_sets, + "primary_list": primary_lists, + "low_priority_list": low_priority_lists + } + json_object = json.dumps(config, indent=4) + + # Writing config.json + with open("config.json", "w") as outfile: + outfile.write(json_object) + config["master_token"] = master_token + + # Before loading the Google Keep object check the settings + if check_settings(keep, config): + pass + else: + raise Exception("Settings are not valid") # Dump Keep Notes to disk for caching with open("keep_notes.json", "w") as outfile: @@ -111,29 +323,26 @@ def main(): else: config = load_settings() - print(f'Loaded settings. Username: {config["username"]}') - assert config['first_run_flag'] == "True" + check_settings(config) # Restore notes from database or online keep.resume(config['username'], config['master_token'], state=json.load(open("keep_notes.json"))) # end_time = timer() # print(f'Time to initialize: {(end_time - start_time)}s') - while True: - # Syc the changes to the Google Keep server - keep.sync() - items_to_move = check_low_priority_items( - keep, config['low_priority_list']) - # if no items to move, return to check for low priority items - if items_to_move == []: - # print('No items to move') - pass - else: - move_items_to_primary_list(keep, config['primary_list'], items_to_move) - print(f'Moved {len(items_to_move)} items to {config["primary_list"]}') - items_to_move = [] - sleep(0.5) + # Start SysTray Icon if running on Windows, do nothing if on Linux + if os.name == 'nt': + hover_text = "Move Low Priority Items to Primary List in Google Keep" + sysTrayIcon = SysTrayIcon("keep_notes_automation.ico", hover_text, + default_menu_index=1) + try: + sysTrayIcon.start() + except KeyboardInterrupt: + sysTrayIcon.shutdown() + else: + pass + loop(keep, config) if __name__ == '__main__':