diff --git a/CHANGELOG.md b/CHANGELOG.md index 8814eb0..3de2286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ # Changelog +## [1.7.0](https://github.com/soberhacker/obsidian-telegram-sync/compare/1.6.1...1.7.0) (2023-06-26) + + +### Features + +* add chat and topic template variables ([60437c9](https://github.com/soberhacker/obsidian-telegram-sync/commit/60437c94817c47b6569580c145bacbec3f8d4fa5)) +* add support of downloading files > 20 MB ([57ec22f](https://github.com/soberhacker/obsidian-telegram-sync/commit/57ec22fd1690c52684fc5ff279057f1b8fea4768)) +* add transcribing voice messages to text ([8405f6d](https://github.com/soberhacker/obsidian-telegram-sync/commit/8405f6d3f478aeb1be0cd2f9b8f38d2719958039)) + + +### Bug Fixes + +* caption handling after handling files error ([d0ed63b](https://github.com/soberhacker/obsidian-telegram-sync/commit/d0ed63bef37650763f09fefe23d2a3d2f187492f)) +* false attempt to create a directory structure ([f2a23ad](https://github.com/soberhacker/obsidian-telegram-sync/commit/f2a23adf613d6c37fa31949104c68738be3fcc37)) +* ignoring Obsidian File & Link settings ([531c70f](https://github.com/soberhacker/obsidian-telegram-sync/commit/531c70fcd52621d8104c7f2b8f367bbd825bb932)) +* inconsistent file names and extensions ([190f560](https://github.com/soberhacker/obsidian-telegram-sync/commit/190f560e434546df45741a83486ecf85c33706ea)) +* two bots instances conflict ([19f6bed](https://github.com/soberhacker/obsidian-telegram-sync/commit/19f6bedb5f1d966bc2f190d49fbd88ebeff193e4)) + ## [1.6.1](https://github.com/soberhacker/obsidian-telegram-sync/compare/1.6.0...1.6.1) (2023-06-09) diff --git a/README.md b/README.md index 96a7d8c..d5b413f 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,83 @@ -# Obsidian Telegram Sync Plugin +# Telegram Sync for Obsidian + + + + + + + + +

