diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +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_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + 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("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +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_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + 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("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py new file mode 100644 index 0000000..1e32e59 --- /dev/null +++ b/htb2trilium/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +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_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_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 sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['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": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['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=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +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_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + 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("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py new file mode 100644 index 0000000..1e32e59 --- /dev/null +++ b/htb2trilium/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +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_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_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 sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['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": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['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=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py new file mode 100644 index 0000000..a35929a --- /dev/null +++ b/htb2trilium/htb_client.py @@ -0,0 +1,214 @@ +import requests +import warnings + +warnings.filterwarnings("ignore") + +class HTBClient: + def __init__(self, password): + self.password = password + self.base_url = 'https://labs.hackthebox.com/api/v4' + self.proxies = { + #"http": "http://127.0.0.1:8080", # Burp proxy for HTTP traffic + #"https": "http://127.0.0.1:8080" # Burp proxy for HTTPS traffic + } + # Fetch user info + self.user = self.get_user_info() + + # Fetch user stats and store them in self.user + user_owns, root_owns, respects = self.get_user_stats(self.user['id']) + self.user['user_owns'] = user_owns + self.user['root_owns'] = root_owns + self.user['respects'] = respects + + def get_user_info(self): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/info', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}") + + # Return the user info as a dictionary + data = response.json().get('info') + return {'id': data['id'], 'name': data['name'], 'email': data['email']} + + def get_user_stats(self, user_id): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/profile/basic/{user_id}', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}") + + # Extract user statistics from the response + data = response.json().get('profile') + user_owns = data['user_owns'] + root_owns = data['system_owns'] + respects = data['respects'] + return user_owns, root_owns, respects + + def get_active_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_retired_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_all_machines(self): + # Combine active and retired machines, ensuring no duplicates + active_machines = self.get_active_machines() + retired_machines = self.get_retired_machines() + + all_machines = active_machines + retired_machines + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + unique_machines = [] + + for machine in all_machines: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + unique_machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + return unique_machines + + def get_all_challenges(self): + challenges = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/challenges?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}") + + data = response.json() + for challenge in data['data']: + if challenge['id'] not in seen_ids and challenge['name'] not in seen_names: + challenges.append(challenge) + seen_ids.add(challenge['id']) + seen_names.add(challenge['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return challenges + + def get_all_sherlocks(self): + sherlocks = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/sherlocks?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}") + + data = response.json() + for sherlock in data['data']: + if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names: + sherlocks.append(sherlock) + seen_ids.add(sherlock['id']) + seen_names.add(sherlock['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return sherlocks \ No newline at end of file diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +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_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + 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("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py new file mode 100644 index 0000000..1e32e59 --- /dev/null +++ b/htb2trilium/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +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_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_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 sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['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": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['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=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py new file mode 100644 index 0000000..a35929a --- /dev/null +++ b/htb2trilium/htb_client.py @@ -0,0 +1,214 @@ +import requests +import warnings + +warnings.filterwarnings("ignore") + +class HTBClient: + def __init__(self, password): + self.password = password + self.base_url = 'https://labs.hackthebox.com/api/v4' + self.proxies = { + #"http": "http://127.0.0.1:8080", # Burp proxy for HTTP traffic + #"https": "http://127.0.0.1:8080" # Burp proxy for HTTPS traffic + } + # Fetch user info + self.user = self.get_user_info() + + # Fetch user stats and store them in self.user + user_owns, root_owns, respects = self.get_user_stats(self.user['id']) + self.user['user_owns'] = user_owns + self.user['root_owns'] = root_owns + self.user['respects'] = respects + + def get_user_info(self): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/info', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}") + + # Return the user info as a dictionary + data = response.json().get('info') + return {'id': data['id'], 'name': data['name'], 'email': data['email']} + + def get_user_stats(self, user_id): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/profile/basic/{user_id}', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}") + + # Extract user statistics from the response + data = response.json().get('profile') + user_owns = data['user_owns'] + root_owns = data['system_owns'] + respects = data['respects'] + return user_owns, root_owns, respects + + def get_active_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_retired_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_all_machines(self): + # Combine active and retired machines, ensuring no duplicates + active_machines = self.get_active_machines() + retired_machines = self.get_retired_machines() + + all_machines = active_machines + retired_machines + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + unique_machines = [] + + for machine in all_machines: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + unique_machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + return unique_machines + + def get_all_challenges(self): + challenges = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/challenges?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}") + + data = response.json() + for challenge in data['data']: + if challenge['id'] not in seen_ids and challenge['name'] not in seen_names: + challenges.append(challenge) + seen_ids.add(challenge['id']) + seen_names.add(challenge['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return challenges + + def get_all_sherlocks(self): + sherlocks = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/sherlocks?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}") + + data = response.json() + for sherlock in data['data']: + if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names: + sherlocks.append(sherlock) + seen_ids.add(sherlock['id']) + seen_names.add(sherlock['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return sherlocks \ No newline at end of file diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png new file mode 100755 index 0000000..5bcb448 --- /dev/null +++ b/htb2trilium/images/machine_view.png Binary files differ diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +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_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + 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("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py new file mode 100644 index 0000000..1e32e59 --- /dev/null +++ b/htb2trilium/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +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_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_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 sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['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": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['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=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py new file mode 100644 index 0000000..a35929a --- /dev/null +++ b/htb2trilium/htb_client.py @@ -0,0 +1,214 @@ +import requests +import warnings + +warnings.filterwarnings("ignore") + +class HTBClient: + def __init__(self, password): + self.password = password + self.base_url = 'https://labs.hackthebox.com/api/v4' + self.proxies = { + #"http": "http://127.0.0.1:8080", # Burp proxy for HTTP traffic + #"https": "http://127.0.0.1:8080" # Burp proxy for HTTPS traffic + } + # Fetch user info + self.user = self.get_user_info() + + # Fetch user stats and store them in self.user + user_owns, root_owns, respects = self.get_user_stats(self.user['id']) + self.user['user_owns'] = user_owns + self.user['root_owns'] = root_owns + self.user['respects'] = respects + + def get_user_info(self): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/info', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}") + + # Return the user info as a dictionary + data = response.json().get('info') + return {'id': data['id'], 'name': data['name'], 'email': data['email']} + + def get_user_stats(self, user_id): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/profile/basic/{user_id}', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}") + + # Extract user statistics from the response + data = response.json().get('profile') + user_owns = data['user_owns'] + root_owns = data['system_owns'] + respects = data['respects'] + return user_owns, root_owns, respects + + def get_active_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_retired_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_all_machines(self): + # Combine active and retired machines, ensuring no duplicates + active_machines = self.get_active_machines() + retired_machines = self.get_retired_machines() + + all_machines = active_machines + retired_machines + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + unique_machines = [] + + for machine in all_machines: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + unique_machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + return unique_machines + + def get_all_challenges(self): + challenges = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/challenges?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}") + + data = response.json() + for challenge in data['data']: + if challenge['id'] not in seen_ids and challenge['name'] not in seen_names: + challenges.append(challenge) + seen_ids.add(challenge['id']) + seen_names.add(challenge['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return challenges + + def get_all_sherlocks(self): + sherlocks = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/sherlocks?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}") + + data = response.json() + for sherlock in data['data']: + if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names: + sherlocks.append(sherlock) + seen_ids.add(sherlock['id']) + seen_names.add(sherlock['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return sherlocks \ No newline at end of file diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png new file mode 100755 index 0000000..5bcb448 --- /dev/null +++ b/htb2trilium/images/machine_view.png Binary files differ diff --git a/htb2trilium/images/machines_overview.png b/htb2trilium/images/machines_overview.png new file mode 100755 index 0000000..2d84760 --- /dev/null +++ b/htb2trilium/images/machines_overview.png Binary files differ diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +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_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + 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("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py new file mode 100644 index 0000000..1e32e59 --- /dev/null +++ b/htb2trilium/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +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_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_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 sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['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": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['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=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py new file mode 100644 index 0000000..a35929a --- /dev/null +++ b/htb2trilium/htb_client.py @@ -0,0 +1,214 @@ +import requests +import warnings + +warnings.filterwarnings("ignore") + +class HTBClient: + def __init__(self, password): + self.password = password + self.base_url = 'https://labs.hackthebox.com/api/v4' + self.proxies = { + #"http": "http://127.0.0.1:8080", # Burp proxy for HTTP traffic + #"https": "http://127.0.0.1:8080" # Burp proxy for HTTPS traffic + } + # Fetch user info + self.user = self.get_user_info() + + # Fetch user stats and store them in self.user + user_owns, root_owns, respects = self.get_user_stats(self.user['id']) + self.user['user_owns'] = user_owns + self.user['root_owns'] = root_owns + self.user['respects'] = respects + + def get_user_info(self): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/info', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}") + + # Return the user info as a dictionary + data = response.json().get('info') + return {'id': data['id'], 'name': data['name'], 'email': data['email']} + + def get_user_stats(self, user_id): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/profile/basic/{user_id}', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}") + + # Extract user statistics from the response + data = response.json().get('profile') + user_owns = data['user_owns'] + root_owns = data['system_owns'] + respects = data['respects'] + return user_owns, root_owns, respects + + def get_active_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_retired_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_all_machines(self): + # Combine active and retired machines, ensuring no duplicates + active_machines = self.get_active_machines() + retired_machines = self.get_retired_machines() + + all_machines = active_machines + retired_machines + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + unique_machines = [] + + for machine in all_machines: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + unique_machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + return unique_machines + + def get_all_challenges(self): + challenges = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/challenges?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}") + + data = response.json() + for challenge in data['data']: + if challenge['id'] not in seen_ids and challenge['name'] not in seen_names: + challenges.append(challenge) + seen_ids.add(challenge['id']) + seen_names.add(challenge['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return challenges + + def get_all_sherlocks(self): + sherlocks = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/sherlocks?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}") + + data = response.json() + for sherlock in data['data']: + if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names: + sherlocks.append(sherlock) + seen_ids.add(sherlock['id']) + seen_names.add(sherlock['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return sherlocks \ No newline at end of file diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png new file mode 100755 index 0000000..5bcb448 --- /dev/null +++ b/htb2trilium/images/machine_view.png Binary files differ diff --git a/htb2trilium/images/machines_overview.png b/htb2trilium/images/machines_overview.png new file mode 100755 index 0000000..2d84760 --- /dev/null +++ b/htb2trilium/images/machines_overview.png Binary files differ diff --git a/htb2trilium/images/navigation.png b/htb2trilium/images/navigation.png new file mode 100755 index 0000000..fda8624 --- /dev/null +++ b/htb2trilium/images/navigation.png Binary files differ diff --git a/README.md b/README.md index f623b41..fefccb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,58 @@ TriliumScripts =============== -My collection of scripts for use with trilium \ No newline at end of file +Collection of scripts for use with trilium + +## syntax highlighting + +**highlight_moodlog.js** & **highlight_todo.js** - Both of these will apply appropriate syntaxy highlighting to the specific note. + +### installation + +1) add the code as the notes contents. + +2) set "note type" to "JS frontend" + +3) add an "owned attribute" of "#widget" + +### use + +on the note you want highlighting set the "owned attributes" to "#todotxt #hideHighlightWidget" or "#moodlog #hideHighlightWidget" depending on which syntax type you want to highlight + +## bidirectional sync + +**trilium_sync.php** - script to keep a folder of local text files and a trilium note in sync. + +### installation + +Edit the config values at the top of the script to match your trilium setup. Run it manually to test it is working as expected. If it is create a cron job. + +### example use + +``` +$> php trilium_sync.php +[i] checking connections +[i] version: x.xx.x +[i] gathering info + +- Folders - +Folder/Path Folder Name Parent noteId Mod Trl Mod Dsk Status +Synced/ Synced root j0DRZhIzV97W 1742502936 1742507367 Exists +Synced/BothPlaces/ BothPlaces j0DRZhIzV97W AQb3Dg0yZHSu 1742507075 1742507200 Exists +Synced/[del] tobe del/ [del] tobe del j0DRZhIzV97W j0xUs1FdZL1Y 1742507321 Missing_Disk +Synced/AddToTrilium/ AddToTrilium 1742507383 Missing_Trilium +Synced/tobe deleted/ tobe deleted 1742507200 Missing_Trilium +- Files - +Folder/Path Filename Parent noteId Mod Trl Mod Dsk Hash Status +Synced/BothPlaces/ UpdateDiskFile AQb3Dg0yZHSu IwUZLwWdbiWG 1742507314 1742507199 Diff Update_Disk +Synced/BothPlaces/ UpdateTriliumFile AQb3Dg0yZHSu 7pMPgsb9AVRf 1742507130 1742507345 Diff Update_Trilium +Synced/[del] tobe del/ gone_also.txt j0xUs1FdZL1Y 4wOoHDy07csr 1742507178 N/A Missing_Disk +Synced/AddToTrilium/ fromDiskToTrilium.txt 1742507394 N/A Missing_Trilium +Synced/tobe deleted/ gone_also.txt 1742507200 N/A Missing_Trilium + +[-] deleting disk folder: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/tobe deleted/ +[+] creating trilium folder: Synced/AddToTrilium/ +[+] creating trilium note: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/AddToTrilium/fromDiskToTrilium.txt +[+] updating file on disk: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateDiskFile +[+] updating file on trilium: /mnt/hgfs/PentestOS/Misc/trilium_sync/Synced/BothPlaces/UpdateTriliumFile +``` \ No newline at end of file diff --git a/highlight_moodlog.js b/highlight_moodlog.js new file mode 100644 index 0000000..be4c283 --- /dev/null +++ b/highlight_moodlog.js @@ -0,0 +1,165 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Moodlog Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('moodlog'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + + applyHighlighting(content) { + //console.log("MDLG: Applying highlighting to content..."); + + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]*>/g, ' '); + //console.log("MDLG: Stripped content:", plainTextContent); + + // Combined regex for date & time + const dateTimePattern = /([1-2]\d{3}[-/.](0?[1-9]|1[0-2])[-/.](0[1-9]|[12]\d|3[01]))\s+(\d{1,2}:\d{2})/g; + const projectPattern = /\[\S+?\]/g; + const contextPattern = /(\s|^)(@\S+)/g; + const hashtagPattern = /(\s|^)(#\S+)/g; + const positivePattern = /(\s|^)(\+\S+)/g; + const negativePattern = /(\s|^)(-\S+)/g; + + // Escape HTML characters to prevent issues with replacements + function escapeHtml(str) { + return str.replace(/[&<>"']/g, function (char) { + return { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }[char]; + }); + } + + // Helper function to map the number to a colour based on the gradient + function getProjectColour(number) { + const minColor = { r: 107, g: 109, b: 229 }; // #6b6de5 + const maxColor = { r: 70, g: 226, b: 96 }; // #46e260 + + const ratio = number / 9; // since we are working with 0-9 + + // Interpolate the RGB values between minColor and maxColor + const r = Math.round(minColor.r + ratio * (maxColor.r - minColor.r)); + const g = Math.round(minColor.g + ratio * (maxColor.g - minColor.g)); + const b = Math.round(minColor.b + ratio * (maxColor.b - minColor.b)); + + return `rgb(${r}, ${g}, ${b})`; + } + + // Process each line of the plain text content + let highlightedLines = plainTextContent.split("\n").map(line => { + //console.log("MDLG: Processing line:", line); + + // Apply combined date & time highlight + line = line.replace(dateTimePattern, `<span style="color: rgb(188, 55, 237);">$1</span> <span style="color: rgb(188, 55, 237);">$4</span>`); + + // Apply other highlights + line = line.replace(projectPattern, (match) => { + // Extract the number inside square brackets + const number = parseInt(match.match(/\d+/)[0], 10); + const colour = getProjectColour(number); + return `<span style="color: ${colour}; font-weight: bold;">${escapeHtml(match)}</span>`; + }); + + // Apply other highlights + //line = line.replace(projectPattern, `<span style="color: rgb(224, 218, 36); font-weight: bold;">$&</span>`); + line = line.replace(contextPattern, (_, space, tag) => `${space}<span style="color: rgb(255 165 10);">${escapeHtml(tag)}</span>`); + line = line.replace(hashtagPattern, (_, space, tag) => `${space}<span style="color: rgb(41, 229, 242);">${escapeHtml(tag)}</span>`); + line = line.replace(positivePattern, (_, space, tag) => `${space}<span style="color: rgb(44, 227, 34);">${escapeHtml(tag)}</span>`); + line = line.replace(negativePattern, (_, space, tag) => `${space}<span style="color: rgb(230, 59, 44);">${escapeHtml(tag)}</span>`); + + return line.trim(); + }); + + // Join lines with proper <br> handling + let finalContent = highlightedLines.join("<br>"); + + //console.log("MDLG: Highlighted content:", finalContent); + return finalContent; +} + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("MDLG: Starting Moodlog Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/highlight_todotxt.js b/highlight_todotxt.js new file mode 100644 index 0000000..1f1b007 --- /dev/null +++ b/highlight_todotxt.js @@ -0,0 +1,157 @@ +const TPL = `<div style="padding: 10px; border-top: 1px solid var(--main-border-color); contain: none;"> + <strong>Todo Highlighting Applied</strong> +</div>`; + +class MoodlogHighlighterWidget extends api.NoteContextAwareWidget { + static get parentWidget() { + //console.log("MDLG: Getting parent widget: center-pane"); + return 'center-pane'; + } + + get position() { + //console.log("MDLG: Setting position: 100"); + return 100; + } + + isEnabled() { + const enabled = super.isEnabled() + && this.note.type === 'text' + && this.note.hasLabel('todotxt'); + //console.log(`MDLG: isEnabled: ${enabled}`); + return enabled; + } + + doRender() { + //console.log("MDLG: Rendering widget..."); + this.$widget = $(TPL); + //console.log("MDLG: Widget rendered:", this.$widget); + return this.$widget; + } + + async refreshWithNote(note) { + //console.log("MDLG: Refreshing with note:", note); + const { content } = await note.getNoteComplement(); + //console.log("MDLG: Note content fetched:", content); + + // Check if content is fetched properly + if (!content) { + //console.log("MDLG: No content fetched. Exiting refresh."); + return; + } + + const highlightedContent = this.applyHighlighting(content); + //console.log("MDLG: Highlighted content:", highlightedContent); + + if (highlightedContent !== content) { + //console.log("MDLG: Updating note content with highlighted version"); + + // Use the setNoteContent function to update the note content + try { + await setNoteContent(note.noteId, highlightedContent); + //console.log("MDLG: Content updated with highlighted version"); + } catch (error) { + //console.error("MDLG: Error updating content:", error); + } + } else { + //console.log("MDLG: No changes to content, skipping update."); + } + } + + +applyHighlighting(content) { + // Replace <br> with \n, then remove all other HTML tags + const plainTextContent = content.replace(/<br\s*\/?\>/gi, '\n').replace(/<[^>]*>/g, ' '); + + // Regular expressions for Todo.txt syntax + const datePattern = /\b(0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([1-2]\d{3})\b/g; + const completedTaskPattern = /^\s*x\s+.*/g; + const priorityPattern = /^(\s*\([A-E]\))\s+/; + const projectPattern = /(?:\s|^)(\+\S+)/g; + const contextPattern = /(?:\s|^)(@\S+)/g; + const waitPattern = /\bWAIT\b/gi; + const customAttributePattern = /(\s+[^\s:]+:[^\s:]+)+\s*$/g; + + function escapeHtml(str) { + return str.replace(/[&<>"']/g, char => ({ + '&': '&', '<': '<', '>': '>', + '"': '"', "'": ''' + })[char]); + } + + let highlightedLines = plainTextContent.split("\n").map(line => { + // If line starts with 'x', make the whole line grey + if (/^\s*x\s+/.test(line)) { + return `<span style="color: grey;">${line}</span>`; + } + + // Highlight priority with different colours based on level + line = line.replace(priorityPattern, (match, priority) => { + const level = priority.match(/[A-E]/)?.[0]; + const colors = { + 'A': 'rgb(255, 0, 0)', // Red + 'B': 'rgb(255, 102, 0)', // Orange + 'C': 'rgb(255, 204, 0)', // Yellow + 'D': 'rgb(0, 153, 255)', // Blue + 'E': 'rgb(102, 204, 102)' // Green + }; + const color = colors[level] || 'black'; + return `<span style="color: ${color}; font-weight: bold;">${escapeHtml(priority)}</span> `; + }); + + // Highlight date + line = line.replace(datePattern, (match) => { + return `<span style="color: rgb(188, 55, 237);">${escapeHtml(match)}</span>`; + }); + + // Highlight project tags + line = line.replace(projectPattern, (_, tag) => `<span style="color: rgb(224, 218, 36);"> ${escapeHtml(tag)}</span>`); + + // Highlight context tags + line = line.replace(contextPattern, (_, tag) => `<span style="color: rgb(255, 165, 10);"> ${escapeHtml(tag)}</span>`); + + // Highlight WAIT command + line = line.replace(waitPattern, `<span style="color: rgb(255, 69, 0);">WAIT</span>`); + + // Highlight custom attributes + line = line.replace(customAttributePattern, `<span style="color: rgb(100, 149, 237);">$1</span>`); + + // Remove leading space (if any) from the beginning of the line, without affecting spaces before + or @ + line = line.replace(/^\s+/, ''); + + return line.trim(); + }); + + return highlightedLines.join("<br>"); +} + + + + + + async entitiesReloadedEvent({ loadResults }) { + //console.log("MDLG: Entities reloaded event triggered"); + if (loadResults.isNoteContentReloaded(this.noteId)) { + //console.log("MDLG: Note content reloaded, refreshing widget..."); + //this.refresh(); + } else { + //console.log("MDLG: No content reload detected."); + } + } + + +} + +console.log("TodoHighlight: Starting todotxt Highlighter Widget"); +module.exports = MoodlogHighlighterWidget; + +function setNoteContent(noteId, content) { + /*const noteElement = document.querySelector(`note-detail-readonly-text-content`); + + if (noteElement) { + noteElement.innerHTML = content; // Update the displayed content + console.log(`MDLG: Updated note ${noteId} in the DOM.`); + } else { + console.warn(`MDLG: Note element with data-note-id='${noteId}' not found.`); + }*/ + return api.runOnBackend((id, data) => api.getNote(id).setContent(data), [noteId, content]); +} diff --git a/htb2trilium/INSTALL.md b/htb2trilium/INSTALL.md new file mode 100644 index 0000000..da8afad --- /dev/null +++ b/htb2trilium/INSTALL.md @@ -0,0 +1,41 @@ +## scripts require: + +``` + > pip3 install trilium-py +``` + +## config.json +- get trilium token in trilium options->ETAPI and "create new token" +- create a new note with a title "Machines" +- click on this note -> "note info". Note ID is there. This ID goes in "trilium_machines_htb_folder" in the config file +- set the folder's owned attributes to: + +``` +#label:user=promoted,single,text #label:root=promoted,single,text #label:respect=promoted,single,text #user=0 #root=0 #respect=0 +``` + + - create a note named "HTBMachineTemplate" + - set it's "owned attributes" to: + +``` +#template #label:User=promoted,single,text #label:Root=promoted,single,text #label:Tags=promoted,single,text +``` + +- get this page's ID and put in "trilium_machines_template_id" +- create "challenges" page, this page's ID goes in: trilium_challenges_folder +- create "Sherlocks" page, this page's ID goes in: trilium_sherlocks_folder +- create 2 pages "HTBChallengesTemplate" and "HTBSherlocksTemplate", both of these should have the owned attributes: + +``` +#template #label:Difficulty=promoted,single,text #label:Solved=promoted,single,text #label:Released=promoted,single,text +``` + +- The ID's of "HTBChallengesTemplate" and "HTBSherlocksTemplate" go in the matching config values. + +- to get "in progress" todo colour - add to trilium demo/scripting/taskmanager/implementation/CSS: + +``` + span.fancytree-node.inprogress .fancytree-title { + color: orange !important; + } +``` \ No newline at end of file diff --git a/htb2trilium/README.md b/htb2trilium/README.md new file mode 100644 index 0000000..1b45ae2 --- /dev/null +++ b/htb2trilium/README.md @@ -0,0 +1,16 @@ +htb2trilium +=============== + +Pull hackthebox info and stats into personal trilium note taking software + +single machine view: + + + +overview of machines: + + + +navigation: + + \ No newline at end of file diff --git a/htb2trilium/config.json b/htb2trilium/config.json new file mode 100644 index 0000000..39597ce --- /dev/null +++ b/htb2trilium/config.json @@ -0,0 +1,11 @@ +{ + "htb_code": "in https://app.hackthebox.com/profile/settings click 'profile settings' and under 'app tokens' you can 'create app token' - that goes here", + "trilium_server_url": "https://place.to.your.trilium.server", + "trilium_token": "", + "trilium_machines_htb_folder": "", + "trilium_machines_template_id": "", + "trilium_challenges_folder": "", + "trilium_challenges_template_id": "", + "trilium_sherlocks_folder": "", + "trilium_sherlocks_template_id": "" +} \ No newline at end of file diff --git a/htb2trilium/htb2trilium_challenges.py b/htb2trilium/htb2trilium_challenges.py new file mode 100644 index 0000000..d0d95ca --- /dev/null +++ b/htb2trilium/htb2trilium_challenges.py @@ -0,0 +1,146 @@ +import os +import json +import requests +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") + +# 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'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_challenges_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(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']}") + res2 = ea.search_note( + search=f"{challenge['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == challenge['name'].lower(): + print(f"[i] found ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + #print(f"Search response for challenge '{challenge['name']}': {res2}") + 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))) diff --git a/htb2trilium/htb2trilium_machines.py b/htb2trilium/htb2trilium_machines.py new file mode 100644 index 0000000..de8be53 --- /dev/null +++ b/htb2trilium/htb2trilium_machines.py @@ -0,0 +1,203 @@ +import os +import json +import requests +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_machines_htb_folder = config['trilium_machines_htb_folder'] +trilium_machines_template_id = config['trilium_machines_template_id'] + +def generate_newpage(machine): + no = "<span style=\"color:hsl(0,75%,60%);\">No</span>" + yes = "<span style=\"color:hsl(120,75%,60%);\">Yes</span>" + user_colour = no + if machine['authUserInUserOwns']: + user_colour = yes + root_colour = no + if machine['authUserInRootOwns']: + root_colour = yes + + status = "Retired" + if machine['active']: + status = "Active" + + release_str = machine['release'] + release_date = datetime.strptime(release_str, "%Y-%m-%dT%H:%M:%S.%fZ") + formatted_release_date = release_date.strftime("%d %B %Y") + + html = """ + <figure class="image image-style-align-left image_resized" style="width:12.28%;"> + <img src="https://www.hackthebox.com{avatar}"> + </figure> + <p> + <strong>OS:</strong> {os}<br> + <strong>Difficulty:</strong> {difficultyText} <br> + <strong>Rating:</strong> {rating} / 5<br> + <strong>Points:</strong> {points}<br> + <strong>User / Root: </strong> {user_colour} / {root_colour}<br> + <strong>Released:</strong> {release_date}<br> + <strong>State:</strong> {status} + </p> + <hr> + <h2>Notes</h2> + <p> </p> + """.format( + os=machine['os'], + difficultyText=machine['difficultyText'], + rating=machine['star'], + points=machine['points'], + release_date=formatted_release_date, + user_colour=user_colour, + root_colour=root_colour, + status = status, + avatar = machine['avatar'], + ) + + return html + +def get_timestamp(machine): + # Parse the release date string into a datetime object + release_str = machine['release'] + 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("[i] user owns:", client.user['user_owns'], "| Root owns:", client.user['root_owns'], "| Respect:", client.user['respects']) + +master_folder = ea.get_note(trilium_machines_htb_folder) +for attribute in master_folder['attributes']: + if attribute['name'] == "user": + if attribute['value'] != str(client.user['user_owns']): + print("[+] updating user owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['user_owns'])) + if attribute['name'] == "root": + if attribute['value'] != str(client.user['root_owns']): + print("[+] updating root owns (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['root_owns'])) + if attribute['name'] == "respect": + if attribute['value'] != str(client.user['respects']): + print("[+] updating respect (folder attribute)") + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(client.user['respects'])) + +print("[+] gathering machines info") +machines = client.get_all_machines() +machines.sort(key=get_timestamp) + +print(f"[i] Retrieved {len(machines)} machines") +#for machine in machines: +# print(f" - ID: {machine['id']}, Name: {machine['name']}, OS: {machine['os']}, Difficulty: {machine['difficultyText']}") + +machine_count = 0 +completed_count = 0 +for machine in machines: + machine_count += 1 + print('processing: ',machine_count, "/", len(machines), "("+machine['name']+") " , end='\r') + + res = ea.search_note( + search="\""+machine['name']+"\"", + ancestorNoteId=trilium_machines_htb_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + if res['results'] and machine['name'] == res['results'][0]['title']: + # page exists - lets check if the details have changed + + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + current_paragraph = current_soup.find_all('p')[0].text + + new_html = generate_newpage(machine) + new_soup = BeautifulSoup(new_html, 'html.parser') + new_paragraph = new_soup.find_all('p')[0].text + + # current page contains first paragraph of "blank" (useful for when it doesnt create or find the note properly.. shouldnt get here) + if current_paragraph == "blank": + ea.update_note_content(noteId=res['results'][0]['noteId'], content=new_html) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=res['results'][0]['noteId'], type="label", name="cssClass", value="todo") + # re-get the current content + current_html = ea.get_note_content(noteId=res['results'][0]['noteId']) + current_soup = BeautifulSoup(current_html, 'html.parser') + + if current_paragraph != new_paragraph: + + # details have updated! + print("[+] updating page:",machine['name'], "-> "+res['results'][0]['title']+" ") + replacement = current_soup.find('p') + replacement.replace_with( new_soup.find_all('p')[0] ) + ea.update_note_content(noteId=res['results'][0]['noteId'], content=current_soup) + + # now to update the label + for attribute in res['results'][0]['attributes']: + if attribute['name'] == "cssClass": + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + else: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + + else: + # title does not exist - create the note + html = generate_newpage(machine) + new_note = ea.create_note( + parentNoteId=trilium_machines_htb_folder, + type="text", + title=machine['name'], + content=html, + ) + print("[+] created note:", machine['name'], " ") + + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_machines_htb_folder) + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + else: + if machine['authUserInUserOwns'] or machine['authUserInRootOwns']: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + else: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + + if machine['authUserInUserOwns'] and machine['authUserInRootOwns']: + completed_count += 1 + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_machines_htb_folder,title="Machines - "+str(completed_count)+" / "+str(len(machines))) + +print("[=] processed", machine_count) \ No newline at end of file diff --git a/htb2trilium/htb2trilium_sherlocks.py b/htb2trilium/htb2trilium_sherlocks.py new file mode 100644 index 0000000..1e32e59 --- /dev/null +++ b/htb2trilium/htb2trilium_sherlocks.py @@ -0,0 +1,151 @@ +import os +import json +import requests +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_sherlocks_folder = config['trilium_sherlocks_folder'] +trilium_sherlocks_template_id = config['trilium_sherlocks_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 sherlocks info") +categories = defaultdict(list) +sherlocks = client.get_all_sherlocks() +sherlocks.sort(key=get_timestamp) +total_completed = 0 # Variable to track total completed sherlocks + +print(f"[i] Retrieved {len(sherlocks)} sherlocks") + +# Group sherlocks by their categories +for sherlock in sherlocks: + categories[sherlock['category_name']].append(sherlock) + if sherlock['progress'] == 100: + total_completed += 1 # Increment total completed if challenge is owned + +# Print out the grouped sherlocks with the number of completed and total sherlocks in each category +for category, grouped_sherlocks in categories.items(): + total = len(grouped_sherlocks) + completed = sum(1 for sherlock in grouped_sherlocks if sherlock['progress'] == 100) # Count completed challenges + + res = ea.search_note( + search=f"note.title %= '{category}*'", + ancestorNoteId=trilium_sherlocks_folder, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + + catId = "" + if res['results'] and res['results'][0]['title'].split(' - ')[0].strip().lower() == category.lower(): + # page exists - lets check if the details have changed + ea.patch_note(noteId=res['results'][0]['noteId'], title=category+" - "+str(completed)+" / "+str(total)) + catId = res['results'][0]['noteId'] + print(f"[i] updated category: {category} - ({completed}/{total})") + else: + new_note = ea.create_note( + parentNoteId=trilium_sherlocks_folder, + type="text", + title=category+" - "+str(completed)+" / "+str(total), + content=" ", + ) + catId = new_note['note']['noteId'] + print(f"[+] created category: {catId} {category} - ({completed}/{total})") + + for sherlock in grouped_sherlocks: + #print(f" - ID: {challenge['id']}, Name: {challenge['name']}, Difficulty: {challenge['difficulty']}") + res2 = ea.search_note( + search=f"{sherlock['name']}", + ancestorNoteId=catId, + ancestorDepth='eq1', + limit=1, + fastSearch=True, + ) + # already exists update the values + if res2['results'] and res2['results'][0]['title'].lower() == sherlock['name'].lower(): + print(f"[i] found ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + + for attribute in res2['results'][0]['attributes']: + if attribute['name'] == "Difficulty": + ea.patch_attribute(attributeId=attribute['attributeId'], value=sherlock['difficulty']) + + if attribute['name'] == "Released": + release_str = sherlock['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": + ea.patch_attribute(attributeId=attribute['attributeId'], value=str(sherlock['progress'])) + + if attribute['name'] == "cssClass": + # Determine the cssClass value based on the progress + if sherlock['progress'] == 0: + ea.patch_attribute(attributeId=attribute['attributeId'], value="todo") + elif sherlock['progress'] == 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="done") + elif 0 < sherlock['progress'] < 100: + ea.patch_attribute(attributeId=attribute['attributeId'], value="inprogress") + + + else: # doesnt already exist, create page + release_str = sherlock['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=sherlock['name'], + content=" ", + ) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="relation", name="template", value=trilium_sherlocks_template_id) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Difficulty", value=sherlock['difficulty']) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Released", value=formatted_release_date) + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="Solved", value=str(sherlock['progress'])) + if sherlock['progress'] == 0: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="todo") + elif sherlock['progress'] == 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="done") + elif 0 < sherlock['progress'] < 100: + ea.create_attribute(attributeId=None, isInheritable=False, noteId=new_note['note']['noteId'], type="label", name="cssClass", value="inprogress") + + print(f"[+] created ID: {sherlock['id']}, Name: {sherlock['name']}, Difficulty: {sherlock['difficulty']}") + +print("[+] updating folder name ") +ea.patch_note(noteId=trilium_sherlocks_folder,title="Sherlocks - "+str(total_completed)+" / "+str(len(sherlocks))) diff --git a/htb2trilium/htb_client.py b/htb2trilium/htb_client.py new file mode 100644 index 0000000..a35929a --- /dev/null +++ b/htb2trilium/htb_client.py @@ -0,0 +1,214 @@ +import requests +import warnings + +warnings.filterwarnings("ignore") + +class HTBClient: + def __init__(self, password): + self.password = password + self.base_url = 'https://labs.hackthebox.com/api/v4' + self.proxies = { + #"http": "http://127.0.0.1:8080", # Burp proxy for HTTP traffic + #"https": "http://127.0.0.1:8080" # Burp proxy for HTTPS traffic + } + # Fetch user info + self.user = self.get_user_info() + + # Fetch user stats and store them in self.user + user_owns, root_owns, respects = self.get_user_stats(self.user['id']) + self.user['user_owns'] = user_owns + self.user['root_owns'] = root_owns + self.user['respects'] = respects + + def get_user_info(self): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/info', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching {self.base_url}/user/info user info: {response.status_code}, {response.text}") + + # Return the user info as a dictionary + data = response.json().get('info') + return {'id': data['id'], 'name': data['name'], 'email': data['email']} + + def get_user_stats(self, user_id): + headers = { + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + } + response = requests.get( + f'{self.base_url}/user/profile/basic/{user_id}', + headers=headers, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + if response.status_code != 200: + raise Exception(f"Error fetching user stats: {response.status_code}, {response.text}") + + # Extract user statistics from the response + data = response.json().get('profile') + user_owns = data['user_owns'] + root_owns = data['system_owns'] + respects = data['respects'] + return user_owns, root_owns, respects + + def get_active_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching active machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_retired_machines(self): + machines = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/machine/list/retired/paginated?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching retired machines: {response.status_code}, {response.text}") + + data = response.json() + for machine in data['data']: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return machines + + def get_all_machines(self): + # Combine active and retired machines, ensuring no duplicates + active_machines = self.get_active_machines() + retired_machines = self.get_retired_machines() + + all_machines = active_machines + retired_machines + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + unique_machines = [] + + for machine in all_machines: + if machine['id'] not in seen_ids and machine['name'] not in seen_names: + unique_machines.append(machine) + seen_ids.add(machine['id']) + seen_names.add(machine['name']) + + return unique_machines + + def get_all_challenges(self): + challenges = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/challenges?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching challenges: {response.status_code}, {response.text}") + + data = response.json() + for challenge in data['data']: + if challenge['id'] not in seen_ids and challenge['name'] not in seen_names: + challenges.append(challenge) + seen_ids.add(challenge['id']) + seen_names.add(challenge['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return challenges + + def get_all_sherlocks(self): + sherlocks = [] + seen_ids = set() # Track unique machine IDs + seen_names = set() # Track unique machine names + page = 1 + + while True: + response = requests.get( + f'{self.base_url}/sherlocks?per_page=100&page={page}', + headers={ + "Authorization": f"Bearer {self.password}", + "User-Agent": None # Explicitly remove User-Agent + }, + proxies=self.proxies, + verify=False # Disable SSL verification + ) + + if response.status_code != 200: + raise Exception(f"Error fetching sherlocks: {response.status_code}, {response.text}") + + data = response.json() + for sherlock in data['data']: + if sherlock['id'] not in seen_ids and sherlock['name'] not in seen_names: + sherlocks.append(sherlock) + seen_ids.add(sherlock['id']) + seen_names.add(sherlock['name']) + + # Check for pagination + if page >= data['meta']['last_page']: + break + page += 1 + + return sherlocks \ No newline at end of file diff --git a/htb2trilium/images/machine_view.png b/htb2trilium/images/machine_view.png new file mode 100755 index 0000000..5bcb448 --- /dev/null +++ b/htb2trilium/images/machine_view.png Binary files differ diff --git a/htb2trilium/images/machines_overview.png b/htb2trilium/images/machines_overview.png new file mode 100755 index 0000000..2d84760 --- /dev/null +++ b/htb2trilium/images/machines_overview.png Binary files differ diff --git a/htb2trilium/images/navigation.png b/htb2trilium/images/navigation.png new file mode 100755 index 0000000..fda8624 --- /dev/null +++ b/htb2trilium/images/navigation.png Binary files differ diff --git a/trilium_sync.php b/trilium_sync.php new file mode 100644 index 0000000..9537759 --- /dev/null +++ b/trilium_sync.php @@ -0,0 +1,740 @@ +<?php + +// Configuration +$apiToken = ""; // API Token (menu->options, ETAPI) +$serverUrl = "http://127.0.0.1:8181"; // location to your trilium instance +$parentNoteId = ""; // Change this to the parent note ID you want to list sub-notes for (The main folder/note) +$localFolder = '/NAS/Docs/trilium-data/'; // folder to sync to + +// leave below here +$deletedFolders = array(); +$deletedFiles = array(); + +function getTriliumVersion($apiToken, $serverUrl) { + $url = "$serverUrl/etapi/app-info"; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: $apiToken", + "Content-Type: application/json" + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + die("Error fetching notes: HTTP $httpCode\n$response"); + } + + return json_decode($response, true); +} + +function checkFolderAccess($folderPath) { + if (!is_dir($folderPath) || !is_readable($folderPath)) { + die("Error: Cannot access folder '$folderPath'."); + } +} + +function fetchSubNotes($parentNoteId, $apiToken, $serverUrl) { + $url = "$serverUrl/etapi/notes/$parentNoteId"; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: $apiToken", + "Content-Type: application/json" + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + die("Error fetching notes: HTTP $httpCode\n$response"); + } + + return json_decode($response, true); +} + +function fetchNoteContents($noteId, $apiToken, $serverUrl) { + $url = "$serverUrl/etapi/notes/$noteId/content"; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: $apiToken", + "Content-Type: application/json" + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + return "Error fetching notes: HTTP $httpCode\n$response"; + } + + $response = convertHtmlToText($response); + + return $response; +} + +function updateNoteContents($noteId, $content, $apiToken, $serverUrl) { + $url = "$serverUrl/etapi/notes/$noteId/content"; + + $html = convertTextToHtml($content); + + // Initialise cURL request + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); // Set the request method to PUT + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: $apiToken", + "Content-Type: text/plain" + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, $html); // Add formatted HTML to the body + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 204) { + die("Error updating note: HTTP $httpCode\n$response"); + } + + return; +} + +function createNote($parentNoteId, $title, $content, $apiToken, $serverUrl) { + $url = "$serverUrl/etapi/create-note"; + + $data = json_encode([ + "parentNoteId" => $parentNoteId, + "title" => $title, + "type" => "text", + "content" => $content + ]); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: $apiToken", + "Content-Type: application/json" + ]); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + $response = json_decode($response, true); + return $response['note']; +} + +function deleteNote($noteId, $apiToken, $serverUrl){ + $url = "$serverUrl/etapi/notes/$noteId"; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + "Authorization: $apiToken", + "Content-Type: application/json" + ]); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 204) { + return true; // Deletion successful + } + + return json_decode($response, true); // Return JSON response for errors +} + +function fetchAllNotes($noteId, $apiToken, $serverUrl, $localFolder, $path = '') { + $note = fetchSubNotes($noteId, $apiToken, $serverUrl); + + if (!empty($note['childNoteIds'])) { + $currentPath = $path . $note['title'] . '/'; + + $fullPath = rtrim($localFolder, '/') . '/' . ltrim($path, '/') . '/' . ltrim($note['title'], '/'); + $fullPath = preg_replace('/\/+/', '/', $fullPath); + + if (is_dir($fullPath)) { + $status = 'Exists'; + $modifiedDisk = stat($fullPath)['mtime']; + } else { + $status = 'Missing_Disk'; + $modifiedDisk = ""; + } + + $folder = [ + 'noteId' => $noteId, + 'folder' => $note['title'], + 'dateModifiedTrilium' => convertToUnixTimestamp($note['dateModified']), + 'dateModifiedDisk' => $modifiedDisk, + 'parent' => $note['parentNoteIds'][0], + 'children' => [], + 'status' => $status + ]; + + foreach ($note['childNoteIds'] as $childId) { + $folder['children'][] = fetchAllNotes($childId, $apiToken, $serverUrl, $localFolder, $currentPath); + } + + return $folder; + } else { + + $fullPath = $localFolder . $path . $note['title']; + //echo "does it exist?: $fullPath\n"; + $fileExists = file_exists($fullPath) ? 'Exists' : 'Missing_Disk'; + + if ($fileExists === 'Exists' && is_file($fullPath)) { + $modifiedDisk = convertToUnixTimestamp(filemtime($fullPath)); + $hashDisk = md5(file_get_contents($fullPath)); + } else { + $modifiedDisk = ""; + $hashDisk = ""; + } + + $hashTrilium = md5(fetchNoteContents($noteId, $apiToken, $serverUrl)); + + return [ + 'noteId' => $noteId, + 'file' => $note['title'], + 'path' => $fullPath, + 'dateModifiedTrilium' => convertToUnixTimestamp($note['dateModified']), + 'dateModifiedDisk' => $modifiedDisk, + 'parent' => $note['parentNoteIds'][0], + 'hashTrilium' => $hashTrilium, + 'hashDisk' => $hashDisk, + 'hashMach' => "", + 'status' => $fileExists + ]; + } +} + +function scanLocalFiles($directory, $basePath, $path = '') { + $files = []; + $items = scandir($directory); + + foreach ($items as $item) { + if ($item === '.' || $item === '..') continue; + + $fullPath = rtrim($directory, '/') . '/' . $item; + $relativePath = str_replace($basePath, '', $fullPath); + $currentPath = rtrim($path, '/') . '/' . $item; + + if (is_dir($fullPath)) { + $modifiedDisk = stat($fullPath)['mtime']; + $files[] = [ + 'noteId' => "", + 'folder' => $item, + 'dateModifiedTrilium' => "", + 'dateModifiedDisk' => $modifiedDisk, + 'children' => scanLocalFiles($fullPath, $basePath, $currentPath), // Recursive call for directories + 'parent' => "", + 'status' => 'Missing_Trilium', + ]; + } else { + $hashDisk = md5(file_get_contents($fullPath)); + $files[] = [ + 'noteId' => "", + 'file' => $item, + 'dateModifiedTrilium' => "", + 'dateModifiedDisk' => convertToUnixTimestamp(filemtime($fullPath)), + 'status' => 'Missing_Trilium', + 'parent' => "", + 'hashTrilium' => '', + 'hashDisk' => $hashDisk, + 'hashMach' => "", + 'path' => ltrim($relativePath, '/'), + ]; + } + } + + return $files; +} + +function mergeStructures(&$triliumStructure, $localFiles) { + $triliumPaths = []; + + if (isset($triliumStructure['children'])) { + foreach ($triliumStructure['children'] as &$child) { + $key = isset($child['folder']) ? $child['folder'] : $child['file']; + $triliumPaths[$key] = &$child; + } + } else { + $triliumStructure['children'] = []; + } + + foreach ($localFiles as $localItem) { + $key = isset($localItem['folder']) ? $localItem['folder'] : $localItem['file']; + + if (!isset($triliumPaths[$key])) { + if (isset($localItem['path'])) { + $localItem['path'] = preg_replace('|^[^/]+/|', '', $localItem['path']); + } + $triliumStructure['children'][] = $localItem; + } else if (isset($localItem['children'])) { + mergeStructures($triliumPaths[$key], $localItem['children']); + } + } +} + +function compareFileHashes(&$item) { + // If the item is an array, check if it has children + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as &$child) { + compareFileHashes($child); + } + } + + // Perform hash comparison only if it's a leaf node (has 'file' but no 'children') + if (isset($item['file']) && (!isset($item['children']) || empty($item['children']))) { + if (!empty($item['hashTrilium']) && !empty($item['hashDisk'])) { + if ($item['hashTrilium'] === $item['hashDisk']) { + $item['hashMatch'] = "Match"; + } else { + $item['hashMatch'] = 'Diff'; + if($item['dateModifiedTrilium'] < $item['dateModifiedDisk']){ $item['status'] ="Update_Trilium"; } + if($item['dateModifiedTrilium'] > $item['dateModifiedDisk']){ $item['status'] ="Update_Disk"; } + } + } else { + $item['hashMatch'] = 'N/A'; + } + + } +} + +function createTriliumFolder(&$item, $path = '',$parentNoteId = null) { + global $localFolder, $apiToken, $serverUrl; + global $deletedFolders, $deletedFiles; + // Assign the parent's noteId as the parentId for the current item + + if (isset($item['folder']) && strpos($item['folder'], '[del]') !== 0 ) { + + $startsWithDeleted = false; + foreach ($deletedFolders as $deletedFolder) { + if (strncmp($path . $item['folder'] ."/", $deletedFolder, strlen($deletedFolder)) === 0) { + $startsWithDeleted = true; + break; + } + } + + if (!empty($item['status']) && $item['status'] == "Missing_Trilium" ) { + if(!$startsWithDeleted){ + $fullpath = $path . $item['folder'] . '/'; + echo "[+] creating trilium folder: $fullpath\n"; + + $newId = createNote($parentNoteId, $item['folder'], "", $apiToken, $serverUrl); + // Update item with new noteId + $item['noteId'] = $newId['noteId']; + $item['dateModifiedTrilium'] = time(); + $item['status'] = "Updated"; + } else{ + $item['status'] = "Skipped"; + } + } + } + + // Update parentId for immediate children + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as &$child) { + if (isset($item['noteId']) && !empty($item['noteId'])) { + $child['parent'] = $item['noteId']; + } + $currentPath = $path . $item['folder'] . '/'; + createTriliumFolder($child, $currentPath, $item['noteId']); + } + } +} + +function createTriliumFile(&$item, $path = '',$parentNoteId = null) { + global $localFolder, $apiToken, $serverUrl; + global $deletedFolders, $deletedFiles; + + if (isset($item['file']) && strpos($item['file'], '[del]') !== 0) { + + $startsWithDeleted = false; + foreach ($deletedFolders as $deletedFolder) { + if (strncmp($path , $deletedFolder, strlen($deletedFolder)) === 0) { + $startsWithDeleted = true; + break; + } + } + foreach ($deletedFiles as $deletedFile) { + if (strncmp($path . $item['file'] , $deletedFile, strlen($deletedFile)) === 0) { + $startsWithDeleted = true; + break; + } + } + + if (!empty($item['status']) && $item['status'] == "Missing_Trilium") { + if(!$startsWithDeleted){ + $fullpath = $localFolder . $path . $item['file'] ; + echo "[+] creating trilium note: $fullpath\n"; + + $contents = file_get_contents($fullpath); + $newId = createNote($parentNoteId, $item['file'], $contents, $apiToken, $serverUrl); + // Update item with new noteId + $item['noteId'] = $newId['noteId']; + $item['dateModifiedTrilium'] = time(); + $item['status'] = "Updated"; + }else{ $item['status'] = "Skipped"; } + } + } + + // Update parentId for immediate children + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as &$child) { + if (isset($item['noteId']) && !empty($item['noteId'])) { + $child['parent'] = $item['noteId']; + } + $currentPath = $path . $item['folder'] . '/'; + createTriliumFile($child, $currentPath, $item['noteId']); + } + } +} + +function createDiskFileAndFolder(&$item, $path = '', $parentNoteId = null) { + global $localFolder, $apiToken, $serverUrl; + global $deletedFolders, $deletedFiles; + + // Assign the parent's noteId as the parentId for the current item + if (isset($item['folder']) && strpos($item['folder'], '[del]') !== 0) { + if (!empty($item['status']) && $item['status'] == "Missing_Disk") { + $fullpath = $localFolder . $path . $item['folder'] . '/'; + echo "[+] creating disk folder: $fullpath\n"; + + mkdir($fullpath, 0777, true); + $item['dateModifiedDisk'] = time(); + $item['status'] = "Updated"; + } + } + + if (isset($item['file']) && strpos($item['file'], '[del]') !== 0) { + + $startsWithDeleted = false; + foreach ($deletedFolders as $deletedFolder) { + if (strncmp($path , $deletedFolder, strlen($deletedFolder)) === 0) { + $startsWithDeleted = true; + break; + } + } + foreach ($deletedFiles as $deletedFile) { + if (strncmp($path . $item['file'] , $deletedFile, strlen($deletedFile)) === 0) { + $startsWithDeleted = true; + break; + } + } + + if (!empty($item['status']) && $item['status'] == "Missing_Disk") { + if(!$startsWithDeleted){ + $fullpath = $localFolder . $path . $item['file'] ; + echo "[+] creating disk note: $fullpath\n"; + + file_put_contents($fullpath, fetchNoteContents($item['noteId'], $apiToken, $serverUrl) ); + $item['dateModifiedDisk'] = time(); + $item['status'] = "Updated"; + }else{ $item['status'] = "Skipped"; } + } + } + + // Update parentId for immediate children + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as &$child) { + if (isset($item['noteId']) && !empty($item['noteId'])) { + $child['parent'] = $item['noteId']; + } + $currentPath = $path . $item['folder'] . '/'; + createDiskFileAndFolder($child, $currentPath, $item['noteId']); + } + } +} + +function deleteItem(&$item, $path = '', $parentNoteId = null) { + global $localFolder, $apiToken, $serverUrl; + global $deletedFolders, $deletedFiles; + // Assign the parent's noteId as the parentId for the current item + if (isset($item['folder']) && strpos($item['folder'], '[del]') === 0) { + $item['status'] = "Delete"; + + $item_fixed = trim(substr($item['folder'], 5)); // Remove "[del]" from the start + $fullpath = $localFolder . $path . $item_fixed . '/'; + echo "[-] deleting disk folder: $fullpath\n"; + $deletedFolders[] = $path . $item_fixed . '/'; + $deletedFolders[] = $path . $item['folder'] . '/'; + + deleteFolderAndContents($fullpath); + deleteNote($item['noteId'], $apiToken, $serverUrl); + } + + if (isset($item['file']) && strpos($item['file'], '[del]') === 0) { + $item['status'] = "Delete"; + + $item_fixed = trim(substr($item['file'], 5)); // Remove "[del]" from the start + $fullpath = $localFolder . $path . $item_fixed ; + echo "[-] deleting disk file: $fullpath\n"; + $deletedFiles[] = $path . $item_fixed; + $deletedFiles[] = $path . $item['file']; + + deleteNote($item['noteId'], $apiToken, $serverUrl); + if (file_exists($fullpath)) { + if (is_file($fullpath)) { + unlink($fullpath); + } elseif (is_dir($fullpath)) { + foreach (array_diff(scandir($fullpath), ['.', '..']) as $file) { + unlink("$fullpath") ?: rmdir("$fullpath"); + } + rmdir($fullpath); + } + } + + } + + // Update parentId for immediate children + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as &$child) { + if (isset($item['folder']) && strpos($item['folder'], '[del]') === 0) + $child['status'] == "Delete"; + $currentPath = $path . $item['folder'] . '/'; + deleteItem($child, $currentPath, $item['noteId']); + } + } +} + +function deleteFolderAndContents($folderPath) { + if (!is_dir($folderPath)) { + return false; // Ensure it's a directory + } + + // Scan the directory for all files and subdirectories + $files = array_diff(scandir($folderPath), array('.', '..')); + + foreach ($files as $file) { + $filePath = $folderPath . DIRECTORY_SEPARATOR . $file; + + if (is_dir($filePath)) { + deleteFolderAndContents($filePath); + } else { + + unlink($filePath); + } + } + return rmdir($folderPath); +} + +function updateExistingFile(&$item, $path = '', $parentNoteId = null){ + global $localFolder, $apiToken, $serverUrl; + + if (isset($item['file'])) { + if (!empty($item['status']) && $item['status'] == "Update_Trilium") { + $fullpath = $localFolder . $path . $item['file'] ; + echo "[+] updating file on trilium: $fullpath\n"; + + $content = file_get_contents($fullpath); + updateNoteContents($item['noteId'], $content, $apiToken, $serverUrl); + $item['dateModifiedTrilium'] = time(); + $item['status'] = "Updated"; + } + } + + if (isset($item['file'])) { + if (!empty($item['status']) && $item['status'] == "Update_Disk") { + $fullpath = $localFolder . $path . $item['file'] ; + echo "[+] updating file on disk: $fullpath\n"; + + file_put_contents($fullpath, fetchNoteContents($item['noteId'], $apiToken, $serverUrl) ); + $item['dateModifiedDisk'] = time(); + $item['status'] = "Updated"; + } + } + + // Update parentId for immediate children + if (isset($item['children']) && is_array($item['children'])) { + foreach ($item['children'] as &$child) { + $currentPath = $path . $item['folder'] . '/'; + updateExistingFile($child, $currentPath, $item['noteId']); + } + } +} + +function printFolderTable($structure, $path = '', $printHeader = true) { + // Define column widths to ensure consistent formatting + $columnWidths = [ + 'path' => 25, + 'folder' => 25, + 'parent' => 13, + 'noteId' => 13, + 'dateModifiedTrilium' => 11, + 'dateModifiedDisk' => 11, + 'status' => 15 + ]; + + if ($printHeader) { + echo "- Folders -\n"; + echo formatColumn("Folder/Path", 25) . + formatColumn("Folder Name", 25) . + formatColumn("Parent", 13) . + formatColumn("noteId", 13) . + formatColumn("Mod Trl", 11) . + formatColumn("Mod Dsk", 11) . + formatColumn("Status", 15) . "\n"; + } + + if (isset($structure['folder'])) { + $currentPath = $path . $structure['folder'] . '/'; + + // Print the folder details + echo formatColumn($currentPath, $columnWidths['path']) . + formatColumn($structure['folder'], $columnWidths['folder']) . + formatColumn($structure['parent'], $columnWidths['parent']) . + formatColumn($structure['noteId'], $columnWidths['noteId']) . + formatColumn($structure['dateModifiedTrilium'], $columnWidths['dateModifiedTrilium']) . + formatColumn($structure['dateModifiedDisk'], $columnWidths['dateModifiedDisk']) . + formatColumn($structure['status'], $columnWidths['status']) . "\n"; + + // Recursively process children + foreach ($structure['children'] as $child) { + printFolderTable($child, $currentPath, false); + } + } +} + +function printFileTable($structure, $path = '', $printHeader = true) { + // Define column widths to ensure consistent formatting + $columnWidths = [ + 'path' => 25, + 'parent' => 13, + 'noteId' => 13, + 'file' => 25, + 'dateModifiedTrilium' => 11, + 'dateModifiedDisk' => 11, + 'hashMatch' => 6, + 'status' => 15 + ]; + + if ($printHeader) { + echo "- Files -\n"; + echo formatColumn("Folder/Path", 25) . + formatColumn("Filename", 25) . + formatColumn("Parent", 13) . + formatColumn("noteId", 13) . + formatColumn("Mod Trl", 11) . + formatColumn("Mod Dsk", 11) . + formatColumn("Hash", 6) . + formatColumn("Status", 15) . "\n"; + } + + if (isset($structure['folder'])) { + $currentPath = $path . $structure['folder'] . '/'; + foreach ($structure['children'] as $child) { + printFileTable($child, $currentPath, false); + } + } else { + // Format and print the table row with aligned columns + echo formatColumn($path, $columnWidths['path']) . + formatColumn($structure['file'], $columnWidths['file']) . + formatColumn($structure['parent'], $columnWidths['parent']) . + formatColumn($structure['noteId'], $columnWidths['noteId']) . + formatColumn($structure['dateModifiedTrilium'], $columnWidths['dateModifiedTrilium']) . + formatColumn($structure['dateModifiedDisk'], $columnWidths['dateModifiedDisk']) . + formatColumn($structure['hashMatch'], $columnWidths['hashMatch']) . + formatColumn($structure['status'], $columnWidths['status']) . "\n"; + } +} + +function convertToUnixTimestamp($timestamp) { + // If timestamp is already an integer (Unix timestamp), return it directly + if (is_int($timestamp)) { + return $timestamp; + } + + // For other formats, convert to Unix timestamp + $date = new DateTime($timestamp); + return $date->getTimestamp(); +} + +function convertTextToHtml($text) { + // 1: Plain Text Search + $text = str_replace("&", "&", $text); + $text = str_replace("<", "<", $text); + $text = str_replace(">", ">", $text); + + // 2: Convert Tabs to 4 's + $text = str_replace("\t", " ", $text); + + // 3: Line Breaks + $text = preg_replace("/\r\n?|\n/", "<br>", $text); + + // 4: Paragraphs (Replacing two or more line breaks with paragraph tags) + $text = preg_replace("/<br>\s*<br>/", "</p><p>", $text); + + // 5: Wrap in Paragraph Tags + $text = "<p>" . $text . "</p>"; + + return $text; +} + +function convertHtmlToText($html) { + // Replace <br>, <br/> and <br /> tags with newlines + $html = str_ireplace(["<br>", "<br/>", "<br />"], "\n", $html); + + // Replace <p> and </p> tags with double newlines (to separate paragraphs) + $html = str_ireplace("</p>", "\n\n", $html); + $html = str_ireplace("<p>", "", $html); // Remove opening <p> tags + + // Convert four into a tab (\t) + $html = str_ireplace(" ", "\t", $html); + + // Replace other common HTML tags (like <div>, <span>, etc.) by stripping them + $html = strip_tags($html); + + // Replace HTML entities like &, <, >, , etc. with their corresponding characters + $html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + // Remove excessive spaces and normalise newlines + $html = preg_replace('/[ ]+/', ' ', $html); // Replace multiple spaces or tabs with a single space + $html = trim($html); // Remove leading/trailing whitespace + + return $html; +} + +function formatColumn($str, $width) { + // Function to pad the strings to align the columns + return str_pad($str, $width, ' ', STR_PAD_RIGHT); +} + +echo "[i] checking connections\n"; +$appInfo = getTriliumVersion($apiToken, $serverUrl); +checkFolderAccess($localFolder); +echo "[i] version: ".$appInfo['appVersion']."\n"; + +echo "[i] gathering info\n"; +$structure = fetchAllNotes($parentNoteId, $apiToken, $serverUrl, $localFolder); +$localFiles = scanLocalFiles($localFolder."Synced/", $localFolder."Synced/"); +mergeStructures($structure, $localFiles); +compareFileHashes($structure); + +// Print the data tables +printFolderTable($structure); +printFileTable($structure); + +// syncing operations +deleteItem($structure); +createTriliumFolder($structure); +createTriliumFile($structure); +createDiskFileAndFolder($structure); +updateExistingFile($structure); + +/* +echo "####### DEBUG ##########\n"; +printFolderTable($structure); +printFileTable($structure); +*/