From 177d3fd521fe3487cd7e7b25b96309d82b093fac Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 14:03:41 -0500 Subject: [PATCH 01/15] chore: improve logging Co-authored-by: tigattack --- info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.plist b/info.plist index 9a90d4f..d8fccf3 100644 --- a/info.plist +++ b/info.plist @@ -372,7 +372,7 @@ queuemode 1 runningsubtext - importing.... + Importing TOTP accounts and downloading icons.... script python3 main.py import scriptargtype From 9942848f149694694e6dc7c829848fd07d57be0e Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 14:05:05 -0500 Subject: [PATCH 02/15] chore: place constants globally --- src/constants.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/constants.py diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..722cb33 --- /dev/null +++ b/src/constants.py @@ -0,0 +1,11 @@ +from os import environ +from pathlib import Path + +# Keychain service and account for storing the TOTP accounts +KEYCHAIN_SERVICE = "ente-totp-alfred-workflow" +KEYCHAIN_ACCOUNT = "totp_secrets" + +# Use an environment variable to cache the JSON data to reduce keychain calls +CACHE_ENV_VAR = "TOTP_CACHE" + +ICONS_FOLDER = Path(environ["alfred_workflow_data"]) / "service_icons" From dd2cb410d529cdc17008dc8cf756a432890b5eb6 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 14:07:12 -0500 Subject: [PATCH 03/15] feat: grab service names from cache --- main.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 325103e..cd8f0c3 100755 --- a/main.py +++ b/main.py @@ -52,7 +52,18 @@ output_alfred_message("Failed to export TOTP data", str(e)) else: try: - import_result = ente_export_to_keychain(ente_export_path) + service_names_list: list[str] = [] + result = ente_export_to_keychain(ente_export_path) + + variables = result.variables + totp_accounts = json.loads(variables[CACHE_ENV_VAR]) + + for k, _ in totp_accounts.items(): + try: + get_icon(sanitize_service_name(k), ICONS_FOLDER) + except Exception as e: + logger.warning(f"Failed to download icon: {e}") + output_alfred_message( "Imported TOTP data", f"Successfully imported {import_result.count} TOTP accounts to keychain and Alfred cache.", From 817f15023b2b0a42d06a9f5f1455867473871513 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 14:24:52 -0500 Subject: [PATCH 04/15] feat: add name sanitazion Co-authored-by: tigattack --- src/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils.py b/src/utils.py index d637a9c..a05674e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -3,6 +3,10 @@ from src.models import AlfredOutput, AlfredOutputItem, TotpAccounts +def sanitize_service_name(service_name): + return service_name.split("-")[0].strip().replace(" ", "").lower() + + def str_to_bool(val): if isinstance(val, str): val = val.lower() From 49fac494f409751ed5a47354dec69c06dc63e2d1 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 14:59:43 -0500 Subject: [PATCH 05/15] feat: add simplepycons dep --- poetry.lock | 12 +++++++++++- pyproject.toml | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7692993..7825388 100644 --- a/poetry.lock +++ b/poetry.lock @@ -490,6 +490,16 @@ files = [ cryptography = ">=2.0" jeepney = ">=0.6" +[[package]] +name = "simplepycons" +version = "1!13.18.0" +description = "Python Wrapper for the simpleicons (https://simpleicons.org) library" +optional = false +python-versions = ">=3.10" +files = [ + {file = "simplepycons-1!13.18.0-py3-none-any.whl", hash = "sha256:66c88e69a6e5fd72e521cd24f3cd6e8d83201264ffb5684f3e95b822d5c0dc17"}, +] + [[package]] name = "urllib3" version = "2.2.3" @@ -529,4 +539,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "37a61e51ac29f4f9991786b869811a37415caa5e79f496da1ed9436ed49fae82" +content-hash = "2b374d8c39c2c84430788d7efcd1fd12267a5d536aaf3485f94405c57ff36c46" diff --git a/pyproject.toml b/pyproject.toml index 5dc6a9b..8901820 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,8 @@ version = "2.0.0" python = "^3.11" pyotp = "^2.9.0" keyring = "^25.5.0" -requests = "^2.3.2" +simplepycons = "^1!13.18.0" +requests = "^2.32.3" [build-system] requires = ["poetry-core>=1.8"] From c775c9dfcb402dbfc372c16ac3b00b5cb6f6ca23 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 15:00:55 -0500 Subject: [PATCH 06/15] fix: variable call --- src/totp_accounts_manager.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/totp_accounts_manager.py b/src/totp_accounts_manager.py index 309a422..2e39388 100644 --- a/src/totp_accounts_manager.py +++ b/src/totp_accounts_manager.py @@ -5,7 +5,6 @@ import pyotp -from src.icon_downloader import get_icon_path from src.models import ( AlfredOutput, AlfredOutputItem, @@ -18,10 +17,6 @@ logger = logging.getLogger(__name__) -USERNAME_IN_TITLE = str_to_bool(os.getenv("USERNAME_IN_TITLE", "False")) -USERNAME_IN_SUBTITLE = str_to_bool(os.getenv("USERNAME_IN_SUBTITLE", "False")) - - def parse_ente_export(file_path: str) -> TotpAccounts: accounts = TotpAccounts() @@ -58,6 +53,10 @@ def parse_ente_export(file_path: str) -> TotpAccounts: def format_totp_result(accounts: TotpAccounts) -> AlfredOutput: """Format TOTP accounts for Alfred.""" result = AlfredOutput([]) + + username_in_title = str_to_bool(os.getenv("USERNAME_IN_TITLE", "False")) + username_in_subtitle = str_to_bool(os.getenv("USERNAME_IN_SUBTITLE", "False")) + try: for service_name, service_data in accounts.items(): # Generate TOTP @@ -74,27 +73,24 @@ def format_totp_result(accounts: TotpAccounts) -> AlfredOutput: # Update title and subtitle title = ( f"{sanitized_service_name} - {service_data.username}" - if service_data.username and USERNAME_IN_TITLE + if service_data.username and username_in_title else sanitized_service_name ) subtitle = ( f"Current TOTP: {current_totp} | Next TOTP: {next_totp}, {time_remaining} seconds left" + ( f" - {service_data.username}" - if service_data.username and USERNAME_IN_SUBTITLE + if service_data.username and username_in_subtitle else "" ) ) - # Add icon dynamically for each item - icon_path = get_icon_path(sanitized_service_name) - result.items.append( AlfredOutputItem( title=title, subtitle=subtitle, arg=current_totp, - icon=AlfredOutputItemIcon(path=icon_path), # Add the icon here + icon=AlfredOutputItemIcon.from_service(sanitized_service_name), ) ) @@ -106,5 +102,3 @@ def format_totp_result(accounts: TotpAccounts) -> AlfredOutput: result.items = [ AlfredOutputItem(title="Unexpected error in format_totp_result function.") ] - - return result From b07d71cdd5793caa0310e0db3e2f1fb1ff15a0b5 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 15:01:40 -0500 Subject: [PATCH 07/15] chore: add additional imports --- main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main.py b/main.py index cd8f0c3..0be60b6 100755 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +import json import logging import os import sys @@ -18,10 +19,19 @@ from src.totp_accounts_manager import format_totp_result # noqa: E402 from src.utils import ( # noqa: E402 fuzzy_search_accounts, + sanitize_service_name, output_alfred_message, str_to_bool, ) +from src.constants import ( # noqa: E402 + CACHE_ENV_VAR, + ICONS_FOLDER, +) + +from src.icon_downloader import get_icon # noqa: E402 + + logger = logging.getLogger(__name__) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" From 861693f5764c550cf6fbe2936e11ff1beefddffb Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 15:03:12 -0500 Subject: [PATCH 08/15] chore: adjust logging message to reflect actions --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 0be60b6..fc226e8 100755 --- a/main.py +++ b/main.py @@ -76,8 +76,8 @@ output_alfred_message( "Imported TOTP data", - f"Successfully imported {import_result.count} TOTP accounts to keychain and Alfred cache.", - import_result.variables, + f"Successfully imported {result.count} TOTP accounts and downloaded icons.", + variables=variables, ) except Exception as e: logger.exception( From f7e23691cfd8e5059ddb33ee67a3fa237dbef7b8 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 15:04:21 -0500 Subject: [PATCH 09/15] chore: adjust imports for constants --- src/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models.py b/src/models.py index 94357a5..942e893 100644 --- a/src/models.py +++ b/src/models.py @@ -3,7 +3,7 @@ from dataclasses import asdict, dataclass from typing import Any -from src.icon_downloader import get_icon_path +from src.constants import ICONS_FOLDER # https://www.alfredapp.com/help/workflows/inputs/script-filter/json From a5769afc44a7e3b2094051aaa7a7a628220f9c55 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 15:05:26 -0500 Subject: [PATCH 10/15] chore: set icon path to the class --- src/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/models.py b/src/models.py index 942e893..2517858 100644 --- a/src/models.py +++ b/src/models.py @@ -23,8 +23,10 @@ def from_service(cls, service_name: str): Returns: AlfredOutputItemIcon: An instance with the correct icon path. """ - icon_path = get_icon_path(service_name) - return cls(path=icon_path) + icon_path = ICONS_FOLDER / f"{service_name}.svg" + if icon_path.exists(): + return cls(path=str(icon_path)) + return cls() def to_dict(self): return {k: v for k, v in asdict(self).items() if v is not None} From ae3d4ba2b1cb8e5d18f57adfe61fd7507d3c86e7 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 15:07:02 -0500 Subject: [PATCH 11/15] feat: grab icons from multiple sources --- src/icon_downloader.py | 133 ++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 69 deletions(-) diff --git a/src/icon_downloader.py b/src/icon_downloader.py index 90fc3a2..bcba7d3 100644 --- a/src/icon_downloader.py +++ b/src/icon_downloader.py @@ -1,82 +1,77 @@ import logging -import os -import pathlib +from pathlib import Path +from urllib.parse import urljoin import requests +from simplepycons import all_icons + +logger = logging.getLogger(__name__) + + +def get_ente_custom_icon(name: str): + icons_database_url = "https://raw.githubusercontent.com/ente-io/ente/refs/heads/main/auth/assets/custom-icons/_data/custom-icons.json" + ente_custom_icons_db = "https://raw.githubusercontent.com/ente-io/ente/refs/heads/main/auth/assets/custom-icons/icons/" + + try: + response = requests.get(icons_database_url) + + if response.status_code == 200: + ente_custom_icons = response.json() + matching_icon = [ + icon["slug"] if icon.get("slug") else icon["title"].lower() + for icon in ente_custom_icons.get("icons", []) + if name.lower() + in [ + icon["title"].lower(), + icon.get("slug", "").lower(), + *[name.lower() for name in icon.get("altNames", [])], + ] + ] + # matching_icon: next(icon["slug"] for icon in iter(ente_custom_icons) if name.lower() in ) + if matching_icon: + response = requests.get( + urljoin(ente_custom_icons_db, f"{matching_icon[0]}.svg") + ) + response.raise_for_status() + return response.text + else: + logger.error(f"Failed to fetch custom icons: {response.status_code}") + except requests.RequestException as e: + logger.error(f"Error while fetching custom icons: {e}") -LOGO_DEV_API_URL = "https://img.logo.dev/{domain}?token={api_key}" -LOGO_DEV_API_KEY = "pk_T0ZUG4poQGqfGcFoeCpRww" -ICONS_FOLDER = pathlib.Path.home() / ".local/share/ente-totp/icons" - - -def sanitize_service_name(service_name): - return service_name.split("-")[0].strip().replace(" ", "") - - -def download_icon(service_name): - # Downloads an icon for a given service name. - - sanitized_name = sanitize_service_name(service_name) - if not LOGO_DEV_API_KEY: - logging.warning("LOGO_DEV_API_KEY is not set. Skipping icon download.") - return "icon.png" +def get_simplepycons_icon(name: str): + """ + Gets an icon for a given service name. - # Determine the domain for the icon service - domain = ( - sanitized_name.lower() - if "." in sanitized_name - else f"{sanitized_name.lower()}.com" - ) - icon_url = LOGO_DEV_API_URL.format(domain=domain, api_key=LOGO_DEV_API_KEY) - icon_path = ICONS_FOLDER / f"{sanitized_name.replace('.', '_').lower()}.png" - - if not icon_path.exists(): - try: - logging.warning( - f"Attempting to download icon for {sanitized_name} from {icon_url}" - ) - response = requests.get(icon_url, timeout=5) - logging.warning( - f"Response status for {sanitized_name}: {response.status_code}" - ) - - if response.status_code == 200: - ICONS_FOLDER.mkdir(parents=True, exist_ok=True) - with open(icon_path, "wb") as icon_file: - icon_file.write(response.content) - logging.warning( - f"Icon downloaded successfully for {sanitized_name} at {icon_path}" - ) - else: - logging.warning( - f"Failed to download icon for {sanitized_name}. Status code: {response.status_code}" - ) - return "icon.png" - except requests.RequestException as e: - logging.warning(f"Request failed for {sanitized_name}: {e}") - return "icon.png" - else: - logging.warning(f"Icon already exists for {sanitized_name} at {icon_path}") + Returns: + str: Path to the icon, or the default icon if retrieving the object fails. + """ - return str(icon_path) + try: + simplepycons_icon = all_icons[name].raw_svg # type: ignore + return str(simplepycons_icon) + except KeyError: + logger.warning(f"Icon for '{name}' not found in Simplepycons.") -def get_icon_path(service_name): - # Gets the path to the icon for a given service, downloading it if necessary. - sanitized_name = sanitize_service_name(service_name) - sanitized_service_name = sanitized_name.replace(" ", "").lower() - icon_path = ICONS_FOLDER / f"{sanitized_service_name}.png" - if icon_path.exists(): - logging.warning(f"Using downloaded icon for {sanitized_name}") - return str(icon_path) +def get_icon(service: str, icons_dir: Path): + icons_dir.mkdir(parents=True, exist_ok=True) + icon_path = icons_dir / f"{service}.svg" - logging.warning(f"No icon found for {sanitized_name}, attempting to download.") - return download_icon(sanitized_name) + ente_custom_icon_url = get_ente_custom_icon(service) + simplepycons_icon = get_simplepycons_icon(service) + icon = ( + ente_custom_icon_url + if ente_custom_icon_url + else simplepycons_icon + if simplepycons_icon + else None + ) -def download_icons(services): - ICONS_FOLDER.mkdir(parents=True, exist_ok=True) - for service in services: - download_icon(service) + if icon: + with open(icon_path, mode="w") as icon_file: + icon_file.write(icon) + logger.debug(f"Icon imported successfully for {service} at {icon_path}") From a7be5dda719afebd9dc61a5113439cfa202c24b6 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 15:24:27 -0500 Subject: [PATCH 12/15] fix: ensure output does not return None --- src/totp_accounts_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/totp_accounts_manager.py b/src/totp_accounts_manager.py index 2e39388..1953e00 100644 --- a/src/totp_accounts_manager.py +++ b/src/totp_accounts_manager.py @@ -94,11 +94,12 @@ def format_totp_result(accounts: TotpAccounts) -> AlfredOutput: ) ) - if not result.items: - result.items = [AlfredOutputItem(title="No matching services found.")] + if not result.items: + result.items = [AlfredOutputItem(title="No matching services found.")] except Exception as e: logging.exception(f"Error: {str(e)}") result.items = [ AlfredOutputItem(title="Unexpected error in format_totp_result function.") ] + return result From 997d0db6e59036afb8556adf397c045423641478 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 16:13:32 -0500 Subject: [PATCH 13/15] feat: set primary_color to svg fill attr --- src/icon_downloader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/icon_downloader.py b/src/icon_downloader.py index bcba7d3..5054c62 100644 --- a/src/icon_downloader.py +++ b/src/icon_downloader.py @@ -49,11 +49,12 @@ def get_simplepycons_icon(name: str): """ try: - simplepycons_icon = all_icons[name].raw_svg # type: ignore - return str(simplepycons_icon) - + icon = all_icons[name] # type: ignore except KeyError: logger.warning(f"Icon for '{name}' not found in Simplepycons.") + else: + icon = icon.customize_svg_as_str(fill=icon.primary_color) + return str(icon) def get_icon(service: str, icons_dir: Path): From 20068efbc630fcda5c46e67d31571578ec2bf31a Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 16:14:05 -0500 Subject: [PATCH 14/15] add launch.json --- .vscode/launch.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8beafeb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: main - search arg", + "type": "debugpy", + "env": { + "alfred_workflow_data": "~/.local/share/ente-totp/icons" + }, + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "args": [ + "search", + "cloudflare" + ] + } + ] +} From 62874fefac00cf40a11c43d2a3ebfb1b6e747356 Mon Sep 17 00:00:00 2001 From: Bryan Jones Date: Tue, 26 Nov 2024 16:17:17 -0500 Subject: [PATCH 15/15] feat: bump version --- info.plist | 46 +++++++++++++++++++++++----------------------- pyproject.toml | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/info.plist b/info.plist index d8fccf3..e60546d 100644 --- a/info.plist +++ b/info.plist @@ -446,79 +446,79 @@ python3 build.py 046A5FD1-F7FF-430A-A92C-62D5AC3C5328 xpos - 965 + 965.0 ypos - 445 + 445.0 3101A957-02C3-4D0C-8B13-1C3721459261 xpos - 280 + 280.0 ypos - 650 + 650.0 4C82069F-5857-4F72-9807-0A05DACC1F7C xpos - 540 + 540.0 ypos - 505 + 505.0 4D86E603-A8EB-4EE2-BB90-12BECBE1D574 xpos - 540 + 540.0 ypos - 380 + 380.0 5B0C442D-7335-4973-A2EE-E58281C60266 xpos - 965 + 965.0 ypos - 710 + 710.0 7F4B5FD3-5072-47C9-8129-E130EF0D2E59 xpos - 960 + 960.0 ypos - 305 + 305.0 7F6F4EBD-9981-4B82-B039-7183EA9EEB15 xpos - 540 + 540.0 ypos - 435 + 435.0 A8D008AC-C61A-4037-A423-E33492A4CC62 xpos - 40 + 40.0 ypos - 440 + 440.0 C7B89588-34B4-49B4-9848-43975E80F16B xpos - 260 + 260.0 ypos - 450 + 450.0 ECD812F2-4D1E-41CA-B344-81B25B4A6357 xpos - 540 + 540.0 ypos - 565 + 565.0 F80F0C10-7220-4984-8EED-67425751FBC3 xpos - 960 + 960.0 ypos - 585 + 585.0 userconfigurationconfig @@ -647,7 +647,7 @@ python3 build.py variablesdontexport version - 2.0.0 + 2.1.0 webaddress https://github.com/chkpwd/alfred-ente-auth diff --git a/pyproject.toml b/pyproject.toml index 8901820..1ee6893 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ authors = ["Bryan Jones "] license = "MIT" readme = "README.md" package-mode = false -version = "2.0.0" +version = "2.1.0" [tool.poetry.dependencies] python = "^3.11"