-This Obsidian plugin allows you to transfer messages and files from a Telegram bot to your Obsidian vault. You can easily save text, images, and other files from your Telegram conversations to Obsidian for further processing and organization. +Transfer messages and files from Telegram to your Obsidian vault. You can easily save text, voice transcripts, images and other files from your Telegram chats to Obsidian for further processing and organization. -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/soberhacker/obsidian-telegram-sync/release.yml?style=shield) ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/soberhacker/obsidian-telegram-sync?display_name=tag) +โ‰๏ธ [Ask a question in Telegram](https://t.me/ObsidianTelegramSync)
+๐Ÿ› [Report issue](https://github.com/soberhacker/obsidian-telegram-sync/issues) + +--- ## Features -- Synchronize text messages and files from a Telegram bot to Obsidian +- Synchronize text messages and files - Save messages as individual notes or append to an existing note +- Transcipt voices and video notes (for Telegram Premium subscibers only) - Use customizable templates for new notes - Set folders for new notes and files - Automatically format text messages with markdown -- Delete processed messages from Telegram (optional) +- Delete processed messages from Telegram -## ~~Installation~~ (on test by Obsidian team) +## Installation -~~1. Open Obsidian and navigate to the Settings.~~ -~~2. Click on the Third-party plugins tab.~~ -~~3. Click the Browse button to open the Community Plugins window.~~ -~~4. Search for Telegram Sync* in the search bar.~~ -~~5. Click the Install button, then enable the plugin by toggling the switch.~~ +1. Open Obsidian and navigate to Settings > Third-party plugin +2. Make sure Safe mode is off +3. Click the Browse button to open the Community plugins window +4. Search for **Telegram Sync** in the search bar +5. Click the Install button, then enable the plugin by toggling the switch ## Manuall Installation -1. This plugin is only for Desktop (Linux, MacOS, Windows). -2. Download main.js, styles.css, manifest.json from the [release page](https://github.com/soberhacker/obsidian-telegram-sync/releases). -3. Copy the downloaded files to YourVaultFolder?/.obsidian/plugins/telegram-sync/. -4. Restart Obsidian and enable Telegram Sync in **Community plugins**. +1. Download main.js, styles.css, manifest.json from the [latest release](https://github.com/soberhacker/obsidian-telegram-sync/releases//latest) +2. Copy the downloaded files to /.obsidian/plugins/telegram-sync/ +3. Restart Obsidian and enable **Telegram Sync** in the Community plugins tab ## Usage -1. Create a new bot on Telegram by talking to the [@botFather](https://t.me/botfather). -2. Copy the bot token provided by the [@botFather](https://t.me/botfather). -3. Open the Obsidian settings and navigate to the Telegram Sync settings tab. -4. Paste your bot token in the **Bot Token** field. -5. Configure the remaining settings according to your preferences. -6. Start sending messages and files to your Telegram bot. -7. When the Obsidian app is running on your lapton or PC, it syncs all your messages. +1. Create a new bot on Telegram by talking to the [@botFather](https://t.me/botfather) +2. Copy the bot token provided by the @botFather +3. Open the Obsidian settings and navigate to the **Telegram Sync** settings tab +4. Paste your bot token to **Bot > Connect > Bot Token** field +5. Configure the remaining settings according to your preferences +6. Start sending messages and files to your Telegram bot +7. When the Obsidian app is running on your lapton or PC, it syncs all your messages +8. You can optionally add your bot to any chats that you want to sync (the bot needs admin rights) ## Tips 1. For syncing your notes between different devices should use Obsidian Sync, Syncthing, ICloud, Google Drive etc. 2. If you use Obsidian only on your cellphone, use "share" to transfer data between mobile applications. -## Support - -If you have any issues or feature requests, please open an issue on the [GitHub](https://github.com/soberhacker/obsidian-telegram-sync). - -If you like this plugin and are considering donating to support continued development, use the buttons below! - -[![Crypto รโŸ naโ‚ฎiโŸ n](https://img.buymeacoffee.com/button-api/?text=Crypto%20Donation&emoji=๐Ÿš€&slug=soberhacker&button_colour=5b5757&font_colour=ffffff&font_family=Lato&outline_colour=ffffff&coffee_colour=FFDD00)](https://oxapay.com/donate/5855474) - -[![Buy me a book](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20book&emoji=๐Ÿ“–&slug=soberhacker&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff)](https://www.buymeacoffee.com/soberhacker) - -[![Ko-fi Donation](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/soberhacker) - -[![PayPal](https://www.paypalobjects.com/webstatic/en_US/i/buttons/PP_logo_h_100x26.png)](https://www.paypal.com/donate/?hosted_button_id=VYSCUZX8MYGCU) - +## Patrons & Supporters + +I want to say thank you to the people who support me, I really appreciate it! + +- maauso +- ekalinin +- knopki +- Fabio Scarsi +- anonymous patrons and contributors + +--- + +
+If you like this plugin and are considering donating to support continued development, use the buttons below!
+ + +  + +
+ + +  + + +
\ No newline at end of file diff --git a/docs/Authorized User Features.md b/docs/Authorized User Features.md new file mode 100644 index 0000000..e9a5796 --- /dev/null +++ b/docs/Authorized User Features.md @@ -0,0 +1,8 @@ +#### Authorized User Features + +โœ… voice and video notes transcription using template variable {{voiceTranscript}} (only for Telegram Premium subscribers) +โŒ reacting with "๐Ÿ‘" instead of replying for marking messages as processed (not implemented) +โŒ getting messages older then 24 hours if obsidian wasn't running (not implemented) +โŒ deleting messages older then 48 hours (not implemented) +โŒ delete messages that were forwarded to the same place where they were and marking original messages as processed (not implemented) +โŒ automatically identifying renamed topic without using command /topicName (not implemented) \ No newline at end of file diff --git a/docs/Message Objects Examples.md b/docs/Message Objects Examples.md new file mode 100644 index 0000000..d1c0298 --- /dev/null +++ b/docs/Message Objects Examples.md @@ -0,0 +1,137 @@ +###### example 1 +```json +{ + "message_id": 802, + "from": { + "id": 1110636370, + "is_bot": false, + "first_name": "soberHacker", + "username": "soberhacker", + "language_code": "en" + }, + "chat": { + "id": 1110636370, + "first_name": "soberHacker", + "username": "soberhacker", + "type": "private" + }, + "date": 1686431489, + "reply_to_message": { + "message_id": 676, + "from": { + "id": 1110636370, + "is_bot": false, + "first_name": "soberHacker", + "username": "soberhacker", + "language_code": "en" + }, + "chat": { + "id": 1110636370, + "first_name": "soberHacker", + "username": "soberhacker", + "type": "private" + }, + "date": 1686087817, + "forward_from": { + "id": 1189295682, + "is_bot": false, + "first_name": "Sober", + "last_name": "Hacker", + "username": "soberHacker" + }, + "forward_date": 1684404891, + "text": "All is good?" + }, + "text": "Yes, I'm ok!" +} +``` +###### example 2 (topic message) +```json +{ + "message_id": 9, + "from": { + "id": 1110636370, + "is_bot": false, + "first_name": "soberHacker", + "username": "soberhacker", + "language_code": "en" + }, + "chat": { + "id": -1001110672472, + "title": "My Notes", + "is_forum": true, + "type": "supergroup" + }, + "date": 1686514218, + "message_thread_id": 3, + "reply_to_message": { + "message_id": 3, + "from": { + "id": 1110636370, + "is_bot": false, + "first_name": "soberHacker", + "username": "soberhacker", + "language_code": "en" + }, + "chat": { + "id": -1001110672472, + "title": "My Notes", + "is_forum": true, + "type": "supergroup" + }, + "date": 1684966126, + "message_thread_id": 3, + "forum_topic_created": { + "name": "Topic name", + "icon_color": 13338331, + "icon_custom_emoji_id": "5312241539987020022" + }, + "is_topic_message": true + }, + "text": "Text texttt txet", + "is_topic_message": true +} +``` +###### example 3 (topic message without topic name) +```json +{ + "message_id": 12, + "from": { + "id": 1110636370, + "is_bot": false, + "first_name": "soberHacker", + "username": "soberhacker", + "language_code": "en" + }, + "chat": { + "id": -1001110672472, + "title": "My Notes", + "is_forum": true, + "type": "supergroup" + }, + "date": 1686514910, + "message_thread_id": 3, + "reply_to_message": { + "message_id": 6, + "from": { + "id": 1110636370, + "is_bot": false, + "first_name": "soberHacker", + "username": "soberhacker", + "language_code": "en" + }, + "chat": { + "id": -1001110672472, + "title": "My Notes", + "is_forum": true, + "type": "supergroup" + }, + "date": 1686514023, + "message_thread_id": 3, + "text": "This is message", + "is_topic_message": true + }, + "text": "No, I'm message", + "is_topic_message": true +} +``` \ No newline at end of file diff --git a/docs/Template Variables List.md b/docs/Template Variables List.md index 0873055..b71815e 100644 --- a/docs/Template Variables List.md +++ b/docs/Template Variables List.md @@ -1,17 +1,31 @@ -#### Note Content Template +#### Note Content Template ###### Variables: ```ts -โŒ{{chat}} - link to the chat (bot / group / channel) (not implemented) -โŒ{{topic}} - topic name (not implemented) -{{userId}} - id of the user who sent the message +{{content}} - forwarded from + file content + message text +{{content:text}} - only message text +{{content:firstLine}} - first line of the message text +{{content:XX}} - XX characters of the message text +{{file}} - file content ![]() +{{file:link}} - link to the file []() +{{voiceTranscript}} - transcribing voice(video notes!) to text (same limits as for Telegram Premium subscribers) +{{voiceTranscript:XX}} - XX symbols of transcribed voices (same limits as for Telegram Premium subscribers) +{{chat}} - link to the chat (bot / group / channel) +{{chatId}} - id of the chat (bot / group / channel) +{{topic}} - link to the topic (if the topic name displays incorrect, set the name manually using bot command "/topicName NAME") +{{topicId}} - head message id representing the topic +{{messageId}} - message id +{{replyMessageId}} - reply message id {{user}} - link to the user who sent the message -{{content:XX}} - message text: full or of specified length -{{forwardFrom}} - link to the initial creator of the message (user / channel) +{{userId}} - id of the user who sent the message +{{forwardFrom}} - link to the forwarded message or its creator (user / channel) {{messageDate:YYYYMMDD}} - date, when the message was sent {{messageTime:HHmmss}} - time, when the message was sent {{creationDate:YYYYMMDD}} - date, when the message was created {{creationTime:HHmmss}} - time, when the message was created +{{url1}} - first url from the message +{{url1:previewYYY}} - first url preview with YYY pixels height (default 250) +{{replace:TEXT=>WITH}} - replace or delete text in resulting note ``` ###### Template example: @@ -24,16 +38,20 @@ Source: {{chat}}-{{forwardFrom}} Created: {{creationDate:YYYY-DD-MM}} {{creationTime:HH:mm:ss}} ``` +- If Note Content Template is unspecified, template by default will be equal {{content}} - All available formats for dates and time you can find in [Monent JS Docs](https://momentjs.com/docs/#/parsing/string-format/) + + #### Note Path Template (โŒ not implemented) ###### Variables: ```json -โŒ{{userId:VALUE}} - only when user id equal VALUE use this path โŒ{{user:VALUE}} - only when user name equal VALUE use this path +โŒ{{userId:VALUE}} - only when user id equal VALUE use this path โŒ{{chat:VALUE}} - only when chat name equal VALUE use this path +โŒ{{chatId:VALUE}} - only when chat id equal VALUE use this path โŒ{{topic:VALUE}} - only when topic name equal VALUE use this path โŒ{{forwardFrom:VALUE}} - only when message creator equal VALUE use this path ``` @@ -46,8 +64,8 @@ myNotes/WorldNews/{{forwardFrom:The Washington Post}}.md myNotes/Telegram.md // Important channels are written in separate folders, other messages - in root folder in separate notes -myNotes/{{chat:Recipies}}/{context:30}.md -myNotes/{{chat:Ideas}}/{{context:firstLine}}.md +myNotes/{{chat:Recipies}}/{content:30}.md +myNotes/{{chat:Ideas}}/{{content:firstLine}}.md myNotes/{{chat:Work}}/{{forwardFrom}}_{{creationDate}}.md myNotes/{{content:20}}_{{messageDate}}_{{messageTime}}.md @@ -61,16 +79,18 @@ myNotes/{{messageDate:YYYY}}/{{messageDate:MM}}/{{messageDate:DD}}/{{messageTime myNotes/{{creationDate:YYYY}}/{{creationDate:MM-DD}}.{{creationTime:HH:mm:ss(SSS)}}.md ``` -- All **Note Content Template Variables** are also available here +- **Note Content Template Variables** are also available here (except for {{file*}}, {{url1*}}, {{replace*}}, {{content}}, {{content:text}}) - Always define note names and finish paths with *".md"* - If a note with such name exists then new data will be always appended to this notes + + #### File Path Template (โŒ not implemented) ###### Variables: ```json -โŒ{{fileType}} - file type identified by Telegram (videos, audios, voices, photos, documents...) +โŒ{{fileType}} - file type identified by Telegram (video, audio, voice, photo, document...) โŒ{{fileExtention}} - file extension (mp3, ogg, docx, png...) โŒ{{fileName}} - unique file name assigned by Telegram (without extension) ``` @@ -82,16 +102,18 @@ myFiles/{{forwardFrom}}_{{fileName}}.{{fileExtension}} myFiles/{{messageDate:YYYY}}/{{fileType}}.{{messageTime:HHmmss}}.{{fileName}}.{{fileExtension}} ``` -- All **Note Path Template Variables** are also available here +- **Note Content Template Variables** are also available here (except for {{file*}}, {{url1*}}, {{replace*}}, {{content}}, {{content:text}}) - Always define file names and finish paths with *".{{fileExtention}}"* - If a file with such name exists then new file will be created with auto-generated unique name + + #### โš  Not Implemented Features โš  Integrating these new features might prove challenging and time-consuming, so your assistance would be much appreciated. You can help by: - Donating to enhance my motivation -- Contributing to the development (branch "develop") +- Contributing to the development (branch "[develop](https://github.com/soberhacker/obsidian-telegram-sync/tree/develop)") [![Crypto รโŸ naโ‚ฎiโŸ n](https://img.buymeacoffee.com/button-api/?text=Crypto%20Donation&emoji=๐Ÿš€&slug=soberhacker&button_colour=5b5757&font_colour=ffffff&font_family=Lato&outline_colour=ffffff&coffee_colour=FFDD00)](https://oxapay.com/donate/5855474) diff --git a/manifest.json b/manifest.json index 433f7ea..d673723 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "telegram-sync", "name": "Telegram Sync", - "version": "1.6.1", + "version": "1.7.0", "minAppVersion": "1.0.0", "description": "Transfer messages and files from Telegram bot to Obsidian.", "author": "soberHacker", diff --git a/package-lock.json b/package-lock.json index 612d02f..47d3516 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,27 @@ { "name": "obsidian-telegram-sync", - "version": "1.6.1", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-telegram-sync", - "version": "1.5.0", + "version": "1.7.0", "license": "GNU Affero General Public License v3.0", "dependencies": { "@popperjs/core": "^2.11.7", + "@types/qrcode": "^1.5.0", "async": "^3.2.4", + "linkify-it": "^4.0.1", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12", "node-telegram-bot-api": "^0.61.0", + "qrcode": "^1.5.3", "telegram": "^2.16.4" }, "devDependencies": { "@types/async": "^3.2.18", + "@types/linkify-it": "^3.0.2", "@types/mime-types": "^2.1.1", "@types/node": "^16.11.6", "@types/node-telegram-bot-api": "^0.61.6", @@ -1024,6 +1028,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", + "dev": true + }, "node_modules/@types/mime-types": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz", @@ -1039,8 +1049,7 @@ "node_modules/@types/node": { "version": "16.18.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz", - "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==", - "dev": true + "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==" }, "node_modules/@types/node-telegram-bot-api": { "version": "0.61.6", @@ -1065,6 +1074,14 @@ "integrity": "sha512-452/1Kp9IdM/oR10AyqAgZOxUt7eLbm+EMJ194L6oarMYdZNiFIFAOJ7IIr0OrZXTySgfHjJezh2oiyk2kc3ag==", "dev": true }, + "node_modules/@types/qrcode": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.0.tgz", + "integrity": "sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/request": { "version": "2.48.8", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", @@ -1372,7 +1389,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1381,7 +1397,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1712,7 +1727,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } @@ -1823,7 +1837,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1834,8 +1847,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-support": { "version": "1.1.3", @@ -2035,7 +2047,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2124,6 +2135,11 @@ "node": ">=0.3.1" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2232,8 +2248,12 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" }, "node_modules/end-of-stream": { "version": "1.4.4", @@ -3080,7 +3100,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -3694,7 +3713,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -4010,6 +4028,14 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/load-json-file": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", @@ -4645,7 +4671,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -4706,7 +4731,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -4770,6 +4794,14 @@ "node": ">=6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4843,6 +4875,132 @@ "teleport": ">=0.2.0" } }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -5201,11 +5359,15 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "node_modules/resolve": { "version": "1.22.3", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", @@ -5344,8 +5506,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -5554,7 +5715,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5610,7 +5770,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5931,6 +6090,11 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -6162,6 +6326,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "node_modules/which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", diff --git a/package.json b/package.json index 06c2982..468bdac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-telegram-sync", - "version": "1.6.1", + "version": "1.7.0", "description": "Transfer messages and files from Telegram bot to Obsidian.", "main": "main.js", "scripts": { @@ -22,6 +22,7 @@ "license": "GNU Affero General Public License v3.0", "devDependencies": { "@types/async": "^3.2.18", + "@types/linkify-it": "^3.0.2", "@types/mime-types": "^2.1.1", "@types/node": "^16.11.6", "@types/node-telegram-bot-api": "^0.61.6", @@ -40,10 +41,13 @@ }, "dependencies": { "@popperjs/core": "^2.11.7", + "@types/qrcode": "^1.5.0", "async": "^3.2.4", + "linkify-it": "^4.0.1", "mime-types": "^2.1.35", "node-machine-id": "^1.1.12", "node-telegram-bot-api": "^0.61.0", + "qrcode": "^1.5.3", "telegram": "^2.16.4" } } diff --git a/release-notes.mjs b/release-notes.mjs index 1b5fca3..fc66e5b 100644 --- a/release-notes.mjs +++ b/release-notes.mjs @@ -1,25 +1,51 @@ -// if "!"" in version code then notify user about latest release -export const pluginVersion = "1.6.1!"; -// change session name when changes in plugin require new client authorization -export const sessionName = "telegram_sync_160"; +export const version = "1.7.0"; +// TODO in github actions creating an archive with 3 main files for easy installing +// TODO Add Demo gif and screenshots to readme.md +// ## Demo +//![](https://raw.githubusercontent.com/vslinko/obsidian-outliner/main/demos/demo1.gif)
+export const showInTelegram = true; export const newFeatures = ` -- add support of downloading files > 20 MB (quite a tough nut to crack ๐Ÿคฏ) -- add skipping command "/start" for keeping empty bots in chat list -- add public Telegram chat for communication +- add note template variables: + {{chat}} - link to the chat (bot / group / channel) + {{chatId}} - id of the chat (bot / group / channel) + {{topic}} - link to the topic (if the topic name displays incorrect, set the name manually using bot command "/topicName NAME") + {{topicId}} - head message id representing the topic + {{messageId}} - message id + {{replyMessageId}} - reply message id + {{url1}} - first url from the message + {{url1:previewYYY}} - first url preview with YYY pixels height (default 250) + {{replace:TEXT=>WITH}} - replace or delete text in resulting note + {{file:link}} - link to the file +- improve behavior of content insertion + {{content}} - forwarded from + file content + message text + {{content:firstLine}} - first line of the message text + {{content:text}} - only message text + {{file}} - only file content ![]() +- add transcribing voices (for Telegram Premium subscribers only) + {{voiceTranscript}} - transcribing to text +* If Note Content Template is unspecified, template by default will be equal {{content}} +* To get full list tap on Template Variables List + `; // - no bugs, no fixes (create issues on github) export const bugFixes = ` -- EISDIR: illegal operation on a directory, read (issue 108) -- 400 Bad Request: file is too big (issue 79) +- missing file captions formatting +- missing inline external links +- problem with nested formattings +- false warnings about two parallel bot connections +- inconsistent names of downloaded files +- ignoring Obsidian File & Link settings `; export const possibleRoadMap = ` -- add new template variables - {{chat}} - link to the chat (bot / group / channel) - {{topic}} - topic name +- add setting Note Path Template to make notes creation more flexible: + * setting any note names + * using conditions for organizing notes by days, topics etc. + For example: + * myNotes/daily/{{messageDate}}.md + * myNotes/{{chat:Recipies}}/{content:30}.md + * myNotes/{{chat:Ideas}}/{{content:firstLine}}.md - send notes (as files) and files from Obsidian to one chat with bot - don't mark messages as processed and don't delete them (sending of errors will remain) -- change replying to liking๐Ÿ‘ when marking a message as processed (needs scanning qr code and entering Telegram password) -- voice recognition for Telegram Premium subscribers (needs scanning qr code and entering Telegram password) You can "like" one of the possible feature in Telegram chat to increase its chances of being implemented. `; @@ -27,10 +53,9 @@ You can "like" one of the possible feature { + this.botUser = this.botUser || (await this.bot?.getMe()); + if (!this.botUser) throw new Error("Can't get access to bot info. Restart the Telegram Sync plugin"); + return this.botUser; + } + // Stop the bot polling async stopTelegramBot() { if (this.bot) { try { await this.bot.stopPolling(); this.bot = undefined; + this.botUser = undefined; } finally { this.botConnected = false; } diff --git a/src/settings/BotSettingsModal.ts b/src/settings/BotSettingsModal.ts new file mode 100644 index 0000000..62b9f5e --- /dev/null +++ b/src/settings/BotSettingsModal.ts @@ -0,0 +1,127 @@ +import { Modal, Setting } from "obsidian"; +import TelegramSyncPlugin from "src/main"; +import { displayAndLog } from "src/utils/logUtils"; + +export class BotSettingsModal extends Modal { + botSetingsDiv: HTMLDivElement; + saved = false; + constructor(public plugin: TelegramSyncPlugin) { + super(plugin.app); + } + + async display() { + this.contentEl.empty(); + this.botSetingsDiv = this.contentEl.createDiv(); + this.botSetingsDiv.createEl("h4", { text: "Bot settings" }); + this.addBotToken(); + this.addAllowedChatFromUsernamesSetting(); + this.addDeviceId(); + this.addFooterButtons(); + } + + addBotToken() { + new Setting(this.botSetingsDiv) + .setName("Bot token (required)") + .setDesc("Enter your Telegram bot token.") + .addText((text) => + text + .setPlaceholder("example: 6123456784:AAX9mXnFE2q9WahQ") + .setValue(this.plugin.settings.botToken) + .onChange(async (value: string) => { + this.plugin.settings.botToken = value; + }) + ); + } + + addAllowedChatFromUsernamesSetting() { + const allowedChatFromUsernamesSetting = new Setting(this.botSetingsDiv) + .setName("Allowed chat from usernames (required)") + .setDesc("Only messages from these usernames will be processed. At least your username must be entered.") + .addTextArea((text) => { + const textArea = text + .setPlaceholder("example: soberHacker,soberHackerBot") + .setValue(this.plugin.settings.allowedChatFromUsernames.join(",")) + .onChange(async (value: string) => { + if (!value.trim()) { + textArea.inputEl.style.borderColor = "red"; + textArea.inputEl.style.borderWidth = "2px"; + textArea.inputEl.style.borderStyle = "solid"; + return; + } + this.plugin.settings.allowedChatFromUsernames = value.split(","); + }); + }); + // add link to Telegram FAQ about getting username + const howDoIGetUsername = document.createElement("div"); + howDoIGetUsername.textContent = "To get help click on -> "; + howDoIGetUsername.createEl("a", { + href: "https://telegram.org/faq?setln=en#q-what-are-usernames-how-do-i-get-one", + text: "Telegram FAQ", + }); + allowedChatFromUsernamesSetting.descEl.appendChild(howDoIGetUsername); + } + + addDeviceId() { + const deviceIdSetting = new Setting(this.botSetingsDiv) + .setName("Main device id") + .setDesc( + "Specify the device to be used for sync when running Obsidian simultaneously on multiple desktops. If not specified, the priority will shift unpredictably." + ) + .addText((text) => + text + .setPlaceholder("example: 98912984-c4e9-5ceb-8000-03882a0485e4") + .setValue(this.plugin.settings.mainDeviceId) + .onChange((value) => (this.plugin.settings.mainDeviceId = value)) + ); + + // current device id copy to settings + const deviceIdLink = deviceIdSetting.descEl.createDiv(); + deviceIdLink.textContent = "To make the current device as main, click on -> "; + deviceIdLink + .createEl("a", { + href: this.plugin.currentDeviceId, + text: this.plugin.currentDeviceId, + }) + .onClickEvent((evt) => { + evt.preventDefault(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let inputDeviceId: any; + try { + inputDeviceId = deviceIdSetting.controlEl.firstElementChild; + inputDeviceId.value = this.plugin.currentDeviceId; + } catch (error) { + displayAndLog(this.plugin, `Try to copy and paste device id manually. Error: ${error}`); + } + if (inputDeviceId && inputDeviceId.value) + this.plugin.settings.mainDeviceId = this.plugin.currentDeviceId; + }); + } + + addFooterButtons() { + const footerButtons = new Setting(this.contentEl.createDiv()); + footerButtons.addButton((b) => { + b.setTooltip("Connect") + .setIcon("checkmark") + .onClick(async () => { + await this.plugin.saveSettings(); + this.saved = true; + this.close(); + }); + return b; + }); + footerButtons.addExtraButton((b) => { + b.setIcon("cross") + .setTooltip("Cancel") + .onClick(async () => { + await this.plugin.loadSettings(); + this.saved = false; + this.close(); + }); + return b; + }); + } + + onOpen() { + this.display(); + } +} diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 9ca00aa..026cfab 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -1,9 +1,19 @@ import TelegramSyncPlugin from "src/main"; -import { PluginSettingTab, Setting, normalizePath } from "obsidian"; +import { ButtonComponent, PluginSettingTab, Setting, TextComponent, normalizePath } from "obsidian"; import { FileSuggest } from "./suggesters/FileSuggester"; import { FolderSuggest } from "./suggesters/FolderSuggester"; -import { displayAndLog } from "src/utils/logUtils"; import { cryptoDonationButton, paypalButton, buyMeACoffeeButton, kofiButton } from "./donation"; +import TelegramBot from "node-telegram-bot-api"; +import { createProgressBar, updateProgressBar, deleteProgressBar } from "src/telegram/progressBar"; +import * as GramJs from "src/telegram/GramJs/client"; +import { BotSettingsModal } from "./BotSettingsModal"; +import { UserLogInModal } from "./UserLogInModal"; + +export interface TopicName { + name: string; + chatId: number; + topicId: number; +} export interface TelegramSyncSettings { botToken: string; @@ -17,7 +27,9 @@ export interface TelegramSyncSettings { pluginVersion: string; appId: string; apiHash: string; - //telegramPassword: string; + topicNames: TopicName[]; + telegramSessionType: GramJs.SessionType; + telegramSessionId: number; } export const DEFAULT_SETTINGS: TelegramSyncSettings = { @@ -32,7 +44,9 @@ export const DEFAULT_SETTINGS: TelegramSyncSettings = { pluginVersion: "", appId: "17349", // public, ok to be here apiHash: "344583e45741c457fe1862106095a5eb", // public, ok to be here - //telegramPassword: "", + topicNames: [], + telegramSessionType: "bot", + telegramSessionId: GramJs.getNewSessionId(), }; export class TelegramSyncSettingTab extends PluginSettingTab { @@ -43,9 +57,9 @@ export class TelegramSyncSettingTab extends PluginSettingTab { display(): void { this.containerEl.empty(); this.addSettingsHeader(); - this.addBotToken(); - this.addAllowedChatFromUsernamesSetting(); - this.addDeviceId(); + + this.addBot(); + this.addUser(); this.containerEl.createEl("h2", { text: "Locations" }); this.addNewNotesLocation(); this.addNewFilesLocation(); @@ -53,11 +67,6 @@ export class TelegramSyncSettingTab extends PluginSettingTab { this.containerEl.createEl("h2", { text: "Behavior settings" }); this.addAppendAllToTelegramMd(); this.addDeleteMessagesFromTelegram(); - // Uncomment only if error API_ID_PUBLISHED_FLOOD - //this.containerEl.createEl("h2", { text: "Client Authorization" }); - //this.addClientAuthorizationDescription(); - //this.addApiId(); - //this.addAppHash(); this.addDonation(); } @@ -69,22 +78,39 @@ export class TelegramSyncSettingTab extends PluginSettingTab { }); } - addBotToken() { - const botFatherSetting = new Setting(this.containerEl) - .setName("Bot token (required)") - .setDesc("Enter your Telegram bot token.") - .addText((text) => - text - .setPlaceholder("example: 6123456784:AAX9mXnFE2q9WahQ") - .setValue(this.plugin.settings.botToken) - .onChange(async (value: string) => { - this.plugin.settings.botToken = value; - await this.plugin.saveSettings(); - this.plugin.initTelegramBot(); // Initialize the bot with the new token - this.plugin.initTelegramClient(); - }) - ); + addBot() { + let botStatusComponent: TextComponent; + + const botStatusConstructor = (botStatus: TextComponent) => { + botStatusComponent = botStatusComponent || botStatus; + botStatus.setDisabled(true); + if (this.plugin.settings.botToken && this.plugin.botConnected) botStatus.setValue("๐Ÿค– connected"); + else botStatus.setValue("โŒ disconnected"); + }; + const botSettingsConstructor = (botSettingsButton: ButtonComponent) => { + if (this.plugin.settings.botToken && this.plugin.botConnected) botSettingsButton.setButtonText("Settings"); + else botSettingsButton.setButtonText("Connect"); + botSettingsButton.onClick(async () => { + const botSettingsModal = new BotSettingsModal(this.plugin); + botSettingsModal.onClose = async () => { + if (botSettingsModal.saved) { + // Initialize the bot with the new token + await this.plugin.initTelegramBot(); + if (this.plugin.settings.telegramSessionType == "bot") + await this.plugin.initTelegramClient(this.plugin.settings.telegramSessionType); + botStatusConstructor.call(this, botStatusComponent); + botSettingsConstructor.call(this, botSettingsButton); + } + }; + botSettingsModal.open(); + }); + }; + const botSettings = new Setting(this.containerEl) + .setName("Bot (required)") + .setDesc("Connect your telegram bot. It's required for all features.") + .addText(botStatusConstructor) + .addButton(botSettingsConstructor); // add link to botFather const botFatherLink = document.createElement("div"); botFatherLink.textContent = "To create a new bot click on -> "; @@ -92,74 +118,54 @@ export class TelegramSyncSettingTab extends PluginSettingTab { href: "https://t.me/botfather", text: "@botFather", }); - botFatherSetting.descEl.appendChild(botFatherLink); + botSettings.descEl.appendChild(botFatherLink); } - addAllowedChatFromUsernamesSetting() { - const allowedChatFromUsernamesSetting = new Setting(this.containerEl) - .setName("Allowed chat from usernames (required)") - .setDesc("Only messages from these usernames will be processed. At least your username must be entered.") - .addTextArea((text) => { - const textArea = text - .setPlaceholder("example: soberHacker,soberHackerBot") - .setValue(this.plugin.settings.allowedChatFromUsernames.join(",")) - .onChange(async (value: string) => { - if (!value.trim()) { - textArea.inputEl.style.borderColor = "red"; - textArea.inputEl.style.borderWidth = "2px"; - textArea.inputEl.style.borderStyle = "solid"; - return; - } - this.plugin.settings.allowedChatFromUsernames = value.split(","); - await this.plugin.saveSettings(); - }); - }); - // add link to Telegram FAQ about getting username - const howDoIGetUsername = document.createElement("div"); - howDoIGetUsername.textContent = "To get help click on -> "; - howDoIGetUsername.createEl("a", { - href: "https://telegram.org/faq?setln=en#q-what-are-usernames-how-do-i-get-one", - text: "Telegram FAQ", - }); - allowedChatFromUsernamesSetting.descEl.appendChild(howDoIGetUsername); - } + addUser() { + let userStatusComponent: TextComponent; - addDeviceId() { - const deviceIdSetting = new Setting(this.containerEl) - .setName("Main device id") - .setDesc( - "Specify the device to be used for sync when running Obsidian simultaneously on multiple desktops. If not specified, the priority will shift unpredictably." - ) - .addText((text) => - text - .setPlaceholder("example: 98912984-c4e9-5ceb-8000-03882a0485e4") - .setValue(this.plugin.settings.mainDeviceId) - .onChange(async (value) => await this.setMainDeviceIdSetting(value)) - ); + const userStatusConstructor = (userStatus: TextComponent) => { + userStatusComponent = userStatusComponent || userStatus; + userStatus.setDisabled(true); + if (this.plugin.settings.telegramSessionType == "user" && this.plugin.userConnected) + userStatus.setValue("๐Ÿ‘จ๐Ÿฝโ€๐Ÿ’ป connected"); + else userStatus.setValue("โŒ disconnected"); + }; - // current device id copy to settings - const deviceIdLink = document.createElement("div"); - deviceIdLink.textContent = "To make the current device as main, click on -> "; - deviceIdLink - .createEl("a", { - href: this.plugin.currentDeviceId, - text: this.plugin.currentDeviceId, - }) - .onClickEvent((evt) => { - evt.preventDefault(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let inputDeviceId: any; - try { - inputDeviceId = deviceIdSetting.controlEl.firstElementChild; - inputDeviceId.value = this.plugin.currentDeviceId; - } catch (error) { - displayAndLog(this.plugin, `Try to copy and paste device id manually. Error: ${error}`); - } - if (inputDeviceId && inputDeviceId.value) { - this.setMainDeviceIdSetting(this.plugin.currentDeviceId); + const userLogInConstructor = (userLogInButton: ButtonComponent) => { + if (this.plugin.settings.telegramSessionType == "user" && this.plugin.userConnected) + userLogInButton.setButtonText("Log out"); + else userLogInButton.setButtonText("Log in"); + + userLogInButton.onClick(async () => { + if (this.plugin.settings.telegramSessionType == "user" && this.plugin.userConnected) { + // Log Out + await this.plugin.initTelegramClient("bot"); + userStatusConstructor.call(this, userStatusComponent); + userLogInConstructor.call(this, userLogInButton); + } else { + // Log In + const userLogInModal = new UserLogInModal(this.plugin); + userLogInModal.onClose = async () => { + userStatusConstructor.call(this, userStatusComponent); + userLogInConstructor.call(this, userLogInButton); + }; + userLogInModal.open(); } }); - deviceIdSetting.descEl.appendChild(deviceIdLink); + }; + + const userSettings = new Setting(this.containerEl) + .setName("User (optionally)") + .setDesc("Connect your telegram user. It's required only for ") + .addText(userStatusConstructor) + .addButton(userLogInConstructor); + + // add link to authorized user features + userSettings.descEl.createEl("a", { + href: "https://github.com/soberhacker/obsidian-telegram-sync/blob/main/docs/Authorized%20User%20Features.md", + text: "a few secondary features", + }); } addNewNotesLocation() { @@ -170,9 +176,9 @@ export class TelegramSyncSettingTab extends PluginSettingTab { new FolderSuggest(cb.inputEl); cb.setPlaceholder("example: folder1/folder2") .setValue(this.plugin.settings.newNotesLocation) - .onChange((newFolder) => { + .onChange(async (newFolder) => { this.plugin.settings.newNotesLocation = newFolder ? normalizePath(newFolder) : newFolder; - this.plugin.saveSettings(); + await this.plugin.saveSettings(); }); }); } @@ -185,9 +191,9 @@ export class TelegramSyncSettingTab extends PluginSettingTab { new FolderSuggest(cb.inputEl); cb.setPlaceholder("example: folder1/folder2") .setValue(this.plugin.settings.newFilesLocation) - .onChange((newFolder) => { + .onChange(async (newFolder) => { this.plugin.settings.newFilesLocation = newFolder ? normalizePath(newFolder) : newFolder; - this.plugin.saveSettings(); + await this.plugin.saveSettings(); }); }); } @@ -200,11 +206,11 @@ export class TelegramSyncSettingTab extends PluginSettingTab { new FileSuggest(cb.inputEl, this.plugin); cb.setPlaceholder("example: folder/zettelkasten.md") .setValue(this.plugin.settings.templateFileLocation) - .onChange((templateFile) => { + .onChange(async (templateFile) => { this.plugin.settings.templateFileLocation = templateFile ? normalizePath(templateFile) : templateFile; - this.plugin.saveSettings(); + await this.plugin.saveSettings(); }); }); // add template available variables @@ -246,80 +252,6 @@ export class TelegramSyncSettingTab extends PluginSettingTab { }); } - // addClientAuthorizationDescription() { - // const clientAuthorizationDescription = new Setting(this.containerEl).setDesc( - // "Entering api_id and api_hash is required for downloading files over 20MB." - // ); - // clientAuthorizationDescription.descEl.createDiv({ text: "To get manual click on -> " }).createEl("a", { - // href: "https://core.telegram.org/api/obtaining_api_id", - // text: "Obtaining api_id", - // }); - // } - - // addApiId() { - // new Setting(this.containerEl) - // .setName("api_id") - // .setDesc("Enter Telegram Client api_id") - // .addText((text) => - // text - // .setPlaceholder("example: 61234") - // .setValue(this.plugin.settings.appId) - // .onChange(async (value: string) => { - // this.plugin.settings.appId = value; - // await this.plugin.saveSettings(); - // this.plugin.initTelegramClient(); - // }) - // ); - // } - - // addAppHash() { - // new Setting(this.containerEl) - // .setName("api_hash") - // .setDesc("Enter Telegram Client api_hash") - // .addText((text) => - // text - // .setPlaceholder("example: asdda623sdk4") - // .setValue(this.plugin.settings.apiHash) - // .onChange(async (value: string) => { - // this.plugin.settings.apiHash = value; - // await this.plugin.saveSettings(); - // this.plugin.initTelegramClient(); - // }) - // ); - // } - - // new Setting(secretSettingsDiv) - // .setName("Telegram Password") - // .setDesc( - // "Enter your password from Telegram. Will not be stored and will be removed after succeeding log in." - // ) - // .addText((text) => - // text - // .setPlaceholder("*********") - // .setValue(this.plugin.settings.telegramPassword) - // .onChange(async (value: string) => { - // this.plugin.settings.telegramPassword = value; - // }) - // ); - - // new Setting(secretSettingsDiv) - // .setName("Log In By Qr Code") - // .setDesc("Scan this Qr Code by official Telegram app on your smartphone. You have 30 sec to do this.") - // .addButton((button) => { - // button.setButtonText("GENERATE QR CODE"); - // button.setDisabled(this.plugin.settings.appId == "" || this.plugin.settings.apiHash == ""); - // button.onClick(async () => { - // const qrCodeContainer: HTMLDivElement = secretSettingsDiv.createDiv({ cls: "qr-code-container" }); - // await gram.initUser( - // +this.plugin.settings.appId, - // this.plugin.settings.apiHash, - // this.plugin.botName, - // this.plugin.settings.telegramPassword, - // qrCodeContainer - // ); - // }); - // }); - addDonation() { this.containerEl.createEl("hr"); @@ -342,9 +274,39 @@ export class TelegramSyncSettingTab extends PluginSettingTab { donationDiv.appendChild(paypalButton); } - async setMainDeviceIdSetting(value: string) { - this.plugin.settings.mainDeviceId = value; - await this.plugin.saveSettings(); - this.plugin.initTelegramBot(); + async storeTopicName(msg: TelegramBot.Message) { + const bot = this.plugin.bot; + if (!bot || !msg.text) return; + + const reply = msg.reply_to_message; + if (msg.message_thread_id || (reply && reply.message_thread_id)) { + const topicName = msg.text.substring(11); + if (!topicName) throw new Error("Set topic name! example: /topicName NewTopicName"); + const newTopicName: TopicName = { + name: topicName, + chatId: msg.chat.id, + topicId: msg.message_thread_id || reply?.message_thread_id || 1, + }; + const topicNameIndex = this.plugin.settings.topicNames.findIndex( + (tn) => tn.topicId == newTopicName.topicId && tn.chatId == newTopicName.chatId + ); + if (topicNameIndex > -1) { + this.plugin.settings.topicNames[topicNameIndex].name = newTopicName.name; + } else this.plugin.settings.topicNames.push(newTopicName); + await this.plugin.saveSettings(); + + const progressBarMessage = await createProgressBar(bot, msg, "stored"); + + // Update the progress bar during the delay + let stage = 0; + for (let i = 1; i <= 10; i++) { + await new Promise((resolve) => setTimeout(resolve, 50)); // 50 ms delay between updates + stage = await updateProgressBar(bot, msg, progressBarMessage, 10, i, stage); + } + await bot.deleteMessage(msg.chat.id, msg.message_id); + await deleteProgressBar(bot, msg, progressBarMessage); + } else { + throw new Error("You can set the topic name only by sending the command to the topic!"); + } } } diff --git a/src/settings/UserLogInModal.ts b/src/settings/UserLogInModal.ts new file mode 100644 index 0000000..462f077 --- /dev/null +++ b/src/settings/UserLogInModal.ts @@ -0,0 +1,133 @@ +import { Modal, Setting } from "obsidian"; +import TelegramSyncPlugin from "src/main"; +import * as GramJs from "src/telegram/GramJs/client"; +import { displayAndLog, displayAndLogError } from "src/utils/logUtils"; + +export class UserLogInModal extends Modal { + botSetingsDiv: HTMLDivElement; + qrCodeContainer: HTMLDivElement; + password = ""; + constructor(public plugin: TelegramSyncPlugin) { + super(plugin.app); + } + + async display() { + this.contentEl.empty(); + this.botSetingsDiv = this.contentEl.createDiv(); + this.botSetingsDiv.createEl("h4", { text: "User authorization" }); + this.addPassword(); + this.addScanner(); + this.addQrCode(); + this.addCheck(); + this.addFooterButtons(); + } + + addPassword() { + new Setting(this.botSetingsDiv) + .setName("1. Enter password (optionally)") + .setDesc( + "Enter your password before scanning QR code only if you use two-step authorization. Password will not be stored" + ) + .addText((text) => { + text.setPlaceholder("*************") + .setValue("") + .onChange(async (value: string) => { + this.password = value; + }); + }); + } + + addScanner() { + new Setting(this.botSetingsDiv) + .setName("2. Prepare QR code scanner") + .setDesc("Open Telegram on your phone. Go to Settings > Devices > Link Desktop Device"); + } + + addQrCode() { + new Setting(this.botSetingsDiv) + .setName("3. Generate & scan QR code") + .setDesc(`Generate QR code and point your phone at it to confirm login`) + .addButton((b) => { + b.setButtonText("Generate QR code"); + b.onClick(async () => { + try { + await this.plugin.initTelegramClient("user"); + await GramJs.signInAsUserWithQrCode(this.qrCodeContainer, this.password); + if (await GramJs.isAuthorizedAsUser()) { + this.plugin.userConnected = true; + displayAndLog(this.plugin, "Successfully logged in"); + } + } catch (e) { + await displayAndLogError(this.plugin, e); + this.plugin.settings.telegramSessionType = "bot"; + await this.plugin.saveSettings(); + } + }); + }); + this.qrCodeContainer = this.botSetingsDiv.createDiv({ + cls: "qr-code-container", + }); + } + + addCheck() { + new Setting(this.botSetingsDiv) + .setName("4. Check active sessions") + .setDesc( + `If the login is successful, you will find the 'Obsidian Telegram Sync' session in the list of active sessions. If you find it in the list of inactive sessions, then you have probably entered the wrong password` + ); + } + addFooterButtons() { + const footerButtons = new Setting(this.contentEl.createDiv()); + footerButtons.addButton((b) => { + b.setIcon("checkmark"); + b.setButtonText("ok"); + b.onClick(async () => this.close()); + }); + } + + onOpen() { + this.display(); + } +} + +// addClientAuthorizationDescription() { +// const clientAuthorizationDescription = new Setting(this.containerEl).setDesc( +// "Entering api_id and api_hash is required for downloading files over 20MB." +// ); +// clientAuthorizationDescription.descEl.createDiv({ text: "To get manual click on -> " }).createEl("a", { +// href: "https://core.telegram.org/api/obtaining_api_id", +// text: "Obtaining api_id", +// }); +// } + +// addApiId() { +// new Setting(this.containerEl) +// .setName("api_id") +// .setDesc("Enter Telegram Client api_id") +// .addText((text) => +// text +// .setPlaceholder("example: 61234") +// .setValue(this.plugin.settings.appId) +// .onChange(async (value: string) => { +// this.plugin.settings.appId = value; +// await this.plugin.saveSettings(); +// this.plugin.initTelegramClient(); +// }) +// ); +// } + +// addAppHash() { +// new Setting(this.containerEl) +// .setName("api_hash") +// .setDesc("Enter Telegram Client api_hash") +// .addText((text) => +// text +// .setPlaceholder("example: asdda623sdk4") +// .setValue(this.plugin.settings.apiHash) +// .onChange(async (value: string) => { +// this.plugin.settings.apiHash = value; +// await this.plugin.saveSettings(); +// this.plugin.initTelegramClient(); +// }) +// ); +// } diff --git a/src/telegram/GramJs/client.ts b/src/telegram/GramJs/client.ts index 8d5d305..8552a06 100644 --- a/src/telegram/GramJs/client.ts +++ b/src/telegram/GramJs/client.ts @@ -1,118 +1,187 @@ import { Api, TelegramClient } from "telegram"; import { StoreSession } from "telegram/sessions"; -// import QRCode from "qrcode"; import { PromisedWebSockets } from "telegram/extensions/PromisedWebSockets"; +import { version } from "release-notes.mjs"; import TelegramBot from "node-telegram-bot-api"; -import { convertBotFileToMessageMedia } from "./convertBotFileToMessageMedia"; -import { pluginVersion, sessionName } from "release-notes.mjs"; +import QRCode from "qrcode"; import os from "os"; +import { convertBotFileToMessageMedia } from "./convertBotFileToMessageMedia"; import { createProgressBar, deleteProgressBar, updateProgressBar } from "../progressBar"; +import { getInputPeerUser, getMessage } from "./convertors"; +import { formatDateTime } from "src/utils/dateUtils"; + +export type SessionType = "bot" | "user"; -let client: TelegramClient; -let botUser: Api.TypeUser | undefined; +let client: TelegramClient | undefined; let _apiId: number; let _apiHash: string; let _botToken: string | undefined; -let inputPeerUser: Api.InputPeerUser; +let _sessionType: SessionType; +let _sessionId: number; +let _clientUser: Api.User | undefined; +let _voiceTranscripts: Map | undefined; + +// change session name when changes in plugin require new client authorization +const sessionName = "telegram_sync_170"; +const NotConnected = new Error("Can't connect to the Telegram Api"); +const NotAuthorized = new Error("Not authorized"); +const NotAuthorizedAsUser = new Error("Not authorized as user. You have to log in as user, not as bot"); + +export function getNewSessionId(): number { + return Number(formatDateTime(new Date(), "YYYYMMDDHHmmssSSS")); +} + +// Stop the bot polling +export async function stop() { + if (client) { + await client.destroy(); + client = undefined; + _botToken = undefined; + _voiceTranscripts = undefined; + } +} -export async function initClient(apiId: number, apiHash: string, deviceId: string) { - stopClient(); - if (_apiId !== apiId || _apiHash !== apiHash) { - const session = new StoreSession(`${sessionName}_${deviceId}`); +// init and connect to Telegram Api +export async function init( + sessionId: number, + sessionType: SessionType, + apiId: number, + apiHash: string, + deviceId: string +) { + if ( + !client || + _apiId !== apiId || + _apiHash !== apiHash || + _sessionType !== sessionType || + _sessionId !== sessionId + ) { + await stop(); + const session = new StoreSession(`${sessionType}_${sessionId}_${sessionName}_${deviceId}`); _apiId = apiId; _apiHash = apiHash; + _sessionId = sessionId; + _sessionType = sessionType; client = new TelegramClient(session, apiId, apiHash, { - connectionRetries: 5, - deviceModel: `Telegram Sync Plugin ${os.type()}`, - appVersion: pluginVersion, + connectionRetries: 2, + deviceModel: `Obsidian Telegram Sync ${os.type().replace("_NT", "")}`, + appVersion: version, useWSS: true, networkSocket: PromisedWebSockets, }); } + if (!client) throw NotConnected; + if (!client.connected) { - await client.connect(); + try { + await client.connect(); + const authorized = await client.checkAuthorization(); + if (sessionType == "user" && authorized && (await client.isBot())) + throw new Error("Stored session conflict. Try to log in again."); + if (!_clientUser && authorized) _clientUser = (await client.getMe()) as Api.User; + } catch (e) { + if (sessionType == "user") { + await init(_sessionId, "bot", apiId, apiHash, deviceId); + throw new Error(`Login as user failed. Error: ${e}`); + } else throw e; + } } } -// TODO add after entering password and sign in button in setting -// npm i qrcode -// npm install --save @types/qrcode -// export async function signInUser(botName?: string, password?: string, container?: HTMLDivElement) { -// if (!(await client.checkAuthorization()) && container) { -// await client.signInUserWithQrCode( -// { apiId: _apiId, apiHash: _apiHash }, -// { -// qrCode: async (qrCode) => { -// const url = "tg://login?token=" + qrCode.token.toString("base64"); -// let qrCodeSvg = await QRCode.toString(url, { type: "svg" }); -// qrCodeSvg = qrCodeSvg.replace(" { -// return password ? password : ""; -// }, -// onError: (error) => { -// container.innerHTML = error.message; -// console.log(error); -// }, -// } -// ); -// } - -// if (await client.checkAuthorization()) { -// const searchResult = await client.invoke( -// new Api.contacts.ResolveUsername({ -// username: botName, -// }) -// ); - -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// const botUser: any = searchResult.users[0]; -// inputPeerUser = new Api.InputPeerUser({ userId: botUser.id, accessHash: botUser.accessHash }); -// } -// } - -export async function signInBot(botToken: string) { - if (!(await client.checkAuthorization()) || _botToken != botToken) { - await client - .signInBot( - { - apiId: _apiId, - apiHash: _apiHash, - }, - { - botAuthToken: botToken, - } - ) - .then(async (bot_user) => { - botUser = bot_user; - _botToken = botToken; - inputPeerUser = new Api.InputPeerUser({ - userId: botUser.id, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - accessHash: (botUser as any).accessHash, - }); - return bot_user; - }) - .catch((e) => { - botUser = undefined; - _botToken = undefined; - throw new Error(e); - }); +export async function isAuthorizedAsUser(): Promise { + return (client && (await client.checkAuthorization()) && !(await client.isBot())) || false; +} + +export async function signInAsBot(botToken: string) { + if (!client) throw NotConnected; + if (await client.checkAuthorization()) { + if (!(await client.isBot())) throw new Error("Bot session is missed"); + if (!_botToken) _botToken = botToken; + if (_botToken == botToken) return; } + await client + .signInBot( + { + apiId: _apiId, + apiHash: _apiHash, + }, + { + botAuthToken: botToken, + } + ) + .then(async (botUser) => { + _botToken = botToken; + _clientUser = botUser as Api.User; + return botUser; + }) + .catch((e) => { + _botToken = undefined; + _clientUser = undefined; + throw new Error(e); + }); +} + +export async function signInAsUserWithQrCode(container: HTMLDivElement, password?: string) { + if (!client) throw NotConnected; + if ((await client.checkAuthorization()) && (await client.isBot())) new Error("User session is missed"); + await client + .signInUserWithQrCode( + { apiId: _apiId, apiHash: _apiHash }, + { + qrCode: async (qrCode) => { + const url = "tg://login?token=" + qrCode.token.toString("base64"); + const qrCodeSvg = await QRCode.toString(url, { type: "svg" }); + const parser = new DOMParser(); + const svg = parser.parseFromString(qrCodeSvg, "image/svg+xml").documentElement; + svg.setAttribute("width", "150"); + svg.setAttribute("height", "150"); + // Removes all children from `container` + while (container.firstChild) { + container.removeChild(container.firstChild); + } + container.appendChild(svg); + }, + password: async (hint) => { + return password ? password : ""; + }, + onError: (error) => { + container.setText(error.message); + console.log(error); + }, + } + ) + .then((clientUser) => { + _clientUser = clientUser as Api.User; + return clientUser; + }) + .catch(() => { + _clientUser = undefined; + }); } // download files > 20MB -export async function downloadMedia(bot: TelegramBot, botMsg: TelegramBot.Message, fileId: string, fileSize: number) { - if (!(await client.checkAuthorization())) { - throw new Error("Not authorized as Bot Client"); +export async function downloadMedia( + bot: TelegramBot, + botMsg: TelegramBot.Message, + fileId: string, + fileSize: number, + botUser?: TelegramBot.User +) { + if (!client) throw NotConnected; + if (!(await client.checkAuthorization())) throw NotAuthorized; + + // user clients needs different file id + let stage = 0; + let message: Api.Message | undefined = undefined; + if (_clientUser && botUser && (await isAuthorizedAsUser())) { + const inputPeerUser = await getInputPeerUser(client, _clientUser, botUser, botMsg); + message = await getMessage(client, inputPeerUser, botMsg); } const progressBarMessage = await createProgressBar(bot, botMsg, "downloading"); - let stage = 0; return await client - .downloadMedia(convertBotFileToMessageMedia(fileId || "", fileSize), { + .downloadMedia(message || convertBotFileToMessageMedia(fileId || "", fileSize), { progressCallback: async (receivedBytes, totalBytes) => { stage = await updateProgressBar( bot, @@ -132,24 +201,60 @@ export async function downloadMedia(bot: TelegramBot, botMsg: TelegramBot.Messag }); } -// TODO integrate after signInUser integration -export async function sendReaction(botMsg: TelegramBot.Message) { - const messages = await client.getMessages(inputPeerUser); - - const clientMsg = messages.find((m) => m.text == botMsg.text); - +export async function sendReaction(botUser: TelegramBot.User, botMsg: TelegramBot.Message) { + if (!client || !(await client.checkAuthorization())) throw NotConnected; + if ((await client.isBot()) || !_clientUser) throw NotAuthorizedAsUser; + const inputPeerUser = await getInputPeerUser(client, _clientUser, botUser, botMsg); + const message = await getMessage(client, inputPeerUser, botMsg); await client.invoke( new Api.messages.SendReaction({ peer: inputPeerUser, - msgId: clientMsg?.id, + msgId: message.id, reaction: [new Api.ReactionEmoji({ emoticon: "๐Ÿ‘" })], }) ); } -// Stop the bot polling -export async function stopClient() { - if (client) { - await client.destroy(); +export async function transcribeAudio( + botMsg: TelegramBot.Message, + botUser?: TelegramBot.User, + mediaId?: number, + limit = 15 // minutes for waiting transcribing (not of the audio) +): Promise { + if (botMsg.text || !(botMsg.voice || botMsg.video_note)) { + return ""; + } + if (!_voiceTranscripts) _voiceTranscripts = new Map(); + if (_voiceTranscripts.size > 100) _voiceTranscripts.clear(); + if (_voiceTranscripts.has(`${botMsg.chat.id}_${botMsg.message_id}`)) + return _voiceTranscripts.get(`${botMsg.chat.id}_${botMsg.message_id}`) || ""; + + if (!client || !(await client.checkAuthorization())) throw NotConnected; + if ((await client.isBot()) || !_clientUser) throw NotAuthorizedAsUser; + if (!_clientUser.premium) { + throw new Error( + "Transcribing voices available only for Telegram Premium subscribers! Remove {{voice:transcript}} from current template or log in with a premium user." + ); + } + if (!botUser) return ""; + const inputPeerUser = await getInputPeerUser(client, _clientUser, botUser, botMsg); + const message = await getMessage(client, inputPeerUser, botMsg, mediaId); + let transcribedAudio: Api.messages.TranscribedAudio | undefined; + // to avoid endless loop, limited waiting + for (let i = 1; i <= limit * 14; i++) { + transcribedAudio = await client.invoke( + new Api.messages.TranscribeAudio({ + peer: inputPeerUser, + msgId: message.id, + }) + ); + if (transcribedAudio.pending) + await new Promise((resolve) => setTimeout(resolve, 5000)); // 5 sec delay between updates + else if (i == limit * 14) throw new Error("Very long audio. Transcribing audio is limited with 15 min."); + else break; } + if (!transcribedAudio) throw new Error("Can't transcribe the audio"); + if (!_voiceTranscripts.has(`${botMsg.chat.id}_${botMsg.message_id}`)) + _voiceTranscripts.set(`${botMsg.chat.id}_${botMsg.message_id}`, transcribedAudio.text); + return transcribedAudio.text; } diff --git a/src/telegram/GramJs/convertBotFileToMessageMedia.ts b/src/telegram/GramJs/convertBotFileToMessageMedia.ts index e0873cf..1f7c953 100644 --- a/src/telegram/GramJs/convertBotFileToMessageMedia.ts +++ b/src/telegram/GramJs/convertBotFileToMessageMedia.ts @@ -1,4 +1,3 @@ -// TODO: PR to GramJs /* eslint-disable @typescript-eslint/no-unused-vars */ import bigInt from "big-integer"; import { Api } from "telegram"; @@ -131,6 +130,36 @@ export function convertBotFileToMessageMedia(fileId: string, fileSize: number): }); } +// converting Telegram Bot Api file_id to Telegram Client Api media object +export function extractMediaId(fileId: string): number { + const decoded = rle_decode(b64_decode(fileId)); + const major = decoded[decoded.length - 1]; + const buffer = major < 4 ? decoded.slice(0, -1) : decoded.slice(0, -2); + + let bufferPosition = 0; + let fileType = buffer.readInt32LE(bufferPosition); + bufferPosition += 4; + buffer.readInt32LE(bufferPosition); + bufferPosition += 4; + + const hasFileReference = Boolean(fileType & FILE_REFERENCE_FLAG); + + fileType &= ~WEB_LOCATION_FLAG; + fileType &= ~FILE_REFERENCE_FLAG; + + if (!(fileType in FileType)) { + throw new Error(`Unknown file_type ${fileType} of file_id ${fileId}`); + } + + if (hasFileReference) { + const { newPosition } = readBytes(buffer, bufferPosition); + bufferPosition = newPosition; + } + + const mediaId = Number(buffer.readBigInt64LE(bufferPosition).toString()); + return mediaId; +} + function b64_decode(s: string): Buffer { const base64Padded = s + "=".repeat(mod(-s.length, 4)); return Buffer.from(base64Padded, "base64"); diff --git a/src/telegram/GramJs/convertors.ts b/src/telegram/GramJs/convertors.ts new file mode 100644 index 0000000..7163dde --- /dev/null +++ b/src/telegram/GramJs/convertors.ts @@ -0,0 +1,68 @@ +import TelegramBot from "node-telegram-bot-api"; +import { Api, TelegramClient } from "telegram"; +import { getFileObject } from "../message/getters"; +import { extractMediaId } from "./convertBotFileToMessageMedia"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getMediaId(media: any): number | undefined { + if (!media) return undefined; + try { + return media.document.id; + } catch { + try { + return media.photo.id; + } catch { + return undefined; + } + } +} + +export async function getInputPeerUser( + client: TelegramClient, + clientUser: Api.User, + botUser: TelegramBot.User, + botMsg: TelegramBot.Message, + limit = 10 +): Promise { + const chatId = botMsg.chat.id == clientUser.id.toJSNumber() ? botUser.id : botMsg.chat.id; + const dialogs = await client.getDialogs({ limit }); + const dialog = dialogs.find((d) => d.id?.toJSNumber() == chatId); + if (!dialog && limit <= 50) return await getInputPeerUser(client, clientUser, botUser, botMsg, limit + 10); + else if (!dialog || !dialog.inputEntity) { + console.log(dialogs); + throw new Error( + `User ${clientUser.username || clientUser.firstName || clientUser.id} does not have chat with ${ + botMsg.chat.username || botMsg.chat.title || botMsg.chat.first_name || botMsg.chat.id + } ` + ); + } + return dialog.inputEntity; +} + +export async function getMessage( + client: TelegramClient, + inputPeerUser: Api.TypeInputPeer, + botMsg: TelegramBot.Message, + mediaId?: number, + limit = 50 +): Promise { + if (!botMsg.text && !mediaId) { + const { fileObject } = getFileObject(botMsg); + const fileObjectToUse = fileObject instanceof Array ? fileObject.pop() : fileObject; + mediaId = extractMediaId(fileObjectToUse.file_id); + } + const messages = await client.getMessages(inputPeerUser, { limit }); + const clientMsg = messages.find( + (m) => (m.message && m.message == botMsg.text) || (!m.message && mediaId && getMediaId(m.media) == mediaId) + ); + if (!clientMsg && limit <= 200) return await getMessage(client, inputPeerUser, botMsg, mediaId, limit + 50); + else if (!clientMsg) { + console.log(messages); + throw new Error( + `Can't find the message using Telegram Client Api. Maybe current connected user doesn't have this chat ${ + botMsg.chat.title || botMsg.chat.username || botMsg.chat.first_name + }` + ); + } + return clientMsg; +} diff --git a/src/telegram/message/convertToMarkdown.ts b/src/telegram/message/convertToMarkdown.ts new file mode 100644 index 0000000..8256935 --- /dev/null +++ b/src/telegram/message/convertToMarkdown.ts @@ -0,0 +1,74 @@ +import TelegramBot from "node-telegram-bot-api"; +import { getInlineUrls } from "./getters"; + +export async function convertMessageTextToMarkdown(msg: TelegramBot.Message): Promise { + let text = msg.text || msg.caption || ""; + const entities = msg.entities || msg.caption_entities || []; + entities.forEach((entity, index, updatedEntities) => { + const entityStart = entity.offset; + let entityEnd = entityStart + entity.length; + let entityText = text.slice(entityStart, entityEnd); + // skip trailing new lines + if (entity.type != "pre") entityEnd = entityEnd - entityText.length + entityText.trimEnd().length; + + const beforeEntity = text.slice(0, entityStart); + entityText = text.slice(entityStart, entityEnd); + const afterEntity = text.slice(entityEnd); + + switch (entity.type) { + case "bold": + entityText = `**${entityText}**`; + updateEntitiesOffset(updatedEntities, entity, index, 2, 2); + break; + case "italic": + entityText = `*${entityText}*`; + updateEntitiesOffset(updatedEntities, entity, index, 1, 1); + break; + case "underline": + entityText = `${entityText}`; + updateEntitiesOffset(updatedEntities, entity, index, 3, 4); + break; + case "strikethrough": + entityText = `~~${entityText}~~`; + updateEntitiesOffset(updatedEntities, entity, index, 2, 2); + break; + case "code": + entityText = "`" + entityText + "`"; + updateEntitiesOffset(updatedEntities, entity, index, 1, 1); + break; + case "pre": + entityText = "```\n" + entityText + "\n```"; + updateEntitiesOffset(updatedEntities, entity, index, 4, 4); + break; + case "text_link": + if (entity.url) { + entityText = `[${entityText}](${entity.url})`; + updateEntitiesOffset(updatedEntities, entity, index, 1, entity.url.length + 3); + } + break; + default: + break; + } + text = beforeEntity + entityText + afterEntity; + }); + const inlineUrls = getInlineUrls(msg); + return text + (inlineUrls ? `\n\n${inlineUrls}` : ""); +} + +function updateEntitiesOffset( + currentEntities: TelegramBot.MessageEntity[], + currentEntity: TelegramBot.MessageEntity, + currentIndex: number, + beforeOffset: number, + afterOffset: number +) { + currentEntities.forEach((entity, index) => { + if (index <= currentIndex) return; + if (entity.offset >= currentEntity.offset) entity.offset += beforeOffset; + if (entity.offset > currentEntity.offset + currentEntity.length) entity.offset += afterOffset; + }); +} + +export function escapeRegExp(str: string) { + return str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); +} diff --git a/src/telegram/message/getters.ts b/src/telegram/message/getters.ts new file mode 100644 index 0000000..25ecc81 --- /dev/null +++ b/src/telegram/message/getters.ts @@ -0,0 +1,141 @@ +import TelegramBot from "node-telegram-bot-api"; +import LinkifyIt from "linkify-it"; +import TelegramSyncPlugin from "src/main"; + +export const fileTypes = ["photo", "video", "voice", "document", "audio", "video_note"]; + +export function getForwardFromLink(msg: TelegramBot.Message): string { + let forwardFromLink = ""; + + if (msg.forward_from || msg.forward_from_chat || msg.forward_sender_name || msg.from) { + let username = ""; + let title = ""; + + if (msg.forward_from) { + username = msg.forward_from.username || "no_username_" + msg.forward_from.id.toString().slice(4); + title = msg.forward_from.first_name + (msg.forward_from.last_name ? " " + msg.forward_from.last_name : ""); + } else if (msg.forward_from_chat) { + username = + (msg.forward_from_chat.username || "c/" + msg.forward_from_chat.id.toString().slice(4)) + + "/" + + (msg.forward_from_message_id || "999999999"); + title = + msg.forward_from_chat.title + (msg.forward_signature ? `(${msg.forward_signature})` : "") || + msg.forward_from_chat.username || + ""; + } else if (msg.forward_sender_name) { + username = "hidden_account_" + msg.forward_date; + title = msg.forward_sender_name; + } else if (msg.from) { + username = msg.from.username || "no_username_" + msg.from.id.toString().slice(4); + title = msg.from.first_name + (msg.from.last_name ? " " + msg.from.last_name : ""); + } + forwardFromLink = forwardFromLink || `[${title}](https://t.me/${username})`; + } + + return forwardFromLink; +} + +export function getUserLink(msg: TelegramBot.Message): string { + if (!msg.from) return ""; + + const username = msg.from.username || "no_username_" + msg.from.id.toString().slice(4); + const title = msg.from.first_name + (msg.from.last_name ? " " + msg.from.last_name : ""); + return `[${title}](https://t.me/${username})`; +} + +export function getChatTitle(msg: TelegramBot.Message): string { + let title = ""; + if (msg.chat.type == "private") { + title = msg.chat.first_name + (msg.chat.last_name ? " " + msg.chat.last_name : ""); + } else { + title = msg.chat.title || msg.chat.type + msg.chat.id; + } + return title; +} + +export function getChatLink(msg: TelegramBot.Message): string { + let username = ""; + if (msg.chat.type == "private") { + username = msg.chat.username || "no_username_" + msg.chat.id.toString().slice(4); + } else { + username = + msg.chat.username || + `c/${msg.chat.id.toString().slice(4)}/${msg.message_thread_id || msg.reply_to_message?.message_thread_id}/${ + msg.message_id + }`; + username = username.replace(/\/\//g, "/"); // because message_thread_id can be undefined + } + const title = getChatTitle(msg); + const chatLink = `[${title}](https://t.me/${username})`; + return chatLink; +} + +export function getUrl(msg: TelegramBot.Message, num = 1, lookInCaptions = true): string { + const text = (msg.text || "") + (lookInCaptions && msg.caption ? msg.caption : ""); + if (!text) return ""; + + const linkify = LinkifyIt(); + const matches = linkify.match(text); + return matches ? matches[num - 1].url : ""; +} + +export function getInlineUrls(msg: TelegramBot.Message): string { + let urls = ""; + if (!msg.reply_markup?.inline_keyboard || msg.reply_markup.inline_keyboard.length == 0) return ""; + msg.reply_markup.inline_keyboard.forEach((buttonsGroup) => { + buttonsGroup.forEach((button) => { + if (button.url) { + urls += `[${button.text}](${button.url})\n`; + } + }); + }); + return urls.trimEnd(); +} + +export async function getTopicLink(plugin: TelegramSyncPlugin, msg: TelegramBot.Message): Promise { + if (!msg.chat.is_forum) return ""; + + const reply = msg.reply_to_message; + let topicName = plugin.settings.topicNames.find( + (tn) => tn.chatId == msg.chat.id && tn.topicId == (msg.message_thread_id || reply?.message_thread_id || 1) + ); + if (!topicName && reply?.forum_topic_created?.name) { + topicName = { + name: reply?.forum_topic_created?.name, + chatId: msg.chat.id, + topicId: msg.message_thread_id || reply.message_thread_id || 1, + }; + plugin.settings.topicNames.push(topicName); + await plugin.saveSettings(); + } + if (!topicName) { + throw new Error( + `Telegram bot has a limitation to get topic names. if the topic name displays incorrect, set the name manually using bot command "/topicName NAME"` + ); + } + + const title = topicName.name; + const path = (msg.chat.username || `c/${topicName.chatId.toString().slice(4)}`) + `/${topicName.topicId}`; + return `[${title}](https://t.me/${path})`; +} + +export function getReplyMessageId(msg: TelegramBot.Message): string { + const reply = msg.reply_to_message; + if (reply && reply.message_thread_id != reply.message_id) { + return reply.message_id.toString(); + } + return ""; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getFileObject(msg: TelegramBot.Message): { fileType?: string; fileObject?: any } { + for (const fileType of fileTypes) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((msg as any)[fileType]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { fileType: fileType, fileObject: (msg as any)[fileType] }; + } + } + return {}; +} diff --git a/src/telegram/messageHandlers.ts b/src/telegram/message/handlers.ts similarity index 57% rename from src/telegram/messageHandlers.ts rename to src/telegram/message/handlers.ts index ae44319..2371f9b 100644 --- a/src/telegram/messageHandlers.ts +++ b/src/telegram/message/handlers.ts @@ -1,23 +1,43 @@ import { TFile, normalizePath } from "obsidian"; -import TelegramSyncPlugin from "../main"; +import TelegramSyncPlugin from "../../main"; import TelegramBot from "node-telegram-bot-api"; -import { sanitizeFileName, getFileObject } from "./utils"; import { date2DateString, date2TimeString } from "src/utils/dateUtils"; -import { createFolderIfNotExist } from "src/utils/fsUtils"; -import { bugFixes, newFeatures, pluginVersion, possibleRoadMap } from "../../release-notes.mjs"; -import { buyMeACoffeeLink, cryptoDonationLink, kofiLink, paypalLink } from "../settings/donation.js"; +import { createFolderIfNotExist, sanitizeFileName } from "src/utils/fsUtils"; +import * as release from "../../../release-notes.mjs"; +import { buyMeACoffeeLink, cryptoDonationLink, kofiLink, paypalLink } from "../../settings/donation"; import { SendMessageOptions } from "node-telegram-bot-api"; import path from "path"; -import * as gram from "./GramJs/client"; +import * as GramJs from "../GramJs/client"; import { extension } from "mime-types"; -import { applyTemplate, finalizeMessageProcessing } from "./messageProcessors"; -import { createProgressBar, deleteProgressBar, updateProgressBar } from "./progressBar"; +import { applyNoteContentTemplate, finalizeMessageProcessing } from "./processors"; +import { createProgressBar, deleteProgressBar, updateProgressBar } from "../progressBar"; +import { getFileObject } from "./getters"; // handle all messages from Telegram export async function handleMessage(plugin: TelegramSyncPlugin, msg: TelegramBot.Message) { let formattedContent = ""; - if (!msg.text || msg.text == "") { + // save topic name and skip handling other data + if (msg.forum_topic_created || msg.forum_topic_edited) { + const topicName = { + name: msg.forum_topic_created?.name || msg.forum_topic_edited?.name || "", + chatId: msg.chat.id, + topicId: msg.message_thread_id || 1, + }; + const topicNameIndex = plugin.settings.topicNames.findIndex( + (tn) => tn.chatId == msg.chat.id && tn.topicId == msg.message_thread_id + ); + if (topicNameIndex == -1) { + plugin.settings.topicNames.push(topicName); + await plugin.saveSettings(); + } else if (plugin.settings.topicNames[topicNameIndex].name != topicName.name) { + plugin.settings.topicNames[topicNameIndex].name = topicName.name; + await plugin.saveSettings(); + } + return; + } + + if (!msg.text) { await handleFiles(plugin, msg); return; } @@ -43,7 +63,7 @@ export async function handleMessage(plugin: TelegramSyncPlugin, msg: TelegramBot const messageDateString = date2DateString(messageDate); const messageTimeString = date2TimeString(messageDate); - formattedContent = await applyTemplate(plugin, plugin.settings.templateFileLocation, msg); + formattedContent = await applyNoteContentTemplate(plugin, plugin.settings.templateFileLocation, msg); const appendAllToTelegramMd = plugin.settings.appendAllToTelegramMd; @@ -51,7 +71,7 @@ export async function handleMessage(plugin: TelegramSyncPlugin, msg: TelegramBot plugin.messageQueueToTelegramMd.push({ msg, formattedContent }); return; } else { - const title = sanitizeFileName(rawText.slice(0, 20)); + const title = sanitizeFileName(rawText.slice(0, 30)); let fileName = `${title} - ${messageDateString}${messageTimeString}.md`; let notePath = normalizePath(location ? `${location}/${fileName}` : fileName); while ( @@ -74,7 +94,9 @@ export async function handleFiles(plugin: TelegramSyncPlugin, msg: TelegramBot.M const basePath = plugin.settings.newFilesLocation || plugin.settings.newNotesLocation || ""; await createFolderIfNotExist(plugin.app.vault, basePath); - let filePath: string | undefined; + let filePath = ""; + let markdownLink = ""; + let telegramFileName = ""; // eslint-disable-next-line @typescript-eslint/no-explicit-any let error: any; @@ -85,13 +107,17 @@ export async function handleFiles(plugin: TelegramSyncPlugin, msg: TelegramBot.M try { // Iterate through each file type const { fileType, fileObject } = getFileObject(msg); + if (!fileType || !fileObject) { + throw new Error("Can't get file object from the message!"); + } const fileObjectToUse = fileObject instanceof Array ? fileObject.pop() : fileObject; const fileId = fileObjectToUse.file_id; + telegramFileName = ("file_name" in fileObjectToUse && fileObjectToUse.file_name) || ""; let fileByteArray: Uint8Array; - let telegramFileName: string; try { const fileLink = await plugin.bot.getFileLink(fileId); - telegramFileName = fileLink?.split("/").pop() || ""; + telegramFileName = + telegramFileName || fileLink?.split("/").pop()?.replace(/file/, `${fileType}_${msg.chat.id}`) || ""; const fileStream = plugin.bot.getFileStream(fileId); const fileChunks: Uint8Array[] = []; @@ -120,11 +146,15 @@ export async function handleFiles(plugin: TelegramSyncPlugin, msg: TelegramBot.M ); } catch (e) { if (e.message == "ETELEGRAM: 400 Bad Request: file is too big") { - const media = await gram.downloadMedia(plugin.bot, msg, fileId, fileObjectToUse.file_size); - //const fileBuffer = media instanceof Buffer ? media : Buffer.alloc(0); - //fileByteArray = new Uint8Array(fileBuffer.buffer, fileBuffer.byteOffset, fileBuffer.byteLength); + const media = await GramJs.downloadMedia( + plugin.bot, + msg, + fileId, + fileObjectToUse.file_size, + plugin.botUser + ); fileByteArray = media instanceof Buffer ? media : Buffer.alloc(0); - telegramFileName = `file_${sanitizeFileName(fileObject.file_unique_id)}`; + telegramFileName = telegramFileName || `${fileType}_${msg.chat.id}_${msg.message_id}`; } else { throw e; } @@ -140,40 +170,42 @@ export async function handleFiles(plugin: TelegramSyncPlugin, msg: TelegramBot.M const fileFullName = `${fileName} - ${messageDateString}${messageTimeString}${fileExtension}`; filePath = `${specificFolder}/${fileFullName}`; - await plugin.app.vault.createBinary(filePath, fileByteArray); + const file = await plugin.app.vault.createBinary(filePath, fileByteArray); + markdownLink = plugin.app.fileManager.generateMarkdownLink(file, filePath); } catch (e) { error = e; } - // Handle message captions and append to Telegram.md if necessary - if ((msg.caption && !(msg.caption === "")) || plugin.settings.appendAllToTelegramMd) { - const captionMd = !error - ? `![](${filePath?.replace(/\s/g, "%20")})\n${msg.caption || ""}` - : `[โŒ error while handling file](${error})\n${msg.caption || ""}`; - - const formattedContent = await applyTemplate(plugin, plugin.settings.templateFileLocation, msg, captionMd); - if (plugin.settings.appendAllToTelegramMd) { - plugin.messageQueueToTelegramMd.push({ msg, formattedContent, error }); - return; - } else if (msg.caption) { - // Save caption as a separate note - const noteLocation = plugin.settings.newNotesLocation || ""; - await createFolderIfNotExist(plugin.app.vault, noteLocation); - const title = sanitizeFileName(msg.caption.slice(0, 20)); - let fileCaptionName = `${title} - ${messageDateString}${messageTimeString}.md`; - let notePath = normalizePath(noteLocation ? `${noteLocation}/${fileCaptionName}` : fileCaptionName); - - while ( - plugin.listOfNotePaths.includes(notePath) || - plugin.app.vault.getAbstractFileByPath(notePath) instanceof TFile - ) { - const newMessageTimeString = date2TimeString(messageDate); - fileCaptionName = `${title} - ${messageDateString}${newMessageTimeString}.md`; - notePath = normalizePath(noteLocation ? `${noteLocation}/${fileCaptionName}` : fileCaptionName); - } - plugin.listOfNotePaths.push(notePath); - await plugin.app.vault.create(notePath, formattedContent); + // exit if only file is needed + if (!plugin.settings.appendAllToTelegramMd && !plugin.settings.templateFileLocation) { + await finalizeMessageProcessing(plugin, msg, error); + return; + } + + const fileLink = !error ? markdownLink : `[โŒ error while handling file](${error})`; + + const noteContent = await applyNoteContentTemplate(plugin, plugin.settings.templateFileLocation, msg, fileLink); + if (plugin.settings.appendAllToTelegramMd) { + plugin.messageQueueToTelegramMd.push({ msg, formattedContent: noteContent, error }); + return; + } else if (msg.caption || telegramFileName) { + // Save caption as a separate note + const noteLocation = plugin.settings.newNotesLocation || ""; + await createFolderIfNotExist(plugin.app.vault, noteLocation); + const title = sanitizeFileName((msg.caption || telegramFileName).slice(0, 30)); + let noteFileName = `${title} - ${messageDateString}${messageTimeString}.md`; + let notePath = normalizePath(noteLocation ? `${noteLocation}/${noteFileName}` : noteFileName); + + while ( + plugin.listOfNotePaths.includes(notePath) || + plugin.app.vault.getAbstractFileByPath(notePath) instanceof TFile + ) { + const newMessageTimeString = date2TimeString(messageDate); + noteFileName = `${title} - ${messageDateString}${newMessageTimeString}.md`; + notePath = normalizePath(noteLocation ? `${noteLocation}/${noteFileName}` : noteFileName); } + plugin.listOfNotePaths.push(notePath); + await plugin.app.vault.create(notePath, noteContent); } await finalizeMessageProcessing(plugin, msg, error); @@ -181,19 +213,13 @@ export async function handleFiles(plugin: TelegramSyncPlugin, msg: TelegramBot.M // show changes about new release export async function ifNewRelaseThenShowChanges(plugin: TelegramSyncPlugin, msg: TelegramBot.Message) { - const pluginVersionCode = pluginVersion.replace(/!/g, ""); - if ( - plugin.settings.pluginVersion && - plugin.settings.pluginVersion !== pluginVersionCode && - // warn user only when "!" sign is in pluginVersion - pluginVersionCode != pluginVersion - ) { - plugin.settings.pluginVersion = pluginVersionCode; - plugin.saveSettings(); - const announcing = `Telegrm Sync ${pluginVersionCode}\n\n`; - const newFeatures_ = `New Features${newFeatures}\n`; - const bugsFixes_ = `Bug Fixes${bugFixes}\n`; - const possibleRoadMap_ = `Possible Road Map${possibleRoadMap}\n`; + if (plugin.settings.pluginVersion && plugin.settings.pluginVersion !== release.version && release.showInTelegram) { + plugin.settings.pluginVersion = release.version; + await plugin.saveSettings(); + const announcing = `Telegrm Sync ${release.version}\n\n`; + const newFeatures_ = `New Features${release.newFeatures}\n`; + const bugsFixes_ = `Bug Fixes${release.bugFixes}\n`; + const possibleRoadMap_ = `Possible Road Map${release.possibleRoadMap}\n`; const donation = "If you like this plugin and are considering donating to support continued development, use the buttons below!"; const releaseNotes = announcing + newFeatures_ + bugsFixes_ + possibleRoadMap_ + donation; @@ -216,7 +242,7 @@ export async function ifNewRelaseThenShowChanges(plugin: TelegramSyncPlugin, msg await plugin.bot?.sendMessage(msg.chat.id, releaseNotes, options); } else if (!plugin.settings.pluginVersion) { - plugin.settings.pluginVersion = pluginVersionCode; - plugin.saveSettings(); + plugin.settings.pluginVersion = release.version; + await plugin.saveSettings(); } } diff --git a/src/telegram/message/processors.ts b/src/telegram/message/processors.ts new file mode 100644 index 0000000..d8a2833 --- /dev/null +++ b/src/telegram/message/processors.ts @@ -0,0 +1,192 @@ +import TelegramBot from "node-telegram-bot-api"; +import TelegramSyncPlugin from "../../main"; +import { getChatLink, getForwardFromLink, getReplyMessageId, getTopicLink, getUrl, getUserLink } from "./getters"; +import { createFolderIfNotExist } from "src/utils/fsUtils"; +import { TFile, normalizePath } from "obsidian"; +import { formatDateTime } from "../../utils/dateUtils"; +import { displayAndLog, displayAndLogError } from "src/utils/logUtils"; +import { createProgressBar, deleteProgressBar, updateProgressBar } from "../progressBar"; +import { escapeRegExp, convertMessageTextToMarkdown } from "./convertToMarkdown"; +import * as GramJs from "../GramJs/client"; + +// Delete a message or send a confirmation reply based on settings and message age +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function finalizeMessageProcessing(plugin: TelegramSyncPlugin, msg: TelegramBot.Message, error?: any) { + if (error) await displayAndLogError(plugin, error, msg); + if (error || !plugin.bot) { + return; + } + + const currentTime = new Date(); + const messageTime = new Date(msg.date * 1000); + const timeDifference = currentTime.getTime() - messageTime.getTime(); + const hoursDifference = timeDifference / (1000 * 60 * 60); + + if (plugin.settings.deleteMessagesFromTelegram && hoursDifference <= 48) { + // Send the initial progress bar + const progressBarMessage = await createProgressBar(plugin.bot, msg, "deleting"); + + // Update the progress bar during the delay + let stage = 0; + for (let i = 1; i <= 10; i++) { + await new Promise((resolve) => setTimeout(resolve, 50)); // 50 ms delay between updates + stage = await updateProgressBar(plugin.bot, msg, progressBarMessage, 10, i, stage); + } + + await plugin.bot?.deleteMessage(msg.chat.id, msg.message_id); + await deleteProgressBar(plugin.bot, msg, progressBarMessage); + } else { + // Send a confirmation reply if the message is too old to be deleted + // TODO: needs deep testing and integration + // if (plugin.botUser) { + // await GramJs.sendReaction(plugin.botUser, msg); + // } + await plugin.bot?.sendMessage(msg.chat.id, "...โœ…...", { reply_to_message_id: msg.message_id }); + } +} + +export async function appendMessageToTelegramMd( + plugin: TelegramSyncPlugin, + msg: TelegramBot.Message, + formattedContent: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error?: any +) { + // Do not append messages if not connected + if (!plugin.botConnected) return; + + // Determine the location for the Telegram.md file + const location = plugin.settings.newNotesLocation || ""; + createFolderIfNotExist(plugin.app.vault, location); + + const telegramMdPath = normalizePath(location ? `${location}/Telegram.md` : "Telegram.md"); + let telegramMdFile = plugin.app.vault.getAbstractFileByPath(telegramMdPath) as TFile; + + // Create or modify the Telegram.md file + if (!telegramMdFile) { + telegramMdFile = await plugin.app.vault.create(telegramMdPath, `${formattedContent}\n`); + } else { + const fileContent = await plugin.app.vault.read(telegramMdFile); + await plugin.app.vault.modify(telegramMdFile, `${fileContent}\n***\n\n${formattedContent}\n`); + } + await finalizeMessageProcessing(plugin, msg, error); +} + +// Apply a template to a message's content +export async function applyNoteContentTemplate( + plugin: TelegramSyncPlugin, + templatePath: string, + msg: TelegramBot.Message, + fileLink?: string +): Promise { + let templateContent: string; + try { + const templateFile = plugin.app.vault.getAbstractFileByPath(normalizePath(templatePath)) as TFile; + templateContent = await plugin.app.vault.read(templateFile); + } catch (e) { + throw new Error(`Template "${templatePath}" not found! ${e}`); + } + + const textContentMd = await convertMessageTextToMarkdown(msg); + // Check if the message is forwarded and extract the required information + const forwardFromLink = getForwardFromLink(msg); + const fullContentMd = + (forwardFromLink ? `**Forwarded from ${forwardFromLink}**\n\n` : "") + + (fileLink ? fileLink + "\n\n" : "") + + textContentMd; + + if (!templateContent) { + return fullContentMd; + } + + let voiceTranscript = ""; + if (!templateContent || templateContent.includes("{{voiceTranscript")) { + voiceTranscript = await GramJs.transcribeAudio(msg, await plugin.getBotUser(msg)); + } + + const messageDateTime = new Date(msg.date * 1000); + const creationDateTime = msg.forward_date ? new Date(msg.forward_date * 1000) : messageDateTime; + + const dateTimeNow = new Date(); + const itemsForReplacing: [string, string][] = []; + + let proccessedContent = templateContent + // TODO Copy tab and blockquotes to every new line of {{content*}} or {{voiceTranscript*}} if they are placed in front of this variables. + .replace("{{content}}", fullContentMd) + .replace(/{{content:(.*?)}}/g, (_, property: string) => { + let subContent = ""; + if (property.toLowerCase() == "firstline") { + subContent = textContentMd.split("\n")[0]; + } else if (property.toLowerCase() == "text") { + subContent = textContentMd; + } else if (Number.isInteger(parseFloat(property))) { + // property is length + subContent = textContentMd.substring(0, Number(property)); + } else { + displayAndLog(plugin, `Template variable {{content:${property}}} isn't supported!`, 15 * 1000); + } + return subContent; + }) // message text of specified length + .replace(/{{file}}/g, fileLink || "") + .replace(/{{file:link}}/g, fileLink?.startsWith("!") ? fileLink.slice(1) : fileLink || "") + .replace(/{{voiceTranscript}}/g, voiceTranscript) + .replace(/{{voiceTranscript:(.*?)}}/g, (_, property: string) => { + let subContent = ""; + if (property.toLowerCase() == "firstline") { + subContent = voiceTranscript.split("\n")[0]; + } else if (Number.isInteger(parseFloat(property))) { + // property is length + subContent = voiceTranscript.substring(0, Number(property)); + } else { + displayAndLog(plugin, `Template variable {{voiceTranscript:${property}}} isn't supported!`, 15 * 1000); + } + return subContent; + }) + .replace(/{{messageDate:(.*?)}}/g, (_, format) => formatDateTime(messageDateTime, format)) + .replace(/{{messageTime:(.*?)}}/g, (_, format) => formatDateTime(messageDateTime, format)) + .replace(/{{date:(.*?)}}/g, (_, format) => formatDateTime(dateTimeNow, format)) + .replace(/{{time:(.*?)}}/g, (_, format) => formatDateTime(dateTimeNow, format)) + .replace(/{{forwardFrom}}/g, forwardFromLink) + .replace(/{{user}}/g, getUserLink(msg)) // link to the user who sent the message + .replace(/{{userId}}/g, msg.from?.id.toString() || msg.message_id.toString()) // id of the user who sent the message + .replace(/{{chat}}/g, getChatLink(msg)) // link to the chat with the message + .replace(/{{chatId}}/g, msg.chat.id.toString()) // id of the chat with the message + .replace(/{{topic}}/g, await getTopicLink(plugin, msg)) // link to the topic with the message + .replace( + /{{topicId}}/g, + (msg.chat.is_forum && (msg.message_thread_id || msg.reply_to_message?.message_thread_id || 1).toString()) || + "" + ) // head message id representing the topic + .replace(/{{messageId}}/g, msg.message_id.toString()) + .replace(/{{replyMessageId}}/g, getReplyMessageId(msg)) + .replace(/{{url1}}/g, getUrl(msg)) // fisrt url from the message + .replace(/{{url1:preview(.*?)}}/g, (_, height: string) => { + let linkPreview = ""; + const url1 = getUrl(msg); + if (url1) { + if (!height || Number.isInteger(parseFloat(height))) { + linkPreview = ``; + } else { + displayAndLog(plugin, `Template variable {{url1:preview${height}}} isn't supported!`, 15 * 1000); + } + } + return linkPreview; + }) // preview for first url from the message + .replace(/{{creationDate:(.*?)}}/g, (_, format) => formatDateTime(creationDateTime, format)) // date, when the message was created + .replace(/{{creationTime:(.*?)}}/g, (_, format) => formatDateTime(creationDateTime, format)) // time, when the message was created + .replace(/{{replace:(.*?)=>(.*?)}}/g, (_, replaceThis, replaceWith) => { + itemsForReplacing.push([replaceThis, replaceWith]); + return ""; + }) + .replace(/{{replace:(.*?)}}/g, (_, replaceThis) => { + itemsForReplacing.push([replaceThis, ""]); + return ""; + }); + + itemsForReplacing.forEach( + // TODO add replacing new lines "\n" + ([replaceThis, replaceWith]) => + (proccessedContent = proccessedContent.replace(new RegExp(escapeRegExp(replaceThis), "g"), replaceWith)) + ); + return proccessedContent; +} diff --git a/src/telegram/messageProcessors.ts b/src/telegram/messageProcessors.ts deleted file mode 100644 index 60f58db..0000000 --- a/src/telegram/messageProcessors.ts +++ /dev/null @@ -1,148 +0,0 @@ -import TelegramBot from "node-telegram-bot-api"; -import TelegramSyncPlugin from "../main.js"; -import { getFormattedMessage, getForwardFromLink, getUserLink } from "./utils"; -import { createFolderIfNotExist } from "src/utils/fsUtils.js"; -import { TFile, TFolder, normalizePath } from "obsidian"; -import { formatDateTime } from "../utils/dateUtils"; -import { displayAndLog, displayAndLogError } from "src/utils/logUtils.js"; -import { createProgressBar, deleteProgressBar, updateProgressBar } from "./progressBar.js"; - -// Delete a message or send a confirmation reply based on settings and message age -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function finalizeMessageProcessing(plugin: TelegramSyncPlugin, msg: TelegramBot.Message, error?: any) { - if (error) await displayAndLogError(plugin, error, msg); - if (error || !plugin.bot) { - return; - } - - const currentTime = new Date(); - const messageTime = new Date(msg.date * 1000); - const timeDifference = currentTime.getTime() - messageTime.getTime(); - const hoursDifference = timeDifference / (1000 * 60 * 60); - - if (plugin.settings.deleteMessagesFromTelegram && hoursDifference <= 48) { - // Send the initial progress bar - const progressBarMessage = await createProgressBar(plugin.bot, msg, "deleting"); - - // Update the progress bar during the delay - let stage = 0; - for (let i = 1; i <= 10; i++) { - await new Promise((resolve) => setTimeout(resolve, 50)); // 50 ms delay between updates - stage = await updateProgressBar(plugin.bot, msg, progressBarMessage, 10, i, stage); - } - - await plugin.bot?.deleteMessage(msg.chat.id, msg.message_id); - await deleteProgressBar(plugin.bot, msg, progressBarMessage); - } else { - // Send a confirmation reply if the message is too old to be deleted - await plugin.bot?.sendMessage(msg.chat.id, "...โœ…...", { reply_to_message_id: msg.message_id }); - } -} - -export async function appendMessageToTelegramMd( - plugin: TelegramSyncPlugin, - msg: TelegramBot.Message, - formattedContent: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error?: any -) { - // Do not append messages if not connected - if (!plugin.botConnected) return; - - // Determine the location for the Telegram.md file - const location = plugin.settings.newNotesLocation || ""; - createFolderIfNotExist(plugin.app.vault, location); - - const telegramMdPath = normalizePath(location ? `${location}/Telegram.md` : "Telegram.md"); - let telegramMdFile = plugin.app.vault.getAbstractFileByPath(telegramMdPath) as TFile; - - // Create or modify the Telegram.md file - if (!telegramMdFile) { - telegramMdFile = await plugin.app.vault.create(telegramMdPath, `${formattedContent}\n`); - } else { - const fileContent = await plugin.app.vault.read(telegramMdFile); - await plugin.app.vault.modify(telegramMdFile, `${fileContent}\n***\n\n${formattedContent}\n`); - } - await finalizeMessageProcessing(plugin, msg, error); -} - -// Apply a template to a message's content -export async function applyTemplate( - plugin: TelegramSyncPlugin, - templatePath: string, - msg: TelegramBot.Message, - content?: string -): Promise { - const contentMd = content || (await getFormattedMessage(msg)); - if (!templatePath) { - return contentMd; - } - let templateFile: TFile; - try { - templateFile = plugin.app.vault.getAbstractFileByPath(normalizePath(templatePath)) as TFile; - } catch (e) { - await displayAndLogError(plugin, `Template "${templatePath}" not found! ${e}`, msg); - return contentMd; - } - if (!templateFile || templateFile instanceof TFolder) { - return contentMd; - } - - const messageDateTime = new Date(msg.date * 1000); - const creationDateTime = msg.forward_date ? new Date(msg.forward_date * 1000) : messageDateTime; - // Check if the message is forwarded and extract the required information - const forwardFromLink = getForwardFromLink(msg); - - const dateTimeNow = new Date(); - const templateContent = await plugin.app.vault.read(templateFile); - return templateContent - .replace("{{content}}", contentMd) - .replace(/{{messageDate:(.*?)}}/g, (_, format) => formatDateTime(messageDateTime, format)) - .replace(/{{messageTime:(.*?)}}/g, (_, format) => formatDateTime(messageDateTime, format)) - .replace(/{{date:(.*?)}}/g, (_, format) => formatDateTime(dateTimeNow, format)) - .replace(/{{time:(.*?)}}/g, (_, format) => formatDateTime(dateTimeNow, format)) - .replace(/{{forwardFrom}}/g, forwardFromLink) - .replace(/{{userId}}/g, msg.from?.id.toString() || msg.message_id.toString()) // id of the user who sent the message - .replace(/{{user}}/g, getUserLink(msg)) // link to the user who sent the message - .replace(/{{content:(.*?)}}/g, (_, length: string) => { - let subContent = ""; - if (length.toLowerCase() == "firstline") { - subContent = contentMd.split("\n")[0]; - } else if (Number.isInteger(parseFloat(length))) { - subContent = contentMd.substring(0, Number(length)); - } else { - displayAndLog(plugin, `Template variable {{content:${length}}} isn't supported!`, 15 * 1000); - } - return subContent; - }) // message text of specified length - .replace(/{{creationDate:(.*?)}}/g, (_, format) => formatDateTime(creationDateTime, format)) // date, when the message was created - .replace(/{{creationTime:(.*?)}}/g, (_, format) => formatDateTime(creationDateTime, format)); // time, when the message was created -} - -// example of msg object -// { -// "message_id": 508, -// "from": { -// "id": 1112226370, -// "is_bot": false, -// "first_name": "soberHacker", -// "username": "soberhacker", -// "language_code": "en" -// }, -// "chat": { -// "id": 1112226370, -// "first_name": "soberHacker", -// "username": "soberhacker", -// "type": "private" -// }, -// "date": 1685138029, -// "forward_from": { -// "id": 1112226370, -// "is_bot": false, -// "first_name": "soberHacker", -// "username": "soberhacker", -// "language_code": "en" -// }, -// "forward_date": 1684944034, -// "text": "Text" -// } diff --git a/src/telegram/utils.ts b/src/telegram/utils.ts deleted file mode 100644 index bb4d04d..0000000 --- a/src/telegram/utils.ts +++ /dev/null @@ -1,129 +0,0 @@ -import TelegramBot from "node-telegram-bot-api"; - -export const fileTypes = ["photo", "video", "voice", "document", "audio", "video_note"]; - -export function sanitizeFileName(fileName: string): string { - const invalidCharacters = /[\\/:*?"<>|\n\r]/g; - const replacementCharacter = "_"; - return fileName.replace(invalidCharacters, replacementCharacter); -} - -export async function getFormattedMessage(msg: TelegramBot.Message): Promise { - let text = msg.text || ""; - - if (msg.entities) { - let offset = 0; - for (const entity of msg.entities) { - const entityStart = entity.offset + offset; - let entityEnd = entityStart + entity.length; - - let entityText = text.slice(entityStart, entityEnd); - - if (entityText.endsWith("\n")) { - entityEnd = entityEnd - 1; - } - const beforeEntity = text.slice(0, entityStart); - entityText = text.slice(entityStart, entityEnd); - const afterEntity = text.slice(entityEnd); - - switch (entity.type) { - case "bold": - entityText = `**${entityText}**`; - offset += 4; - break; - case "italic": - entityText = `*${entityText}*`; - offset += 2; - break; - case "underline": - entityText = `${entityText}`; - offset += 7; - break; - case "strikethrough": - entityText = `~~${entityText}~~`; - offset += 4; - break; - case "code": - entityText = "`" + entityText + "`"; - offset += 2; - break; - case "pre": - entityText = "```\n" + entityText + "\n```"; - offset += 8; - break; - case "text_link": - if (entity.url) { - entityText = `[${entityText}](${entity.url})`; - offset += 4 + entity.url.length; - } - break; - default: - break; - } - text = beforeEntity + entityText + afterEntity; - } - } - - return text; -} - -export function getForwardFromLink(msg: TelegramBot.Message): string { - let forwardFromLink = ""; - - if (msg.forward_from || msg.forward_from_chat || msg.forward_sender_name || msg.from) { - let username = ""; - let title = ""; - - if (msg.forward_from) { - username = msg.forward_from.username || "no_username_" + msg.forward_from.id.toString().slice(4); - title = msg.forward_from.first_name + (msg.forward_from.last_name ? " " + msg.forward_from.last_name : ""); - } else if (msg.forward_from_chat) { - username = - msg.forward_from_chat.username || - `c/${msg.forward_from_chat.id.toString().slice(4)}/${msg.forward_from_message_id || "999999999"}`; - title = - msg.forward_from_chat.title + (msg.forward_signature ? `(${msg.forward_signature})` : "") || - msg.forward_from_chat.username || - ""; - } else if (msg.forward_sender_name) { - username = "hidden_account_" + msg.forward_date; - title = msg.forward_sender_name; - } else if (msg.from) { - username = msg.from.username || "no_username_" + msg.from.id.toString().slice(4); - title = msg.from.first_name + (msg.from.last_name ? " " + msg.from.last_name : ""); - } - forwardFromLink = forwardFromLink || `[${title}](https://t.me/${username})`; - } - - return forwardFromLink; -} - -export function getUserLink(msg: TelegramBot.Message): string { - let userLink = ""; - - if (msg.from) { - let username = ""; - let title = ""; - username = msg.from.username || "no_username_" + msg.from.id.toString().slice(4); - title = msg.from.first_name + (msg.from.last_name ? " " + msg.from.last_name : ""); - userLink = `[${title}](https://t.me/${username})`; - } - - return userLink; -} - -export function base64ToString(base64: string): string { - return Buffer.from(base64, "base64").toString("utf-8"); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getFileObject(msg: TelegramBot.Message): { fileType?: string; fileObject?: any } { - for (const fileType of fileTypes) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((msg as any)[fileType]) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { fileType: fileType, fileObject: (msg as any)[fileType] }; - } - } - return {}; -} diff --git a/src/utils/fsUtils.ts b/src/utils/fsUtils.ts index 3ce083a..e593be2 100644 --- a/src/utils/fsUtils.ts +++ b/src/utils/fsUtils.ts @@ -23,3 +23,13 @@ export async function createFolderIfNotExist(vault: Vault, folderpath: string) { } }); } + +export function sanitizeFileName(fileName: string): string { + const invalidCharacters = /[\\/:*?"<>|\n\r]/g; + const replacementCharacter = "_"; + return fileName.replace(invalidCharacters, replacementCharacter); +} + +export function base64ToString(base64: string): string { + return Buffer.from(base64, "base64").toString("utf-8"); +}