Newer
Older
TriliumScripts / htb2trilium / htb2trilium_challenges.py
  1. import os
  2. import json
  3. import requests
  4. import re
  5. import unicodedata
  6. from collections import defaultdict
  7. from bs4 import BeautifulSoup
  8. from datetime import datetime, timezone
  9. from htb_client import HTBClient
  10. from trilium_py.client import ETAPI
  11.  
  12. # Get the absolute path of the script's directory
  13. script_dir = os.path.dirname(os.path.abspath(__file__))
  14.  
  15. # Construct the full path to the config.json file
  16. config_path = os.path.join(script_dir, 'config.json')
  17.  
  18. # Load configuration from the JSON file
  19. with open(config_path, 'r') as f:
  20. config = json.load(f)
  21.  
  22. # Accessing config values
  23. htb_code = config['htb_code']
  24. trilium_server_url = config['trilium_server_url']
  25. trilium_token = config['trilium_token']
  26. trilium_challenges_folder = config['trilium_challenges_folder']
  27. trilium_challenges_template_id = config['trilium_challenges_template_id']
  28.  
  29. def get_timestamp(machine):
  30. # Parse the release date string into a datetime object
  31. release_str = machine['release_date']
  32. dt = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
  33. # Set the timezone to UTC
  34. dt = dt.replace(tzinfo=timezone.utc)
  35. # Get the timestamp
  36. timestamp = dt.timestamp()
  37. return timestamp
  38.  
  39. print("[+] connecting to HTB")
  40. client = HTBClient(password=htb_code)
  41. print("[+] connecting to trilium")
  42. ea = ETAPI(trilium_server_url, trilium_token)
  43. print("[i] version: ", ea.app_info()['appVersion'])
  44.  
  45. print("[i] HTB User:", client.user['id'], "-", client.user['name'])
  46.  
  47.  
  48. print("[+] gathering challenges info")
  49. categories = defaultdict(list)
  50. challenges = client.get_all_challenges()
  51. challenges.sort(key=get_timestamp)
  52. total_completed = 0 # Variable to track total completed challenges
  53.  
  54. print(f"[i] Retrieved {len(challenges)} challenges")
  55.  
  56. def normalise_title(title):
  57. # Strip accents, smart quotes, etc.
  58. title = unicodedata.normalize("NFKD", title)
  59. # Replace fancy punctuation
  60. title = title.replace("’", "'").replace("`", "'").replace("–", "-").replace("“", '"').replace("”", '"')
  61. # Remove excess whitespace
  62. title = re.sub(r'\s+', ' ', title)
  63. return title.strip().lower()
  64.  
  65. # Group challenges by their categories
  66. for challenge in challenges:
  67. categories[challenge['category_name']].append(challenge)
  68. if challenge['is_owned']:
  69. total_completed += 1 # Increment total completed if challenge is owned
  70.  
  71. # Print out the grouped challenges with the number of completed and total challenges in each category
  72. for category, grouped_challenges in categories.items():
  73. total = len(grouped_challenges)
  74. completed = sum(1 for challenge in grouped_challenges if challenge['is_owned']) # Count completed challenges
  75. res = ea.search_note(
  76. search=f"note.title %= '^{category}*'",
  77. ancestorNoteId=trilium_challenges_folder,
  78. ancestorDepth='eq1',
  79. limit=1,
  80. fastSearch=True,
  81. )
  82. catId = ""
  83. if res['results']:
  84. matched_note = res['results'][0]
  85. existing_category = " - ".join(matched_note['title'].split(" - ")[:-1]) # Extract category portion
  86. if existing_category.strip().lower() == category.lower():
  87. ea.patch_note(
  88. noteId=matched_note['noteId'],
  89. title=f"{category} - {completed} / {total}"
  90. )
  91. catId = matched_note['noteId']
  92. print(f"[i] Updated category: {category} - ({completed}/{total})")
  93. else:
  94. new_note = ea.create_note(
  95. parentNoteId=trilium_challenges_folder,
  96. type="text",
  97. title=f"{category} - {completed} / {total}",
  98. content=" ",
  99. )
  100. catId = new_note['note']['noteId']
  101. print(f"[+] Created category: {catId} {category} - ({completed}/{total})")
  102. for challenge in grouped_challenges:
  103. #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
  104. escaped_name = challenge['name'].replace("'", "\\'").replace(",", "\\,")
  105. res2 = ea.search_note(
  106. search=f"note.title = '{escaped_name}'",
  107. ancestorNoteId=catId,
  108. ancestorDepth='eq1',
  109. orderBy=["title"],
  110. limit=1,
  111. fastSearch=True,
  112. )
  113.  
  114. if res2['results']:
  115. note_title = normalise_title(res2['results'][0]['title'])
  116. challenge_title = normalise_title(challenge['name'])
  117.  
  118. if note_title == challenge_title:
  119.  
  120. for attribute in res2['results'][0]['attributes']:
  121. if attribute['name'] == "Difficulty":
  122. ea.patch_attribute(attributeId=attribute['attributeId'], value=challenge['difficulty'])
  123. if attribute['name'] == "Released":
  124. release_str = challenge['release_date']
  125. release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
  126. formatted_release_date = release_date.strftime("%d %B %Y")
  127. ea.patch_attribute(attributeId=attribute['attributeId'], value=formatted_release_date)
  128. if attribute['name'] == "Solved":
  129. if challenge['is_owned']:
  130. ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
  131. else:
  132. ea.patch_attribute(attributeId=attribute['attributeId'], value=" ")
  133. if attribute['name'] == "cssClass":
  134. if challenge['is_owned']:
  135. ea.patch_attribute(attributeId=attribute['attributeId'], value="done")
  136. else:
  137. ea.patch_attribute(attributeId=attribute['attributeId'], value="todo")
  138.  
  139. else: # doesnt already exist, create page
  140. release_str = challenge['release_date']
  141. release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ")
  142. formatted_release_date = release_date.strftime("%d %B %Y")
  143. new_note = ea.create_note(
  144. parentNoteId=catId,
  145. type="text",
  146. title=challenge['name'],
  147. content=" ",
  148. )
  149. ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_challenges_template_id)
  150. ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=challenge['difficulty'])
  151. ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date)
  152. if challenge['is_owned']:
  153. ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done")
  154. ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value="done")
  155. else:
  156. ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo")
  157. ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=" ")
  158.  
  159. print(f"[+] created ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}")
  160.  
  161. print("[+] updating folder name ")
  162. ea.patch_note(noteId=trilium_challenges_folder,title="Challenges - "+str(total_completed)+" / "+str(len(challenges)))
Buy Me A Coffee