import os import json import requests import re import unicodedata from collections import defaultdict from bs4 import BeautifulSoup from datetime import datetime, timezone from htb_client import HTBClient from trilium_py.client import ETAPI # Get the absolute path of the script's directory script_dir = os.path.dirname(os.path.abspath(__file__)) # Construct the full path to the config.json file config_path = os.path.join(script_dir, 'config.json') # Load configuration from the JSON file with open(config_path, 'r') as f: config = json.load(f) # Accessing config values htb_code = config['htb_code'] trilium_server_url = config['trilium_server_url'] trilium_token = config['trilium_token'] trilium_challenges_folder = config['trilium_challenges_folder'] trilium_challenges_template_id = config['trilium_challenges_template_id'] def get_timestamp(machine): # Parse the release date string into a datetime object release_str = machine['release_date'] dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") # Set the timezone to UTC dt = dt.replace(tzinfo=timezone.utc) # Get the timestamp timestamp = dt.timestamp() return timestamp print("[+] connecting to HTB") client = HTBClient(password=htb_code) print("[+] connecting to trilium") ea = ETAPI(trilium_server_url, trilium_token) print("[i] version: ", ea.app_info()['appVersion']) print("[i] HTB User:", client.user['id'], "-", client.user['name']) print("[+] gathering challenges info") categories = defaultdict(list) challenges = client.get_all_challenges() challenges.sort(key=get_timestamp) total_completed = 0 # Variable to track total completed challenges print(f"[i] Retrieved {len(challenges)} challenges") def normalise_title(title): # Strip accents, smart quotes, etc. title = unicodedata.normalize("NFKD", title) # Replace fancy punctuation title = title.replace("’", "'").replace("`", "'").replace("–", "-").replace("“", '"').replace("”", '"') # Remove excess whitespace title = re.sub(r'\s+', ' ', title) return title.strip().lower() # Group challenges by their categories for challenge in challenges: categories[challenge['category_name']].append(challenge) if challenge['is_owned']: total_completed += 1 # Increment total completed if challenge is owned # Print out the grouped challenges with the number of completed and total challenges in each category for category, grouped_challenges in categories.items(): total = len(grouped_challenges) completed = sum(1 for challenge in grouped_challenges if challenge['is_owned']) # Count completed challenges res = ea.search_note( search=f"note.title %= '^{category}*'", ancestorNoteId=trilium_challenges_folder, ancestorDepth='eq1', limit=1, fastSearch=True, ) catId = "" if res['results']: matched_note = res['results'][0] existing_category = " - ".join(matched_note['title'].split(" - ")[:-1]) # Extract category portion if existing_category.strip().lower() == category.lower(): ea.patch_note( noteId=matched_note['noteId'], title=f"{category} - {completed} / {total}" ) catId = matched_note['noteId'] print(f"[i] Updated category: {category} - ({completed}/{total})") else: new_note = ea.create_note( parentNoteId=trilium_challenges_folder, type="text", title=f"{category} - {completed} / {total}", content=" ", ) catId = new_note['note']['noteId'] print(f"[+] Created category: {catId} {category} - ({completed}/{total})") for challenge in grouped_challenges: #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") escaped_name = challenge['name'].replace("'", "\\'").replace(",", "\\,") res2 = ea.search_note( search=f"note.title = '{escaped_name}'", ancestorNoteId=catId, ancestorDepth='eq1', orderBy=["title"], limit=1, fastSearch=True, ) if res2['results']: note_title = normalise_title(res2['results'][0]['title']) challenge_title = normalise_title(challenge['name']) if note_title == challenge_title: for attribute in res2['results'][0]['attributes']: if attribute['name'] == "Difficulty": ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty']) if attribute['name'] == "Released": release_str = challenge['release_date'] release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") formatted_release_date = release_date.strftime("%d %B %Y") ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date) if attribute['name'] == "Solved": if challenge['is_owned']: ea.patch_attribute(attributeId=attribute['attributeId'], value="done") else: ea.patch_attribute(attributeId=attribute['attributeId'], value=" ") if attribute['name'] == "cssClass": if challenge['is_owned']: ea.patch_attribute(attributeId=attribute['attributeId'], value="done") else: ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") else: # doesnt already exist, create page release_str = challenge['release_date'] release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") formatted_release_date = release_date.strftime("%d %B %Y") new_note = ea.create_note( parentNoteId=catId, type="text", title=challenge['name'], content=" ", ) ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id) ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty']) ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) if challenge['is_owned']: ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done") else: ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ") print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") print("[+] updating folder name ") ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))