├── .yarnrc.yml ├── icon.png ├── assets ├── query.PNG ├── run.PNG └── playground.PNG ├── .gitignore ├── LICENSE ├── index.html ├── package.json ├── src ├── timed-job.ts ├── index.css ├── utils.ts ├── command-utils.ts ├── command-playground.ts ├── index.ts ├── settings.ts ├── message-handlers.ts └── command-handlers.ts ├── .github └── workflows │ └── publish.yml ├── README.md └── tsconfig.json /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LelouchHe/logseq-local-telegram-bot/HEAD/icon.png -------------------------------------------------------------------------------- /assets/query.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LelouchHe/logseq-local-telegram-bot/HEAD/assets/query.PNG -------------------------------------------------------------------------------- /assets/run.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LelouchHe/logseq-local-telegram-bot/HEAD/assets/run.PNG -------------------------------------------------------------------------------- /assets/playground.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LelouchHe/logseq-local-telegram-bot/HEAD/assets/playground.PNG -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Dependency directories 10 | node_modules/ 11 | 12 | # Optional cache directory 13 | .parcel-cache/ 14 | .yarn/ 15 | 16 | # Dist folder 17 | dist/ 18 | 19 | .vscode/ 20 | 21 | coverage/ 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 LelouchHe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | Document 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logseq-local-telegram-bot", 3 | "version": "0.2.14", 4 | "description": "A local Telegram bot plugin that can handle messages from and share notes with eligible Telegram users", 5 | "author": "LelouchHe", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "parcel ./index.html", 9 | "build": "parcel build --public-url . --no-source-maps index.html" 10 | }, 11 | "devDependencies": { 12 | "@parcel/transformer-sass": "2.8.3", 13 | "@types/codemirror": "^5.60.7", 14 | "@types/marked": "^4.0.8", 15 | "@types/minimist": "^1.2.2", 16 | "@types/string-argv": "^0.3.0", 17 | "buffer": "^5.5.0", 18 | "crypto-browserify": "^3.12.0", 19 | "events": "^3.3.0", 20 | "https-browserify": "^1.0.0", 21 | "parcel": "^2.8.3", 22 | "path-browserify": "^1.0.1", 23 | "punycode": "^1.4.1", 24 | "querystring-es3": "^0.2.1", 25 | "stream-browserify": "^3.0.0", 26 | "stream-http": "^3.2.0", 27 | "url": "^0.11.0", 28 | "util": "^0.12.5" 29 | }, 30 | "dependencies": { 31 | "@codemirror/lang-javascript": "^6.1.4", 32 | "@codemirror/theme-one-dark": "^6.1.1", 33 | "@logseq/libs": "^0.0.14", 34 | "@nextjournal/lang-clojure": "^1.0.0", 35 | "@pgrabovets/json-view": "^2.7.1", 36 | "codemirror": "^6.0.1", 37 | "marked": "^4.2.12", 38 | "minimist": "^1.2.8", 39 | "string-argv": "^0.3.1", 40 | "telegraf": "3.40.0" 41 | }, 42 | "logseq": { 43 | "main": "dist/index.html", 44 | "id": "logseq-local-telegram-bot", 45 | "title": "Local Telegram Bot", 46 | "icon": "./icon.png" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/timed-job.ts: -------------------------------------------------------------------------------- 1 | import { log } from "./utils"; 2 | 3 | export { runAtInterval, runAt, cancelJob }; 4 | 5 | const MINIMUM_GAP_IN_SECONDS = 10; 6 | const MS_PER_SECOND = 1000; 7 | const jobIds: { [key: string]: number } = {}; 8 | 9 | function getNextTargetTime(time: Date, seconds: number) { 10 | let target = time.getTime(); 11 | const now = Date.now(); 12 | if (target < now) { 13 | target += (1 + Math.floor((now - target) / seconds / MS_PER_SECOND)) * seconds * MS_PER_SECOND; 14 | } 15 | 16 | return new Date(target); 17 | } 18 | 19 | function runAtInterval(name: string, time: Date, seconds: number, cb: () => void) { 20 | let target = getNextTargetTime(time, seconds); 21 | if (target.getTime() - Date.now() < MINIMUM_GAP_IN_SECONDS * MS_PER_SECOND) { 22 | log(`next running time(${target.toLocaleString()}) is too close, go to next interval`); 23 | target.setTime(target.getTime() + seconds * MS_PER_SECOND); 24 | } 25 | 26 | jobIds[name] = setTimeout(() => { 27 | log(`job(${name}: ${jobIds[name]}) is running at ${new Date().toLocaleString()}`); 28 | cb(); 29 | runAtInterval(name, time, seconds, cb); 30 | }, target.getTime() - Date.now()); 31 | 32 | log(`job(${name}: ${jobIds[name]}) will run at ${target.toLocaleString()}`); 33 | } 34 | 35 | function runAt(name: string, time: Date, cb: () => void) { 36 | const delay = time.getTime() - Date.now(); 37 | if (delay < 0) { 38 | log(`can't run at past time: ${time.toLocaleString()}`); 39 | return; 40 | } 41 | 42 | jobIds[name] = setTimeout(() => { 43 | log(`job(${name}: ${jobIds[name]}) is running at ${new Date().toLocaleString()}`); 44 | cb(); 45 | }, delay); 46 | 47 | log(`job(${name}: ${jobIds[name]}) will run at ${time.toLocaleString()}`); 48 | } 49 | 50 | function cancelJob(name: string) { 51 | log(`job(${name}: ${jobIds[name]}) is cancelled`); 52 | clearTimeout(jobIds[name]); 53 | delete jobIds[name]; 54 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | width: 100vw; 5 | height: 100vh; 6 | overflow: hidden; 7 | } 8 | 9 | body { 10 | position: fixed; 11 | } 12 | 13 | #playground { 14 | width: 90vw; 15 | height: 90vh; 16 | top: 5vh; 17 | left: 5vw; 18 | background-color: gray; 19 | position: relative; 20 | } 21 | 22 | #playground .close { 23 | position: absolute; 24 | right: 5px; 25 | cursor: pointer; 26 | } 27 | 28 | #playground .info { 29 | padding: 10px 10px 5px; 30 | font-size: 20px; 31 | } 32 | 33 | #playground .block { 34 | display: none; 35 | } 36 | 37 | #playground .signature { 38 | margin: 0 10px 0; 39 | width: 75%; 40 | font-size: 20px; 41 | } 42 | 43 | #playground .input { 44 | padding: 0 10px; 45 | font-size: 20px; 46 | } 47 | 48 | #playground .debug { 49 | color: green; 50 | margin: 5px; 51 | cursor: pointer; 52 | margin-left: 60px; 53 | } 54 | 55 | #playground .args { 56 | margin: 0 10px 0; 57 | width: 75%; 58 | font-size: 20px; 59 | } 60 | 61 | #playground .code, 62 | #playground .result, 63 | #playground .logs { 64 | margin: 10px; 65 | display: flex; 66 | justify-content: center; 67 | position: relative; 68 | } 69 | 70 | #playground .cm-editor { 71 | height: 100%; 72 | } 73 | 74 | @media screen and (max-width: 800px) { 75 | #playground .code { 76 | height: 40%; 77 | } 78 | 79 | #playground .result { 80 | height: 25%; 81 | } 82 | 83 | #playground .logs { 84 | height: 16%; 85 | } 86 | } 87 | 88 | @media screen and (min-width: 800px) { 89 | #playground .code { 90 | height: 85%; 91 | width: 55%; 92 | float: left; 93 | } 94 | 95 | #playground .result { 96 | height: 50%; 97 | } 98 | 99 | #playground .logs { 100 | height: 33%; 101 | } 102 | } 103 | 104 | #playground .content { 105 | width: 100%; 106 | background-color: white; 107 | overflow: auto; 108 | white-space: nowrap; 109 | } 110 | 111 | #playground .content::after { 112 | color: #000; 113 | content: attr(data-bg-text); 114 | display: block; 115 | font-size: 40px; 116 | line-height: 1; 117 | position: absolute; 118 | bottom: 0; 119 | right: 15px; 120 | color: lightgray; 121 | opacity: 0.7; 122 | } -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build plugin 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10 8 | 9 | env: 10 | PLUGIN_NAME: logseq-local-telegram-bot 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: "19.x" # You might need to adjust this value to your own version 23 | - name: Build 24 | id: build 25 | run: | 26 | npm install -g yarn 27 | yarn 28 | yarn build 29 | mkdir ${{ env.PLUGIN_NAME }} 30 | cp -r README.md package.json icon.png assets ${{ env.PLUGIN_NAME }} 31 | mv dist ${{ env.PLUGIN_NAME }} 32 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 33 | ls 34 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 35 | 36 | - name: Create Release 37 | uses: ncipollo/release-action@v1 38 | id: create_release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | VERSION: ${{ github.ref }} 42 | with: 43 | allowUpdates: true 44 | draft: false 45 | prerelease: false 46 | 47 | - name: Upload zip file 48 | id: upload_zip 49 | uses: actions/upload-release-asset@v1 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | upload_url: ${{ steps.create_release.outputs.upload_url }} 54 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 55 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 56 | asset_content_type: application/zip 57 | 58 | - name: Upload package.json 59 | id: upload_metadata 60 | uses: actions/upload-release-asset@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | upload_url: ${{ steps.create_release.outputs.upload_url }} 65 | asset_path: ./package.json 66 | asset_name: package.json 67 | asset_content_type: application/json 68 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { BlockEntity } from "@logseq/libs/dist/LSPlugin.user"; 3 | 4 | import { Message } from "typegram"; 5 | 6 | import { settings } from "./settings"; 7 | 8 | export { log, error, showMsg, showError, getDateString, getTimestampString, isMessageAuthorized, nameof, stringifyBlocks }; 9 | 10 | const PROJECT_NAME = "Local Telegram Bot"; 11 | 12 | function format(message: string) { 13 | return `[${PROJECT_NAME}] ` + message; 14 | } 15 | 16 | // Though it doesn't provide the name, at least it does compile check 17 | // https://stackoverflow.com/a/50470026 18 | function nameof(name: Extract): string { 19 | return name; 20 | } 21 | 22 | function log(message: string) { 23 | console.log(format(message)); 24 | } 25 | 26 | function error(message: string) { 27 | console.error(format(message)); 28 | } 29 | 30 | function showMsg(message: string) { 31 | logseq.UI.showMsg(format(message)); 32 | } 33 | 34 | function showError(message: string) { 35 | logseq.UI.showMsg(format(message), "error"); 36 | } 37 | 38 | function getDateString(date: Date) { 39 | const d = { 40 | day: `${date.getDate()}`.padStart(2, "0"), 41 | month: `${date.getMonth() + 1}`.padStart(2, "0"), 42 | year: date.getFullYear() 43 | }; 44 | 45 | return `${d.year}${d.month}${d.day}`; 46 | } 47 | 48 | function getTimestampString(date: Date) { 49 | const t = { 50 | hour: `${date.getHours()}`.padStart(2, "0"), 51 | minute: `${date.getMinutes()}`.padStart(2, "0") 52 | }; 53 | 54 | return `${t.hour}:${t.minute}`; 55 | } 56 | 57 | function isMessageAuthorized(message: Message.ServiceMessage): boolean { 58 | if (!message.from?.username) { 59 | log("Invalid username from message"); 60 | return false; 61 | } 62 | 63 | if (settings.authorizedUsers.length > 0) { 64 | if (!settings.authorizedUsers.includes(message.from.username)) { 65 | log(`Unauthorized username: ${message.from.username}`) 66 | return false; 67 | } 68 | } 69 | 70 | const chatIds = settings.chatIds; 71 | if (!(message.from.username in chatIds)) { 72 | chatIds[message.from.username] = message.chat.id; 73 | } 74 | 75 | settings.chatIds = chatIds; 76 | 77 | return true; 78 | } 79 | 80 | function convertBlocksToText(root: BlockEntity, addId: boolean, tab: string, indent: string): string { 81 | if (!root) { 82 | error("Block doesn't include content"); 83 | return ""; 84 | } 85 | 86 | let text = indent + root.content + (addId ? `(\`${root.uuid}\`)` : "") + "\n"; 87 | if (root.children) { 88 | for (let child of root.children) { 89 | text += convertBlocksToText(child as BlockEntity, addId, tab, indent + tab); 90 | } 91 | } 92 | 93 | return text; 94 | } 95 | 96 | function stringifyBlocks(root: BlockEntity, addId: boolean) { 97 | return convertBlocksToText(root, addId, "\t\t", ""); 98 | } -------------------------------------------------------------------------------- /src/command-utils.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { LSPluginUser } from "@logseq/libs/dist/LSPlugin.user"; 3 | 4 | import { OPEN_PLAYGROUND_RENDERER } from "./command-playground"; 5 | import { log, error } from "./utils"; 6 | 7 | export { Command, parseCommand, runCommand, stringifyCommand, setupSlashCommands, commandInfos, COMMAND_PAGE_NAME }; 8 | 9 | class Command { 10 | public type: string = ""; 11 | public name: string = ""; 12 | public params: string[] = []; 13 | public script: string = ""; 14 | public description: string = ""; 15 | } 16 | 17 | class CommandInfo { 18 | public type: string = ""; 19 | public language: string = ""; 20 | public description: string = ""; 21 | public slashCommand: string = ""; 22 | 23 | public constructor( 24 | type: string, 25 | language: string, 26 | description: string, 27 | slashCommand: string) { 28 | this.type = type; 29 | this.language = language; 30 | this.description = description; 31 | this.slashCommand = slashCommand; 32 | } 33 | 34 | public get pageName(): string { 35 | return `[[${COMMAND_PAGE_NAME}/${this.type}]]`; 36 | } 37 | } 38 | 39 | const COMMAND_PAGE_NAME = "local-telegram-bot"; 40 | const QUERY_COMMAND = "query"; 41 | const RUN_COMMAND = "run"; 42 | 43 | const commandInfos: CommandInfo[] = [ 44 | new CommandInfo( 45 | QUERY_COMMAND, 46 | "clojure", 47 | "Query customized datascript", 48 | "Local Telegram Bot: Define Customized Query"), 49 | new CommandInfo( 50 | RUN_COMMAND, 51 | "js", 52 | "Run customized js", 53 | "Local Telegram Bot: Define Customized Query") 54 | ]; 55 | 56 | function slashTemplate(name: string, language: string) { 57 | let template = `[[${COMMAND_PAGE_NAME}/${name}]] name param0 param1 ${OPEN_PLAYGROUND_RENDERER}\n`; 58 | template += `\`\`\`${language}\n\`\`\`\n`; 59 | template += "description"; 60 | return template; 61 | } 62 | 63 | function setupSlashCommands() { 64 | // FIXME: unable to un-register? 65 | for (let info of commandInfos) { 66 | logseq.Editor.registerSlashCommand(info.slashCommand, async (e) => { 67 | logseq.Editor.updateBlock(e.uuid, slashTemplate(info.type, info.language)); 68 | }); 69 | } 70 | } 71 | 72 | // FIXME: not that sandboxed 73 | // function needs to be run here, not outside iframe 74 | async function runFunction(body: string, argv: any[], params: string[] = []) { 75 | const func = `function(${params.join(", ")}) { "use stricts"; ${body} }`; 76 | const wrap = `{ return async ${func}; };`; 77 | 78 | const iframe = document.createElement('iframe'); 79 | iframe.style.display = "none"; 80 | // try best to sandbox 81 | iframe.sandbox.value = "allow-same-origin allow-scripts"; 82 | document.body.appendChild(iframe); 83 | 84 | // pass logseq to iframe 85 | iframe.contentWindow!.logseq = logseq as LSPluginUser; 86 | const logs: any[] = []; 87 | const newLog = (...data: any[]) => { 88 | logs.push(...data); 89 | } 90 | iframe.contentWindow!.self.console.log = newLog; 91 | iframe.contentWindow!.self.console.error = newLog; 92 | 93 | const sandboxedFunc: Function = new iframe.contentWindow!.self.Function(wrap).call(null); 94 | const result = await sandboxedFunc.apply(null, argv); 95 | document.body.removeChild(iframe); 96 | 97 | return { 98 | result: result, 99 | logs: logs 100 | }; 101 | } 102 | 103 | async function runScript(script: string, inputs: any[]) { 104 | return { 105 | result: await logseq.DB.datascriptQuery(script, ...inputs), 106 | logs: [] as any[] 107 | } 108 | } 109 | 110 | function parseCommand(content: string): Command | null { 111 | let commandInfo: CommandInfo | undefined; 112 | for (let info of commandInfos) { 113 | if (content.startsWith(info.pageName)) { 114 | commandInfo = info; 115 | break; 116 | } 117 | } 118 | 119 | if (!commandInfo) { 120 | log(`content is not valid: ${content}`); 121 | return null; 122 | } 123 | 124 | const parts = content.substring(commandInfo.pageName.length).split("```"); 125 | if (parts.length < 2) { 126 | log(`content is not valid: ${content}`); 127 | return null; 128 | } 129 | 130 | const command = new Command(); 131 | command.type = commandInfo.type; 132 | 133 | let signature = parts[0].trim(); 134 | if (signature.endsWith(OPEN_PLAYGROUND_RENDERER)) { 135 | signature = signature.substring(0, signature.length - OPEN_PLAYGROUND_RENDERER.length).trim(); 136 | } 137 | 138 | const names = signature.split(" "); 139 | command.name = names[0]; 140 | command.params = names.slice(1); 141 | 142 | command.script = parts[1].substring(parts[1].indexOf("\n")).trim(); 143 | command.description = parts.length == 3 ? parts[2].trim() : ""; 144 | 145 | return command; 146 | } 147 | 148 | async function runCommand(command: Command, argv: any[]) { 149 | switch (command.type) { 150 | case "query": 151 | return await runScript(command.script, argv); 152 | 153 | case "run": 154 | return await runFunction(command.script, argv, command.params); 155 | 156 | default: 157 | error(`invalid command type: ${command.type}`); 158 | return null; 159 | } 160 | } 161 | 162 | function stringifyCommand(command: Command): string { 163 | let commandInfo: CommandInfo | undefined; 164 | for (let info of commandInfos) { 165 | if (info.type === command.type) { 166 | commandInfo = info; 167 | break; 168 | } 169 | } 170 | 171 | if (!commandInfo) { 172 | log(`command is not valid: ${command.type}`); 173 | return ""; 174 | } 175 | 176 | const lines: string[] = [ 177 | `${commandInfo.pageName} ${command.name} ${command.params.join(" ")} ${OPEN_PLAYGROUND_RENDERER}\n`, 178 | "```" + commandInfo.language + "\n" + command.script + "\n```\n", 179 | `${command.description}\n` 180 | ]; 181 | 182 | return lines.join(""); 183 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Local Telegram Bot 2 | 3 | This is a local Telegram bot plugin that can handle messages from and share notes with eligible Telegram users. It's designed to be used as another way to use Logseq from mobile, when existing sync feature is not available yet for non-iCloud users. 4 | 5 | Currently, it's still under heavy development. 6 | 7 | ## How to use it 8 | 9 | 1. [Create a Telegram bot](https://core.telegram.org/bots#3-how-do-i-create-a-bot). 10 | 2. Complete the setting 11 | * "Bot Token" is required 12 | * "Is Main Bot" is required for the main Logseq to handle requests from Telegram. If you have multiple Logseq open at same time, probably from different devices, make sure only one of them is set to main bot, to avoid conflicts. 13 | * "Authorized Users" is required to stop ineligible users sending messages to your Logseq. 14 | * "Enable Customized Command" and "Enable Customized Command From Message" are for advanced users, who can create customized ts/datascript to respond to Telegram command. It works on main bot only. They're experimenting features, and could be changed later. 15 | 3. Send texts and photos to this bot directly. The texts and photos will be automatically writen to the specified page and inbox 16 | 4. **NOTE**: since it's a local bot, the logseq needs to be open all the time, or the bot won't run and the data sent from Telegram might be expired before bot could fetch them. 17 | 18 | ## Current available features 19 | 20 | * Send text and photo to Logseg 21 | * inlucindg forward and reply, but only content is sent. Forward from who or reply to what is not. 22 | * Page and Inbox can be changed from setting page 23 | * Users can choose to add new messages to the top or bottom of the inbox 24 | * Send block and its children blocks to authorized users who have send messages to Logseq before 25 | * Right-click the block and choose "Local Telegram Bot: Send" 26 | * This is to get its chat id without asking users to type in 27 | * Once someone is removed from authorized users, it won't get any message 28 | * Send not-done task notification at specific time **one day before its time** 29 | * This is only enabled for **Main Bot**. 30 | * Task with scheduled time and deadline time are handled separately 31 | * Users can set the time to send each of the notifications in the settings, or disable this feature by clearing it. 32 | * If it's set to a future date, the notification will wait until that date comes, regardless of the time. 33 | * It now works only with scheduled/deadline. 34 | * Command playgroud is available to debug eligible js/datascript within Logseq. 35 | * There are 2 slash commands `Local Telegram Bot: Define Customized Query` and `Local Telegram Bot: Define Customized Run` to generate template for query/run, with extra debug button to open playground. 36 | * Query is for datascript, which looks like advanced query in Logseq, but it only includes query part and optional input, like below 37 | * ![query](./assets/query.PNG) 38 | * It returns in JSON 39 | * Run is for js, which could uses normal js/DOM and Logseq plugin apis. It looks like below 40 | * ![query](./assets/run.PNG) 41 | * It has access to `logseq` plugin api, and `await` could be used inside, as shown in the example 42 | * It returns in JSON 43 | * Clicking the green arrow opens the playground, where users can debug their datascript or js to make sure it works. 44 | * ![playground](./assets/playground.PNG) 45 | * "Signature" is for the selected command. It's readonly, and can be changed out of playground 46 | * "param0 param1" is the placeholder for actual arguments. no need to type `run_name` any more 47 | * "Code" region is for normal datascript/js code, with limited highlight and auto-completion 48 | * Clicking the green arrow will run the code. The result is shown in json in Result region, and exceptions, console logs and console errors are shown in Logs region. 49 | * Customized command system, which enable users to write datascript(query) or ts/js(run) and get response from Telegram by sending command 50 | * "Enable Customized Command" needs to be enabled. This feature is still experimenting. It might change when it's finalized. 51 | * Users need to send `/query query_name query_input0 query_input1` or just `/query_name query_input0 query_input1` to invoke above query 52 | * Users need to send `/run run_name param0 param1` or just `/run_name param0 param1` to invoke above run 53 | * There is a `/help` command, to list all available commands with their signature and description 54 | * When "Enable Customized Command From Message" is also enabled, users are able to add new commands from Telegram directly, as long as its format is 55 | 56 | ## Future features 57 | 58 | *not a full list, either not ordered by priority* 59 | * Change Page and Inbox from Telegram 60 | * Fetch customized notes 61 | * Support other types of messages 62 | * Support channel message 63 | * Convert non-plain command into correct form (like DEADLINE) 64 | * Send blocks with embed block 65 | * Send page 66 | * Send to specific users, including those un-authorized users 67 | * More time control over TODO notification 68 | * Use Agenda-plugin-style date, instead of builtin date 69 | * Update task status from Telegram 70 | * Add more builtin commands 71 | * Add auto-complete for datascript and Logseq data schema 72 | 73 | ## Contribute 74 | 75 | Feel free to raise an issue or create a pull request! 76 | 77 | ### How to develop it locally 78 | 1. install [node](https://nodejs.org/en/) (v19.6 is used by me) 79 | 2. clone the repo to local folder 80 | 3. `yarn install` 81 | 4. `yarn build` 82 | 5. enable dev mode in logseq 83 | 6. load unpacked pluging from repo folder 84 | 85 | 86 | ## Thanks 87 | 88 | It's inspired by [shady2k](https://github.com/shady2k)'s work on [ 89 | logseq-inbox-telegram-plugin](https://github.com/shady2k/logseq-inbox-telegram-plugin) -------------------------------------------------------------------------------- /src/command-playground.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | 3 | import stringArgv from "string-argv"; 4 | import minimist from "minimist"; 5 | import { basicSetup } from "codemirror"; 6 | import { EditorView, keymap } from "@codemirror/view" 7 | import { indentWithTab } from "@codemirror/commands" 8 | import { javascript } from "@codemirror/lang-javascript"; 9 | import { clojure } from "@nextjournal/lang-clojure"; 10 | import { oneDark, color } from "@codemirror/theme-one-dark"; 11 | 12 | // json-view doesn't have types 13 | // @ts-ignore 14 | import jsonview from '@pgrabovets/json-view'; 15 | import "@pgrabovets/json-view/src/jsonview.scss" 16 | 17 | import { Command, parseCommand, runCommand, stringifyCommand, commandInfos } from "./command-utils"; 18 | import { log } from "./utils"; 19 | 20 | export { setupCommandPlayground, OPEN_PLAYGROUND_RENDERER }; 21 | 22 | const OPEN_PLAYGROUND_NAME = ":local_telegram_bot-openPlayground"; 23 | const OPEN_PLAYGROUND_RENDERER = `{{renderer ${OPEN_PLAYGROUND_NAME}}}`; 24 | 25 | function showResult(result: any, logs: any[]) { 26 | const resultContent = document.querySelector("#playground .result .content") as HTMLDivElement; 27 | const logsContent = document.querySelector("#playground .logs .content") as HTMLDivElement; 28 | 29 | // json-view assume string as json in string, instead of simple string 30 | // https://github.com/pgrabovets/json-view/blob/f37382acb982ffd5e43c4df335b3eaa45f8f2c48/src/json-view.js#L187 31 | if (typeof result === "string" && !result.startsWith("\"") && !result.endsWith("\"")) { 32 | result = `"${result}"`; 33 | } 34 | const resultView = jsonview.create(result); 35 | jsonview.render(resultView, resultContent); 36 | 37 | const logsView = jsonview.create(logs); 38 | jsonview.render(logsView, logsContent); 39 | } 40 | 41 | function showPlayground(blockId: string, command: Command) { 42 | const blockSpan = document.querySelector("#playground .block") as HTMLSpanElement; 43 | const closeButton = document.querySelector("#playground .close") as HTMLElement; 44 | const signatureInput = document.querySelector("#playground .signature") as HTMLInputElement; 45 | const argsInput = document.querySelector("#playground .args") as HTMLInputElement; 46 | const debugButton = document.querySelector("#playground .debug") as HTMLSpanElement; 47 | const codeContent = document.querySelector("#playground .code .content") as HTMLDivElement; 48 | const resultContent = document.querySelector("#playground .result .content") as HTMLDivElement; 49 | const logsContent = document.querySelector("#playground .logs .content") as HTMLDivElement; 50 | 51 | blockSpan.innerText = blockId; 52 | signatureInput.value = [command.name, ...command.params].join(" "); 53 | argsInput.value = ""; 54 | argsInput.placeholder = command.params.join(" "); 55 | codeContent.innerHTML = ""; 56 | codeContent.style.backgroundColor = color.background; 57 | resultContent.innerHTML = ""; 58 | logsContent.innerHTML = ""; 59 | 60 | let language = ""; 61 | for (let info of commandInfos) { 62 | if (command.type == info.type) { 63 | language = info.language; 64 | } 65 | } 66 | 67 | const languageSupport = language == "js" ? javascript() : clojure(); 68 | 69 | const codeView = new EditorView({ 70 | doc: command.script, 71 | extensions: [basicSetup, oneDark, keymap.of([indentWithTab]), languageSupport], 72 | parent: codeContent 73 | }); 74 | 75 | async function startDebug() { 76 | resultContent.innerHTML = ""; 77 | logsContent.innerHTML = ""; 78 | 79 | const args = argsInput.value; 80 | const argv = minimist(stringArgv(args))._; 81 | command.script = codeView.state.doc.toJSON().join("\n"); 82 | 83 | let result: any = null; 84 | let logs: any[] = []; 85 | 86 | try { 87 | const commandResult = await runCommand(command, argv); 88 | if (commandResult == null) { 89 | logs.push("unknow error"); 90 | } else { 91 | result = commandResult.result; 92 | logs = commandResult.logs; 93 | } 94 | } catch (e) { 95 | logs.push(e); 96 | } 97 | 98 | showResult(result, logs); 99 | } 100 | 101 | function endDebug() { 102 | command.script = codeView.state.doc.toJSON().join("\n"); 103 | logseq.Editor.updateBlock(blockId, stringifyCommand(command)); 104 | 105 | logseq.hideMainUI(); 106 | debugButton.removeEventListener("click", startDebug); 107 | closeButton.removeEventListener("click", endDebug); 108 | } 109 | 110 | debugButton.addEventListener("click", startDebug); 111 | closeButton.addEventListener("click", endDebug); 112 | logseq.showMainUI(); 113 | } 114 | 115 | function setupCommandPlayground() { 116 | logseq.provideStyle(` 117 | .command-playground-open { 118 | color: green; 119 | margin: 0 5px 0; 120 | cursor: pointer; 121 | } 122 | `); 123 | 124 | logseq.provideModel({ 125 | async command_playground_open(e: any) { 126 | const { blockid } = e.dataset; 127 | const block = await logseq.Editor.getBlock(blockid); 128 | if (!block) { 129 | return; 130 | } 131 | 132 | const cmd = parseCommand(block.content); 133 | if (!cmd) { 134 | log(`invalid command content: ${block.content}`); 135 | return; 136 | } 137 | 138 | showPlayground(blockid, cmd); 139 | } 140 | }); 141 | 142 | logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => { 143 | let [type] = payload.arguments; 144 | if (type !== OPEN_PLAYGROUND_NAME) { 145 | return; 146 | } 147 | 148 | logseq.provideUI({ 149 | key: payload.uuid, 150 | slot, 151 | template: ` 152 | 155 | `, 156 | }); 157 | }); 158 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { PageEntity, BlockEntity } from "@logseq/libs/dist/LSPlugin.user"; 3 | 4 | // 4.* has URL is not constructor error, fallback to 3.* 5 | import { Telegraf, Context } from "telegraf"; 6 | import { marked } from "marked"; 7 | 8 | // internal 9 | import { log, error, showError, getDateString, nameof, stringifyBlocks } from "./utils"; 10 | import { runAtInterval, cancelJob } from "./timed-job"; 11 | import { settings, initializeSettings, Settings } from "./settings"; 12 | import { setupMessageHandlers } from "./message-handlers"; 13 | import { disableCustomizedCommands, enableCustomizedCommands, setupCommandHandlers } from "./command-handlers"; 14 | import { setupCommandPlayground } from "./command-playground"; 15 | 16 | type OperationHandler = (bot: Telegraf, blockId: string) => Promise; 17 | 18 | const ONE_DAY_IN_SECOND = 24 * 60 * 60; 19 | const SCHEDULED_NOTIFICATION_JOB = "ScheduledTimedJob"; 20 | const DEADLINE_NOTIFICATION_JOB = "DeadlineNotificationJob"; 21 | const JOB_TYPES: { [ key: string ]: string } = { 22 | [SCHEDULED_NOTIFICATION_JOB]: "scheduled", 23 | [DEADLINE_NOTIFICATION_JOB]: "deadline" 24 | }; 25 | 26 | const blockContextMenuHandlers: { [key: string]: OperationHandler } = { 27 | "Send": handleSendOperation 28 | }; 29 | 30 | async function findTask(date: Date, type: string, status: string[]) { 31 | const dateString = getDateString(date); 32 | const ret: Array | undefined = await logseq.DB.datascriptQuery(` 33 | [:find (pull ?b [*]) 34 | :where 35 | [?b :block/${type} ?d] 36 | [(= ?d ${dateString})] 37 | [?b :block/marker ?marker] 38 | [(contains? #{${status.map(s => ("\"" + s + "\"")).join(" ")}} ?marker)]] 39 | `); 40 | 41 | if (!ret) { 42 | log(`There are no tasks with ${type} for ${dateString}`); 43 | return []; 44 | } 45 | 46 | return ret.flat(); 47 | } 48 | 49 | async function handleSendOperation(bot: Telegraf, blockId: string, addId: boolean = false) { 50 | if (Object.keys(settings.chatIds).length == 0) { 51 | showError("Authorized users need to \"/register\" first"); 52 | return; 53 | } 54 | const root = await logseq.Editor.getBlock(blockId, { includeChildren: true }); 55 | if (!root) { 56 | showError("Fail to get block"); 57 | return; 58 | } 59 | 60 | const text = stringifyBlocks(root, false); 61 | const html = marked.parseInline(text); 62 | for (let key in settings.chatIds) { 63 | bot.telegram.sendMessage(settings.chatIds[key], html, { parse_mode: "HTML" }); 64 | log("Send message"); 65 | } 66 | } 67 | 68 | function setupBlockContextMenu(bot: Telegraf) { 69 | for (let key in blockContextMenuHandlers) { 70 | logseq.Editor.registerBlockContextMenuItem(`Local Telegram Bot: ${key}`, async (e) => { 71 | blockContextMenuHandlers[key](bot, e.uuid); 72 | }); 73 | } 74 | } 75 | 76 | function setupSlashCommand(bot: Telegraf) { 77 | } 78 | 79 | function startTimedJob(bot: Telegraf, name: string, time: Date) { 80 | runAtInterval(name, time, ONE_DAY_IN_SECOND, async () => { 81 | const tomorrow = new Date(); 82 | tomorrow.setDate(tomorrow.getDate() + 1); 83 | const tasks = await findTask(tomorrow, JOB_TYPES[name], ["TODO", "DOING", "NOW", "LATER", "WAITING"]); 84 | for (let task of tasks) { 85 | handleSendOperation(bot, task.uuid); 86 | } 87 | }); 88 | } 89 | 90 | function updateTimedJob(bot: Telegraf, name: string, time: Date | null) { 91 | cancelJob(name); 92 | if (time) { 93 | startTimedJob(bot, name, time); 94 | } 95 | } 96 | 97 | function startTimedJobs(bot: Telegraf) { 98 | if (settings.scheduledNotificationTime) { 99 | startTimedJob(bot, SCHEDULED_NOTIFICATION_JOB, settings.scheduledNotificationTime); 100 | } 101 | 102 | if (settings.deadlineNotificationTime) { 103 | startTimedJob(bot, DEADLINE_NOTIFICATION_JOB, settings.deadlineNotificationTime); 104 | } 105 | } 106 | 107 | function stopTimedJobs() { 108 | cancelJob(SCHEDULED_NOTIFICATION_JOB); 109 | cancelJob(DEADLINE_NOTIFICATION_JOB); 110 | } 111 | 112 | function setupMarked(bot: Telegraf) { 113 | const renderer = new marked.Renderer(); 114 | renderer.image = (href, title, text) => { 115 | return `${title ? title : "⁠"}`; 116 | }; 117 | 118 | marked.use({ renderer }); 119 | } 120 | 121 | async function startMainBot(bot: Telegraf) { 122 | try { 123 | // bot.launch can't catch all exception 124 | // use getMe first 125 | await bot.telegram.getMe(); 126 | await bot.launch(); 127 | } catch (e) { 128 | error("bot failed to launch"); 129 | showError("Bot Token is not valid"); 130 | logseq.showSettingsUI(); 131 | 132 | // rethrow to stop the process 133 | throw e; 134 | } 135 | 136 | startTimedJobs(bot); 137 | 138 | if (settings.enableCustomizedCommand) { 139 | enableCustomizedCommands(); 140 | } else { 141 | disableCustomizedCommands(); 142 | } 143 | 144 | log("bot has started as Main Bot"); 145 | } 146 | 147 | async function stopMainBot(bot: Telegraf) { 148 | disableCustomizedCommands(); 149 | stopTimedJobs(); 150 | await bot.stop(); 151 | 152 | log("bot has stopped as Main Bot"); 153 | } 154 | 155 | function setupBot(bot: Telegraf) { 156 | // command should be before message 157 | setupCommandHandlers(bot); 158 | 159 | // need this to handle photo renderer for non-Main bot 160 | setupMessageHandlers(bot); 161 | 162 | // logseq operation 163 | setupBlockContextMenu(bot); 164 | setupSlashCommand(bot); 165 | 166 | setupCommandPlayground(); 167 | 168 | // setupMarked(bot); 169 | } 170 | 171 | // this is called only when botToken is valid in format 172 | async function start(bot: Telegraf) { 173 | if (bot.token) { 174 | log("try to stop the old bot"); 175 | await stopMainBot(bot); 176 | } 177 | 178 | bot.token = settings.botToken; 179 | 180 | if (settings.enableCustomizedCommand) { 181 | enableCustomizedCommands(); 182 | } 183 | 184 | if (settings.isMainBot) { 185 | await startMainBot(bot); 186 | } 187 | 188 | log("bot is ready"); 189 | } 190 | 191 | async function main() { 192 | const bot = new Telegraf(""); 193 | 194 | // logseq.settings is NOT available until now 195 | initializeSettings((name) => { 196 | switch (name) { 197 | case nameof("botToken"): 198 | start(bot); 199 | break; 200 | 201 | case nameof("isMainBot"): 202 | if (bot.token) { 203 | if (settings.isMainBot) { 204 | startMainBot(bot); 205 | } else { 206 | stopMainBot(bot); 207 | } 208 | } 209 | break; 210 | 211 | case nameof("scheduledNotificationTime"): 212 | updateTimedJob(bot, SCHEDULED_NOTIFICATION_JOB, settings.scheduledNotificationTime); 213 | break; 214 | 215 | case nameof("deadlineNotificationTime"): 216 | updateTimedJob(bot, DEADLINE_NOTIFICATION_JOB, settings.deadlineNotificationTime); 217 | break; 218 | 219 | case nameof("enableCustomizedCommand"): 220 | if (settings.enableCustomizedCommand) { 221 | enableCustomizedCommands(); 222 | } else { 223 | disableCustomizedCommands(); 224 | } 225 | break; 226 | } 227 | }); 228 | 229 | setupBot(bot); 230 | 231 | if (!settings.botToken) { 232 | showError("Bot Token is not valid"); 233 | logseq.showSettingsUI(); 234 | return; 235 | } 236 | 237 | start(bot); 238 | } 239 | 240 | // bootstrap 241 | logseq.ready(main).catch(console.error); 242 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { SettingSchemaDesc } from "@logseq/libs/dist/LSPlugin.user"; 3 | 4 | import { showError, nameof } from "./utils"; 5 | 6 | export { Settings, settings, initializeSettings, JOURNAL_PAGE_NAME }; 7 | 8 | const JOURNAL_PAGE_NAME = "Journal"; 9 | const BOT_TOKEN_REGEX = /^[0-9]{8,10}:[a-zA-Z0-9_-]{35}$/; 10 | 11 | class Settings { 12 | constructor(onUpdate: (key: string) => void) { 13 | if (!logseq.settings!.chatIds) { 14 | logseq.updateSettings({ "chatIds": {} }); 15 | } 16 | 17 | logseq.onSettingsChanged((new_settings, old_settings) => { 18 | if (new_settings.botToken != old_settings.botToken) { 19 | if (settings.botToken) { 20 | onUpdate(nameof("botToken")); 21 | } else { 22 | showError("Bot Token is not valid"); 23 | } 24 | } 25 | 26 | if (new_settings.isMainBot != old_settings.isMainBot) { 27 | onUpdate(nameof("isMainBot")); 28 | } 29 | 30 | if (new_settings.scheduledNotificationTime != old_settings.scheduledNotificationTime) { 31 | onUpdate(nameof("scheduledNotificationTime")); 32 | } 33 | 34 | if (new_settings.deadlineNotificationTime != old_settings.deadlineNotificationTime) { 35 | onUpdate(nameof("deadlineNotificationTime")); 36 | } 37 | 38 | if (new_settings.enableCustomizedCommand != old_settings.enableCustomizedCommand) { 39 | onUpdate(nameof("enableCustomizedCommand")); 40 | } 41 | }); 42 | } 43 | 44 | // it only has 2 value 45 | // 1. valid token 46 | // 2. "" 47 | public get botToken(): string { 48 | if (!logseq.settings!.botToken.match(BOT_TOKEN_REGEX)) { 49 | return ""; 50 | } 51 | 52 | return logseq.settings!.botToken; 53 | } 54 | 55 | public get isMainBot(): boolean { 56 | return logseq.settings!.isMainBot; 57 | } 58 | 59 | public get authorizedUsers(): string[] { 60 | return logseq.settings!.authorizedUsers.split(",").map((rawUserName: string) => rawUserName.trim()); 61 | } 62 | 63 | public get pageName(): string { 64 | if (!logseq.settings!.pageName) { 65 | logseq.settings!.pageName = JOURNAL_PAGE_NAME; 66 | } 67 | 68 | return logseq.settings!.pageName; 69 | } 70 | 71 | public get inboxName(): string { 72 | return logseq.settings!.inboxName; 73 | } 74 | 75 | public get appendAtBottom(): boolean { 76 | return logseq.settings!.appendAtBottom; 77 | } 78 | 79 | public get addTimestamp(): boolean { 80 | return logseq.settings!.addTimestamp; 81 | } 82 | 83 | public get scheduledNotificationTime() { 84 | if (this.isMainBot && logseq.settings!.scheduledNotificationTime) { 85 | return new Date(logseq.settings!.scheduledNotificationTime); 86 | } else { 87 | return null; 88 | } 89 | } 90 | 91 | public get deadlineNotificationTime() { 92 | if (this.isMainBot && logseq.settings!.deadlineNotificationTime) { 93 | return new Date(logseq.settings!.deadlineNotificationTime); 94 | } else { 95 | return null; 96 | } 97 | } 98 | 99 | public get enableCustomizedCommand(): boolean { 100 | return this.isMainBot && logseq.settings!.enableCustomizedCommand; 101 | } 102 | 103 | public get enableCustomizedCommandFromMessage(): boolean { 104 | return this.enableCustomizedCommand && logseq.settings!.enableCustomizedCommandFromMessage; 105 | } 106 | 107 | // below are internal persistent data 108 | 109 | // key: userName 110 | // value: chatId 111 | public get chatIds(): { [key: string]: number } { 112 | const users = this.authorizedUsers; 113 | let chatIds = logseq.settings!.chatIds; 114 | for (let key in chatIds) { 115 | if (!users.includes(key)) { 116 | delete chatIds[key]; 117 | } 118 | } 119 | this.chatIds = chatIds; 120 | return logseq.settings!.chatIds; 121 | } 122 | public set chatIds(ids: { [key: string]: number }) { 123 | // it's a bug to update settings for array/object type 124 | // need to set it to something else before updating it 125 | logseq.updateSettings({ "chatIds": null }); 126 | logseq.updateSettings({ "chatIds": ids }); 127 | } 128 | } 129 | 130 | let settings: Settings; 131 | 132 | const settingsSchema: SettingSchemaDesc[] = [ 133 | { 134 | key: "botToken", 135 | description: "Telegram Bot token. In order to start you need to create Telegram bot: https://core.telegram.org/bots#3-how-do-i-create-a-bot. Create a bot with BotFather, which is essentially a bot used to create other bots. The command you need is /newbot. After you choose title, BotFaher give you the token", 136 | type: "string", 137 | default: "", 138 | title: "Bot Token", 139 | }, 140 | { 141 | key: "isMainBot", 142 | description: "If you have multiple Logseq open at the same time, probably from different devices, only one should be set to true, to avoid conflicts of multiple bots running together", 143 | type: "boolean", 144 | default: false, 145 | title: "Is Main Bot", 146 | }, 147 | { 148 | key: "authorizedUsers", 149 | description: "Be sure to add your username in authorizedUsers, because your recently created bot is publicly findable and other peoples may send messages to your bot. If there are multiple usernames, separate them by \",\", with optional leading or trailing space, like \"your_username1, your_username2\". If you leave this empty - all messages from all users will be processed!", 150 | type: "string", 151 | default: "", 152 | title: "Authorized Users", 153 | }, 154 | { 155 | key: "pageName", 156 | description: "The name of the page that all regular messages from Telegram are added to. \"Journal\" is reserved for today's Journal, and it's the default. The page should be available.", 157 | type: "string", 158 | default: "Journal", 159 | title: "Page Name", 160 | }, 161 | { 162 | key: "inboxName", 163 | description: "The content of the block that all regular messages from Telegram are added to. If it's not available, a new Inbox will be created at the end of target page. If its value is empty, the messages will be added to the target page", 164 | type: "string", 165 | default: "#Inbox", 166 | title: "Inbox Name", 167 | }, 168 | { 169 | key: "appendAtBottom", 170 | description: "If it's set to true, the new messages will be appended at the bottom of Inbox, instead of the top.", 171 | type: "boolean", 172 | default: false, 173 | title: "Append At Bottom", 174 | }, 175 | { 176 | key: "addTimestamp", 177 | description: "If it's set to true, message received time in format HH:mm will be added to the front of message text", 178 | type: "boolean", 179 | default: false, 180 | title: "Add Timestamp", 181 | }, 182 | { 183 | key: "scheduledNotificationTime", 184 | description: "The local time of notificaiton for not-done task with scheduled date. The message should be sent one day before the scheduled at this specific time. Clearing it disable this feature. It's only enabled for main bot", 185 | type: "string", 186 | default: "", 187 | title: "Scheduled Notification Time", 188 | inputAs: "datetime-local" 189 | }, 190 | { 191 | key: "deadlineNotificationTime", 192 | description: "The local time of notificaiton for not-done task with deadline date. The message should be sent one day before the deadline at this specific time. Clearing it disables this feature. It's only enabled for main bot", 193 | type: "string", 194 | default: "", 195 | title: "Deadline Notification Time", 196 | inputAs: "datetime-local" 197 | }, 198 | { 199 | key: "enableCustomizedCommand", 200 | description: "Whether to enable customized command mode, which enables eligible users to run native js/ts or query datascript. **This is still experimenting**", 201 | type: "boolean", 202 | default: false, 203 | title: "Enable Customized Command", 204 | }, 205 | { 206 | key: "enableCustomizedCommandFromMessage", 207 | description: "Whether to allow messages to include customized command, which enables eligible users to add new commands from Telegram. **This is still experimenting**", 208 | type: "boolean", 209 | default: false, 210 | title: "Enable Customized Command From Message", 211 | } 212 | ]; 213 | 214 | function initializeSettings(onUpdate: (key: string) => void) { 215 | logseq.useSettingsSchema(settingsSchema); 216 | settings = new Settings(onUpdate); 217 | } -------------------------------------------------------------------------------- /src/message-handlers.ts: -------------------------------------------------------------------------------- 1 | import { PageEntity, BlockEntity } from "@logseq/libs/dist/LSPlugin.user"; 2 | 3 | import { Telegraf, Context } from "telegraf"; 4 | import { MessageSubTypes } from "telegraf/typings/telegram-types"; 5 | import { Message, MessageEntity } from "typegram"; 6 | 7 | import { log, getDateString, getTimestampString, isMessageAuthorized } from "./utils"; 8 | import { settings, JOURNAL_PAGE_NAME } from "./settings"; 9 | 10 | export { setupMessageHandlers }; 11 | 12 | type MessageHandler = (ctx: Context, message: Message.ServiceMessage) => Promise; 13 | type EntityHandler = (text: string, entity: MessageEntity) => string; 14 | 15 | // FIXME: it matches all showPhoto, instead of current one 16 | const SHOW_PHOTO_RENDERER_REGEX = /{{renderer :local_telegram_bot-showPhoto[^}]*}}!\[[^\]]*\]\([^\)]*\)/; 17 | const DEFAULT_CAPTION = "no caption"; 18 | 19 | const entityHandlers: { [ type: string ]: EntityHandler } = { 20 | "pre": handleCodeEntity, 21 | "code": handleCodeEntity 22 | }; 23 | 24 | async function findPage(pageName: string): Promise { 25 | if (pageName != JOURNAL_PAGE_NAME) { 26 | return logseq.Editor.getPageBlocksTree(pageName); 27 | } 28 | 29 | const todayDate = getDateString(new Date()); 30 | const ret: Array | undefined = await logseq.DB.datascriptQuery(` 31 | [:find (pull ?p [*]) 32 | :where 33 | [?b :block/page ?p] 34 | [?p :block/journal? true] 35 | [?p :block/journal-day ?d] 36 | [(= ?d ${todayDate})]] 37 | `); 38 | 39 | if (!ret) { 40 | log("Today's Journal is not available"); 41 | return []; 42 | } 43 | 44 | const pages = ret.flat(); 45 | if (pages.length == 0 || !pages[0].name) { 46 | log("Today's Journal is not available"); 47 | return []; 48 | } 49 | 50 | return logseq.Editor.getPageBlocksTree(pages[0].name);; 51 | } 52 | 53 | async function writeBlock(pageName: string, inboxName: string, text: string): Promise { 54 | const pageBlocksTree = await findPage(pageName); 55 | if (!pageBlocksTree || pageBlocksTree.length == 0) { 56 | log("Request page is not available"); 57 | return false; 58 | } 59 | 60 | let inboxBlock: BlockEntity | undefined | null = settings.appendAtBottom 61 | ? pageBlocksTree[pageBlocksTree.length - 1] 62 | : pageBlocksTree[0]; 63 | 64 | if (inboxName) { 65 | inboxBlock = pageBlocksTree.find((block: { content: string }) => { 66 | return block.content === inboxName; 67 | }); 68 | if (!inboxBlock) { 69 | inboxBlock = await logseq.Editor.insertBlock( 70 | pageBlocksTree[pageBlocksTree.length - 1].uuid, 71 | inboxName, 72 | { 73 | before: false, 74 | sibling: true 75 | } 76 | ); 77 | } 78 | } 79 | 80 | if (!inboxBlock) { 81 | log(`Unable to find Inbox: ${inboxName}`); 82 | return false; 83 | } 84 | 85 | const params = { before: !settings.appendAtBottom, sibling: !inboxName }; 86 | await logseq.Editor.insertBlock(inboxBlock.uuid, text, params); 87 | return true; 88 | } 89 | 90 | function handleCodeEntity(text: string, entity: MessageEntity): string { 91 | let code = "`"; 92 | if (text.indexOf("\n") > 0) { 93 | code = "```"; 94 | } 95 | 96 | return code + text + code; 97 | } 98 | 99 | function handleEntity(text: string, entity: MessageEntity): string { 100 | if (entityHandlers[entity.type]) { 101 | text = entityHandlers[entity.type](text, entity); 102 | } 103 | 104 | return text; 105 | } 106 | 107 | function textHandlerGenerator() { 108 | async function handler(ctx: Context, message: Message.TextMessage) { 109 | if (!message?.text) { 110 | ctx.reply("Message is not valid"); 111 | return; 112 | } 113 | 114 | let text = message.text; 115 | 116 | if (message.entities) { 117 | message.entities.sort((a, b) => a.offset - b.offset); 118 | let subs: string[] = []; 119 | let offset = 0; 120 | for (let entity of message.entities) { 121 | subs.push(text.substring(offset, entity.offset)); 122 | let sub = text.substring(entity.offset, entity.offset + entity.length); 123 | subs.push(handleEntity(sub, entity)); 124 | offset = entity.offset + entity.length; 125 | } 126 | 127 | if (offset < text.length) { 128 | subs.push(text.substring(offset)); 129 | } 130 | 131 | text = subs.join(""); 132 | } 133 | 134 | if (settings.addTimestamp) { 135 | const receiveDate = new Date(); 136 | receiveDate.setTime(message.date * 1000); 137 | 138 | text = `${getTimestampString(receiveDate)} - ${text}`; 139 | } 140 | 141 | if (!await writeBlock( 142 | settings.pageName, 143 | settings.inboxName, 144 | text)) { 145 | ctx.reply("Failed to write this to Logseq"); 146 | } 147 | } 148 | 149 | return { 150 | type: "text", 151 | handler: handler as MessageHandler 152 | }; 153 | } 154 | 155 | function photoTemplate(caption: string, id: string, url: string) { 156 | return `{{renderer :local_telegram_bot-showPhoto,${caption},${id}}}![${caption}](${url})`; 157 | } 158 | 159 | function photoHandlerGenerator(bot: Telegraf) { 160 | async function handler(ctx: Context, message: Message.PhotoMessage) { 161 | if (!message?.photo || message.photo.length == 0) { 162 | ctx.reply("Photo is not valid"); 163 | return; 164 | } 165 | 166 | const lastPhoto = message.photo[message.photo.length - 1]; 167 | const photoUrl = await ctx.telegram.getFileLink(lastPhoto.file_id); 168 | const caption = message.caption ?? DEFAULT_CAPTION; 169 | let text = photoTemplate(caption, lastPhoto.file_id, photoUrl); 170 | if (settings.addTimestamp) { 171 | const receiveDate = new Date(); 172 | receiveDate.setTime(message.date * 1000); 173 | 174 | text = `${getTimestampString(receiveDate)} - ${text}`; 175 | } 176 | 177 | if (!await writeBlock( 178 | settings.pageName, 179 | settings.inboxName, 180 | text)) { 181 | ctx.reply("Failed to write this to Logseq"); 182 | } 183 | } 184 | 185 | logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => { 186 | let [type, caption, photoId] = payload.arguments; 187 | // backward compatibility 188 | if (type !== ':local_telegram_bot' && type !== ":local_telegram_bot-showPhoto") { 189 | return; 190 | } 191 | 192 | const block = await logseq.Editor.getBlock(payload.uuid); 193 | if (!block) { 194 | log(`fail to get block(${payload.uuid})`); 195 | return; 196 | } 197 | 198 | const photoUrl = await bot.telegram.getFileLink(photoId); 199 | const content = block.content.replace( 200 | SHOW_PHOTO_RENDERER_REGEX, 201 | photoTemplate(caption, photoId, photoUrl)); 202 | 203 | // replace the whole block with new renderer and img 204 | // renderer runs once at one time, so no loop 205 | // invalid renderer removes itself from rendering 206 | // photo url from Telegram is not permanent, need to fetch everytime 207 | logseq.Editor.updateBlock(payload.uuid, content); 208 | }); 209 | 210 | return { 211 | type: "photo", 212 | handler: handler as MessageHandler 213 | }; 214 | } 215 | 216 | function documentHandlerGenerator(bot: Telegraf) { 217 | async function handler(ctx: Context, message: Message.DocumentMessage) { 218 | if (!message.document.mime_type?.startsWith("image/")) { 219 | log(`document mime_type is not image: ${message.document.mime_type}`); 220 | return; 221 | } 222 | 223 | const photoUrl = await ctx.telegram.getFileLink(message.document.file_id); 224 | const caption = message.caption ?? DEFAULT_CAPTION; 225 | let text = photoTemplate(caption, message.document.file_id, photoUrl); 226 | if (settings.addTimestamp) { 227 | const receiveDate = new Date(); 228 | receiveDate.setTime(message.date * 1000); 229 | 230 | text = `${getTimestampString(receiveDate)} - ${text}`; 231 | } 232 | 233 | if (!await writeBlock( 234 | settings.pageName, 235 | settings.inboxName, 236 | text)) { 237 | ctx.reply("Failed to write this to Logseq"); 238 | } 239 | } 240 | return { 241 | type: "document", 242 | handler: handler as MessageHandler 243 | }; 244 | } 245 | 246 | function setupMessageHandlers(bot: Telegraf) { 247 | const messageHandlers: { type: string, handler: MessageHandler }[] = [ 248 | textHandlerGenerator(), 249 | photoHandlerGenerator(bot), 250 | documentHandlerGenerator(bot) 251 | ]; 252 | 253 | for (let handler of messageHandlers) { 254 | // FIXME: no way to check union type? 255 | bot.on(handler.type as MessageSubTypes, (ctx) => { 256 | if (ctx.message 257 | && isMessageAuthorized(ctx.message as Message.ServiceMessage)) { 258 | handler.handler(ctx, ctx.message); 259 | } 260 | }); 261 | } 262 | } -------------------------------------------------------------------------------- /src/command-handlers.ts: -------------------------------------------------------------------------------- 1 | import { IUserOffHook } from "@logseq/libs/dist/LSPlugin.user"; 2 | 3 | import { Telegraf, Context } from "telegraf"; 4 | import { Message } from "typegram"; 5 | import stringArgv from "string-argv"; 6 | import minimist from "minimist"; 7 | import { marked } from "marked"; 8 | 9 | import { settings } from "./settings"; 10 | import { Command, parseCommand, runCommand, setupSlashCommands, commandInfos, COMMAND_PAGE_NAME } from "./command-utils"; 11 | import { isMessageAuthorized, log, error, stringifyBlocks } from "./utils"; 12 | 13 | export { setupCommandHandlers, enableCustomizedCommands, disableCustomizedCommands }; 14 | 15 | interface CommandHandler { 16 | type: string; 17 | description: string; 18 | handler: (ctx: Context) => Promise; 19 | }; 20 | 21 | const builtinCommandHandlers: CommandHandler[] = [ 22 | // getHandlerGenerator(), 23 | // updateTaskHandlerGenerator(), 24 | helpHandlerGenerator() 25 | ]; 26 | 27 | const customizedCommandHandlers: CommandHandler[] = []; 28 | 29 | const commands = new Map; 30 | 31 | async function updateCustomizedCommands() { 32 | // FIXME: no need to clear everytime 33 | commands.clear(); 34 | 35 | const refs = await logseq.Editor.getPageLinkedReferences(COMMAND_PAGE_NAME); 36 | if (!refs) { 37 | log(`no customized commands`); 38 | return; 39 | } 40 | 41 | for (let ref of refs) { 42 | for (let block of ref[1]) { 43 | const command = parseCommand(block.content); 44 | if (!command) { 45 | continue; 46 | } 47 | 48 | if (!commands.has(command.type)) { 49 | commands.set(command.type, {}); 50 | } 51 | 52 | commands.get(command.type)![command.name] = command; 53 | } 54 | } 55 | } 56 | 57 | function handleArgs(key: string, args: string): { command: Command, argv: string[] } | null { 58 | if (!commands.has(key)) { 59 | return null; 60 | } 61 | 62 | const cmds = commands.get(key)!; 63 | 64 | const argv = minimist(stringArgv(args))._; 65 | if (!cmds[argv[0]]) { 66 | return null; 67 | } 68 | 69 | return { 70 | command: cmds[argv[0]], 71 | argv: argv 72 | } 73 | } 74 | 75 | async function processCommand(type: string, ctx: Context) { 76 | const prefix = `/${type}`; 77 | const text = ctx.message!.text; 78 | 79 | // this should never happen 80 | if (!text.startsWith(prefix)) { 81 | error(`invalid command: ${type}: ${text}`); 82 | ctx.reply("not a valid command"); 83 | return; 84 | } 85 | 86 | const args = text.substring(prefix.length + 1); 87 | const h = handleArgs(type, args); 88 | if (!h) { 89 | ctx.reply("not a valid command"); 90 | return; 91 | } 92 | 93 | await handleCommand(h.command, h.argv, ctx); 94 | } 95 | 96 | async function handleCommand(command: Command, argv: string[], ctx: Context) { 97 | if (!command.script) { 98 | ctx.reply("not a valid command"); 99 | return; 100 | } 101 | 102 | try { 103 | const result = await runCommand(command, argv.slice(1)); 104 | if (result?.result != undefined && result?.result != null) { 105 | // FIXME: maximum size of message is 4k 106 | // how to enable users to copy uuid? 107 | const msg = JSON.stringify(result.result, null, 2); 108 | const html = marked.parseInline(msg); 109 | await ctx.reply(html, { parse_mode: "HTML" }); 110 | } else if (result?.logs && result?.logs.length > 0) { 111 | ctx.reply(JSON.stringify(result.logs, null, 2)); 112 | } else { 113 | ctx.reply("unknown error"); 114 | } 115 | } catch (e) { 116 | ctx.reply((e).message); 117 | } 118 | } 119 | 120 | function setupCustomizedCommandHandlers() { 121 | for (let info of commandInfos) { 122 | customizedCommandHandlers.push({ 123 | type: info.type, 124 | description: info.description, 125 | handler: (ctx: Context) => { 126 | return processCommand(info.type, ctx); 127 | } 128 | }); 129 | } 130 | } 131 | 132 | function getHandlerGenerator() { 133 | const type = "get"; 134 | return { 135 | type: type, 136 | description: "uuid. Get block content from uuid", 137 | handler: async (ctx: Context) => { 138 | const text = ctx.message!.text; 139 | const parts = text.split(" "); 140 | if (parts.length < 2) { 141 | ctx.reply("invalid command"); 142 | return; 143 | } 144 | 145 | const block = await logseq.Editor.getBlock(parts[1], { includeChildren: true }); 146 | if (!block) { 147 | ctx.reply("Block is not available"); 148 | return; 149 | } 150 | 151 | const html = marked.parseInline(stringifyBlocks(block, true)); 152 | await ctx.reply(html, { parse_mode: "HTML" }); 153 | } 154 | } 155 | } 156 | 157 | function updateTaskHandlerGenerator() { 158 | const type = "updateTask"; 159 | return { 160 | type: type, 161 | description: "uuid status. Update task status", 162 | handler: async (ctx: Context) => { 163 | const text = ctx.message!.text; 164 | const parts = text.split(" "); 165 | if (parts.length < 3) { 166 | ctx.reply("invalid command"); 167 | return; 168 | } 169 | 170 | const status = parts[2].toUpperCase(); 171 | if (!["TODO", "DOING", "DONE", "NOW", "LATER", "WAITING"].includes(status)) { 172 | ctx.reply("invalid status"); 173 | return; 174 | } 175 | 176 | const block = await logseq.Editor.getBlock(parts[1]); 177 | if (!block || !block["marker"]) { 178 | ctx.reply("invalid task"); 179 | return; 180 | } 181 | 182 | const content = status + block.content.substring(block.content.indexOf(" ")); 183 | await logseq.Editor.updateBlock(block.uuid, content); 184 | const html = marked.parseInline(content); 185 | await ctx.reply(html, { parse_mode: "HTML" }); 186 | } 187 | } 188 | } 189 | 190 | function helpHandlerGenerator() { 191 | return { 192 | type: "help", 193 | description: "List all available commands", 194 | handler: async (ctx: Context) => { 195 | let msg = "Available commands:\n"; 196 | for (let { type, description, handler } of builtinCommandHandlers) { 197 | msg += `/${type}: ${description}\n`; 198 | } 199 | 200 | if (settings.enableCustomizedCommand) { 201 | for (let { type, description, handler } of customizedCommandHandlers) { 202 | msg += `/${type}: ${description}\n`; 203 | } 204 | 205 | if (commands.size > 0) { 206 | msg += "\nCustomized commands:\n"; 207 | commands.forEach((cmds, type) => { 208 | for (let subType in cmds) { 209 | const cmd = cmds[subType]; 210 | msg += `[/${type} ${subType}|/${subType}] ${cmd.params.join(" ")}: ${cmd.description}\n` 211 | } 212 | }); 213 | } 214 | } 215 | 216 | ctx.reply(msg); 217 | } 218 | } 219 | } 220 | 221 | function setupCommandMiddleware(bot: Telegraf) { 222 | bot.use((ctx, next) => { 223 | if (!ctx.message?.text) { 224 | next(); 225 | return; 226 | } 227 | 228 | const text = ctx.message.text; 229 | if (!settings.enableCustomizedCommandFromMessage) { 230 | for (let info of commandInfos) { 231 | if (text.startsWith(info.pageName)) { 232 | log(`command is not allowed in message: ${text}`); 233 | ctx.reply("Command is not allowed in message"); 234 | return; 235 | } 236 | } 237 | } 238 | 239 | if (text.startsWith("/")) { 240 | let handled = false; 241 | commands.forEach((_, type) => { 242 | if (handled) { 243 | return; 244 | } 245 | 246 | const h = handleArgs(type, text.substring(1)); 247 | if (!h) { 248 | return; 249 | } 250 | 251 | handled = true; 252 | handleCommand(h.command, h.argv, ctx); 253 | }); 254 | 255 | if (handled) { 256 | return; 257 | } 258 | } 259 | 260 | next(); 261 | }); 262 | } 263 | 264 | let unsubscribe: IUserOffHook = () => { }; 265 | 266 | function setupCommandHandlers(bot: Telegraf) { 267 | setupCustomizedCommandHandlers(); 268 | 269 | for (let handler of [...builtinCommandHandlers, ...customizedCommandHandlers]) { 270 | bot.command(handler.type, (ctx) => { 271 | if (ctx.message 272 | && isMessageAuthorized(ctx.message as Message.ServiceMessage)) { 273 | handler.handler(ctx); 274 | } 275 | }); 276 | } 277 | 278 | setupCommandMiddleware(bot); 279 | setupSlashCommands(); 280 | } 281 | 282 | function enableCustomizedCommands() { 283 | unsubscribe(); 284 | unsubscribe = logseq.DB.onChanged((e) => { 285 | updateCustomizedCommands(); 286 | }); 287 | updateCustomizedCommands(); 288 | log("customized commands are enabled"); 289 | } 290 | 291 | function disableCustomizedCommands() { 292 | unsubscribe(); 293 | commands.clear(); 294 | log("customized commands are disabled"); 295 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2019", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | --------------------------------------------------------------------------------