{"id":23771625,"url":"https://github.com/michurin/cnbot","last_synced_at":"2025-10-04T21:53:45.391Z","repository":{"id":48472061,"uuid":"133980292","full_name":"michurin/cnbot","owner":"michurin","description":"The tool to build your custom Telegram bot easier than ever","archived":false,"fork":false,"pushed_at":"2025-09-14T05:46:18.000Z","size":1522,"stargazers_count":34,"open_issues_count":0,"forks_count":6,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-14T07:17:39.225Z","etag":null,"topics":["bot","bot-framework","bots","go","golang","telegram","telegram-bot"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/michurin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2018-05-18T16:42:38.000Z","updated_at":"2025-09-14T05:46:21.000Z","dependencies_parsed_at":"2024-06-19T01:09:11.377Z","dependency_job_id":"b186ab65-ebe0-46b3-b704-2e8c1bb71c4f","html_url":"https://github.com/michurin/cnbot","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/michurin/cnbot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michurin%2Fcnbot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michurin%2Fcnbot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michurin%2Fcnbot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michurin%2Fcnbot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michurin","download_url":"https://codeload.github.com/michurin/cnbot/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michurin%2Fcnbot/sbom","scorecard":{"id":642146,"data":{"date":"2025-08-11","repo":{"name":"github.com/michurin/cnbot","commit":"6320c6f19a49c24e9ab68689770798dc3f644639"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.4,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/29 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"1 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ci.yaml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yaml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/michurin/cnbot/ci.yaml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yaml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/michurin/cnbot/ci.yaml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yaml:28: update your workflow using https://app.stepsecurity.io/secureworkflow/michurin/cnbot/ci.yaml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ci.yaml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/michurin/cnbot/ci.yaml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yaml:32: update your workflow using https://app.stepsecurity.io/secureworkflow/michurin/cnbot/ci.yaml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ci.yaml:39: update your workflow using https://app.stepsecurity.io/secureworkflow/michurin/cnbot/ci.yaml/master?enable=pin","Warn: containerImage not pinned by hash: demo/Dockerfile:1: pin your Docker image by updating debian:trixie-slim to debian:trixie-slim@sha256:c85a2732e97694ea77237c61304b3bb410e0e961dd6ee945997a06c788c545bb","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 third-party GitHubAction dependencies pinned","Info:   0 out of   1 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 2 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T11:05:42.481Z","repository_id":48472061,"created_at":"2025-08-21T11:05:42.481Z","updated_at":"2025-08-21T11:05:42.481Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278380462,"owners_count":25977216,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-04T02:00:05.491Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot","bot-framework","bots","go","golang","telegram","telegram-bot"],"created_at":"2025-01-01T04:20:13.589Z","updated_at":"2025-10-04T21:53:45.378Z","avatar_url":"https://github.com/michurin.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cnbot\n\n[![build](https://github.com/michurin/cnbot/actions/workflows/ci.yaml/badge.svg)](https://github.com/michurin/cnbot/actions/workflows/ci.yaml)\n[![codecov](https://codecov.io/gh/michurin/cnbot/graph/badge.svg?token=3GdCf3TqZC)](https://codecov.io/gh/michurin/cnbot)\n[![Go Report Card](https://goreportcard.com/badge/github.com/michurin/cnbot)](https://goreportcard.com/report/github.com/michurin/cnbot)\n\nThe goal of this project is to provide a way\nto alive Telegram bots by scripting that\neven simpler than CGI scripts.\nAll you need to write is a script (on any language)\nthat is complying with extremely simple contract.\n\n![Telegram bot demo screenshot](https://raw.githubusercontent.com/michurin/cnbot/static/screenshot-2024.gif)\n\n## What is it for\n\nThis bot engine has proven itself in alerting, system monitoring and managing tasks.\n\nIt also good for prototyping and fast proofing ideas.\n\n## How mature is it\n\nThe engine is not perfect. Some error messages could be more informative.\nSomewhere you can face a lug of documentation and the need to appeal to source code.\n\nHowever, the engine has already proven itself in production and prototyping.\n\nIt served bots for huge conferences, meetings and events. It has helped customers\nand provided control functionality for crew.\n\nThe engine successfully drives several monitoring and alerting bots.\n\nIt seems, API of this bot engines is quite stable and won't change dramatically in the near future.\n\n## Basic ideas\n\nYou implement all your business logic in your scripts. You are totally free to use all Telegram API abilities.\n\n`cnbot` interact with scripts using (i) `stdout` stream, (ii) arguments and (iii) environment variables.\n\nThe engine automatically recognize multimedia and images. It cares about concurrency and races.\n\nIt also provides simple API for asynchronous messaging from `cron`s and such things.\n\nIt manages tasks (subprocesses), controls timeouts, sends signals and provides abilities to\nrun long-running tasks like long image/video conversions and/or downloading.\n\nOne instance of engine is able to manage several different bots.\n\n## Quick start\n\n### Zero-effort Docker-way to run full-featured bot\n\nAll you need is bot token ([instructions](https://core.telegram.org/bots#how-do-i-create-a-bot)).\n\n```sh\ndocker build -t cnbot:latest https://raw.githubusercontent.com/michurin/cnbot/master/demo/Dockerfile\ndocker run -it --rm --name cnbot -e TB_TOKEN=4839574812:AAFD39kkdpWt3ywyRZergyOLMaJhac60qc cnbot:latest\n```\n\n[More details](https://github.com/michurin/cnbot/tree/master/demo)\n\n### Run simplest one-line bot\n\n#### Prepare\n\nFirst things first, you need to create bot and get it's token.\nIt is free, just follow [instructions](https://core.telegram.org/bots#how-do-i-create-a-bot).\n\n#### Build and run\n\nYou need Telegram API token, `golang` and standard system commands `echo` and `true`.\n\n```sh\ngo install github.com/michurin/cnbot/cmd/...@latest\ntb_token='4839574812:AAFD39kkdpWt3ywyRZergyOLMaJhac60qc' tb_script=echo tb_long_running_script=true tb_ctrl_addr=:9999 cnbot\n```\n\nor without installation:\n\n```sh\ngit clone https://github.com/michurin/cnbot\ncd cnbot\ntb_token='4839574812:AAFD39kkdpWt3ywyRZergyOLMaJhac60qc' tb_script=echo tb_long_running_script=true tb_ctrl_addr=:9999 go run ./cmd/...\n```\n\nYou are free to keep your token in file and use syntax like this to refer to file: `tb_token=@filename`\n\nDon't worry, we will use configuration file further. The engine is able to use both files and direct environment variables.\n\n- `tb_YOURBOTNAME_token` is a token your are given: `digits:long_string`\n- `tb_YOURBOTNAME_script` is a command to run. We use the standard system command `echo`. I can be located elsewhere in your system. Try to say `whereis echo` to fine it\n- `tb_YOURBOTNAME_long_running_script` let it be the same command. We consider it later\n- `tb_YOURBOTNAME_ctrl_addr` we consider it soon\n\nRun this command with correct variables and try to say something to you bot. You will be echoed by it.\n\n### Put your configuration into file\n\nYou may as well put your configuration into env-file. The format of file is literally the same as `systemd` use.\nSo you are able to load it in `systemd` files as well. For example:\n\n```sh\n# let's name it config.env\ntb_token='TOKEN'\ntb_script=/usr/bin/echo\ntb_long_running_script=/usr/bin/echo\ntb_ctrl_addr=:9999\n```\n\nNow just start bot like this:\n\n```sh\ncnbot config.env\n```\n\n## Playing with random features\n\n### Your first script (finding out your UserID)\n\nLet's look at the script, that shows its arguments and environment variables:\n\n```sh\n#!/bin/sh\n\necho \"Args: $@\"\necho \"Environment:\"\nenv | grep tg_ | sort\n```\n\nName it `mybot.sh` and mention it in configuration variable `tb_script=./mybot.sh`. Restart the bot and say to it `Hello bot!`.\nIt will reply to you something like that:\n\n```\n╭─────────────────────────────────────────╮\n│ Args: hello bot!                        │\n│ Environment:                            │\n│ tg_message_chat_first_name=Alexey       │\n│ tg_message_chat_id=153333328            │\n│ tg_message_chat_last_name=Michurin      │\n│ tg_message_chat_type=private            │\n│ tg_message_chat_username=AlexeyMichurin │\n│ tg_message_date=1717171717              │\n│ tg_message_from_first_name=Alexey       │\n│ tg_message_from_id=153333328            │\n│ tg_message_from_is_bot=false            │\n│ tg_message_from_language_code=en        │\n│ tg_message_from_last_name=Michurin      │\n│ tg_message_from_username=AlexeyMichurin │\n│ tg_message_message_id=4554              │\n│ tg_message_text=Hello bot!              │\n│ tg_update_id=513333387                  │\n│ tg_x_build=development (devel)          │\n│ tg_x_ctrl_addr=:9999                    │\n╰─────────────────────────────────────────╯\n```\n\nYou can see that your message has been put to arguments in convenient normalized form, and you have a bunch of useful variables\nwith additional information. We will consider them further. At this point we just figure out that our user id is `tg_message_from_id=153333328`.\nWe will use this information very soon.\n\n### Asynchronous messaging\n\nYou are free to send messages from anywhere: from cron jobs, from init scripts... Try it just from command line:\n\n```sh\ncurl -qs http://localhost:9999/?to=153333328 -d 'OK!'\n```\n\nIf you bot is running, you will obtain the message `OK!` in you Telegram client.\n\n```\n╭──────────╮\n│ OK!      │\n╰──────────╯\n```\n\nDo not forget to use *your* user id from previous section.\n\nIt makes sense what variable `tb_ctrl_addr=:9999` is for. It defines a control interface for external interactions with bot engine.\n\n### Call arbitrary Telegram API methods\n\nYou can call whatever method you want. Full list of methods can be found in the\n[official Telegram bot API documentation](https://core.telegram.org/bots/api).\n\nFor example, you can obtain information about your bot\n(using method [getMe](https://core.telegram.org/bots/api#getme)):\n\n```sh\ncurl -qs http://localhost:9999/method/getMe | jq\n```\n\nThe response will look like this:\n\n```json\n{\n  \"ok\": true,\n  \"result\": {\n    \"id\": 223333386,\n    \"is_bot\": true,\n    \"first_name\": \"Your Bot\",\n    \"username\": \"your_bot\",\n    \"can_join_groups\": true,\n    \"can_read_all_group_messages\": false,\n    \"supports_inline_queries\": false,\n    \"can_connect_to_business\": false\n  }\n}\n```\n\nIt enables you to send extended messages. For example, you can send a message with buttons\n(method [sendMessage](https://core.telegram.org/bots/api#sendmessage)):\n\n```sh\ncurl -qs http://localhost:9999/sendMessage -F chat_id=153333328 -F text='Select search engine' -F reply_markup='{\"inline_keyboard\":[[{\"text\":\"Google\",\"url\":\"https://www.google.com/\"}, {\"text\":\"DuckDuckGo\",\"url\":\"https://duckduckgo.com/\"}]]}'\n```\n\nYou will receive message with two clickable buttons:\n\n```\n╭───────────────────────────╮\n│ Select search engine      │\n├─────────────┬─────────────┤\n│ Google     ↗│ DuckDuckGo ↗│\n╰─────────────┴─────────────╯\n```\n\nDo not forget to change `user_id`.\n\n\u003e [!NOTE]\n\u003e You can use any prefixes in URLs.\n\u003e URLs `http://localhost:9999/sendMessage` and `http://localhost:9999/ANITHING/sendMessage` are equal.\n\u003e It allows you to put engine's API behind prefix.\n\n### Sending images\n\nBot recognizes media type of input. It will send text:\n\n```sh\necho 'Hello!' | curl -qs http://localhost:9999/?to=153333328 --data-binary '@-'\n```\n\nHowever, it will send you image:\n\n```sh\ncurl -qs https://github.githubassets.com/favicons/favicon.png | curl -qs http://localhost:9999/?to=153333328 --data-binary '@-'\n```\n\n\u003e [!IMPORTANT]\n\u003e Please use the `--data-binary` option for binary data. Option `-d` corrupts EOLs.\n\n### Formatted text\n\n```sh\n(echo '%!PRE'; echo 'Hello!') | curl -qs http://localhost:9999/?to=153333328 --data-binary '@-'\n```\n\n## Big picture\n\n### Prepare playground\n\nLet's extend our `mybot.sh` like that (it is literally [demo script](demo/demo_bot.sh) you can run by [docker compose](demo/compose.yaml)):\n\n```sh\n#!/bin/bash\n\nLOG=logs/log.log # /dev/null\n\nFROM=\"$tg_message_from_id\"\n\nAPI() {\n    API_STDOUT \"$@\" \u003e\u003e\"$LOG\"\n}\n\nAPI_STDOUT() {\n    url=\"http://localhost$tg_x_ctrl_addr/$1\"\n    shift\n    echo \"====== curl $url $@\" \u003e\u003e\"$LOG\"\n    curl -qs \"$url\" \"$@\" 2\u003e\u003e\"$LOG\"\n    echo \u003e\u003e\"$LOG\"\n    echo \u003e\u003e\"$LOG\"\n}\n\n(\n    echo '==================='\n    echo \"Args: $@\"\n    echo \"Environment:\"\n    env | grep tg_ | sort\n    echo '...................'\n) \u003e\u003e\"$LOG\"\n\ncase \"$1\" in\n    debug)\n        echo '%!PRE'\n        echo \"Args: $@\"\n        echo \"Environment:\"\n        env | grep tg_ | sort\n        echo \"FROM=$FROM\"\n        echo \"LOG=$LOG\"\n        ;;\n    about)\n        echo '%!PRE'\n        API_STDOUT getMe | jq\n        ;;\n    two)\n        API \"?to=$FROM\" -d 'OK ONE!'\n        API \"?to=$FROM\" -d 'OK TWO!!'\n        echo 'OK NATIVE'\n        ;;\n    buttons)\n        bGoogle='{\"text\":\"Google\",\"url\":\"https://www.google.com/\"}'\n        bDuck='{\"text\":\"DuckDuckGo\",\"url\":\"https://duckduckgo.com/\"}'\n        API sendMessage \\\n            -F chat_id=$FROM \\\n            -F text='Select search engine' \\\n            -F reply_markup='{\"inline_keyboard\":[['\"$bGoogle,$bDuck\"']]}'\n        ;;\n    image)\n        curl -qs https://github.com/fluidicon.png\n        ;;\n    invert)\n        wm=0\n        fid=''\n        for x in $tg_message_photo # finding the biggest image but ignoring too big ones\n        do\n            v=${x}_file_size\n            s=${!v} # trick: getting variable name from variable; we need bash for it\n            if test $s -gt 102400; then continue; fi # skipping too big files\n            v=${x}_width\n            w=${!v}\n            v=${x}_file_id\n            f=${!v}\n            if test $w -gt $wm; then wm=$w; fid=$f; fi\n        done\n        if test -n \"$fid\"\n        then\n            API_STDOUT '' -G --data-urlencode \"file_id=$fid\" -o - | mogrify -flip -flop -format png -\n        else\n            echo \"attache not found (maybe it was skipped due to enormous size)\"\n        fi\n        ;;\n    reaction)\n        API setMessageReaction \\\n            -F chat_id=$FROM \\\n            -F message_id=$tg_message_message_id \\\n            -F reaction='[{\"type\":\"emoji\",\"emoji\":\"👾\"}]'\n        echo 'Bot reacted to your message☝️'\n        ;;\n    madrid)\n        API sendLocation \\\n            -F chat_id=\"$FROM\" \\\n            -F latitude='40.423467' \\\n            -F longitude='-3.712184'\n        ;;\n    menu)\n        mShowEnv='{\"text\":\"show environment\",\"callback_data\":\"menu-debug\"}'\n        mShowNotification='{\"text\":\"show notification\",\"callback_data\":\"menu-notification\"}'\n        mShowAlert='{\"text\":\"show alert\",\"callback_data\":\"menu-alert\"}'\n        mLikeIt='{\"text\":\"like it\",\"callback_data\":\"menu-like\"}'\n        mUnlikeIt='{\"text\":\"unlike it\",\"callback_data\":\"menu-unlike\"}'\n        mDelete='{\"text\":\"delete this message\",\"callback_data\":\"menu-delete\"}'\n        mLayout=\"[[$mShowEnv],[$mShowAlert,$mShowNotification],[$mLikeIt,$mUnlikeIt],[$mDelete]]\"\n        API sendMessage \\\n            -F chat_id=$FROM \\\n            -F text='Actions' \\\n            -F reply_markup='{\"inline_keyboard\":'\"$mLayout\"'}'\n        ;;\n    run)\n        API \"?to=$FROM\u0026a=reactions\u0026a=$tg_message_message_id\" -X RUN\n        echo \"I'll show you long run\"\n        ;;\n    edit)\n        API \"?to=$FROM\u0026a=editing\" -X RUN\n        ;;\n    id)\n        echo '%!PRE'\n        id 2\u003e\u00261\n        ;;\n    caps)\n        echo '%!PRE'\n        getpcaps --verbose --iab $$\n        ;;\n    hostname)\n        echo '%!PRE'\n        hostname 2\u003e\u00261\n        ;;\n    help)\n        API sendMessage -F chat_id=$FROM -F parse_mode=Markdown -F text='\nKnown commands:\n\n- `debug` — show args, environment and vars\n- `about` — reslut of getMe\n- `two` — one request, two responses\n- `buttons` — message with buttons\n- `image` — show image\n- `invert` (as capture to image) — returns flipped flopped image\n- `reaction` — show reaction\n- `madrid` — show location\n- `menu` — scripted buttons\n- `run` — long-run example (long sequence of reactions)\n- `edit` — long-run example (editing)\n- `id` — check user who script runs from\n- `caps` — check current capabilities (`getpcaps $$`)\n- `hostname` — check hostname where script runs\n- `help` — show this message\n- `privacy` — mandatory privacy information\n- `start` — just very first greeting message\n'\n        ;;\n    start)\n        API sendMessage -F chat_id=$FROM -F parse_mode=Markdown -F text='\nHi there!👋\nIt is demo bot to show an example of usage [cnbot](https://github.com/michurin/cnbot) bot engine.\nYou can use `help` command to see all available commands.'\n        ;;\n    privacy) # https://telegram.org/tos/bot-developers#4-privacy\n        echo \"This bot does not collect or share any personal information.\"\n        ;;\n    *)\n        if test -n \"$tg_callback_query_data\"\n        then\n            case \"$1\" in\n                menu-debug)\n                    API answerCallbackQuery -F callback_query_id=\"$tg_callback_query_id\"\n                    echo '%!PRE'\n                    echo \"Environment:\"\n                    env | grep tg_ | sort\n                    ;;\n                menu-like)\n                    API answerCallbackQuery -F callback_query_id=\"$tg_callback_query_id\" -F \"text=Like it\"\n                    API setMessageReaction -F chat_id=$tg_callback_query_message_chat_id \\\n                        -F message_id=$tg_callback_query_message_message_id \\\n                        -F reaction='[{\"type\":\"emoji\",\"emoji\":\"👾\"}]'\n                    ;;\n                menu-unlike)\n                    API answerCallbackQuery -F callback_query_id=\"$tg_callback_query_id\" -F \"text=Don't like it\"\n                    API setMessageReaction -F chat_id=$tg_callback_query_message_chat_id \\\n                        -F message_id=$tg_callback_query_message_message_id \\\n                        -F reaction='[]'\n                    ;;\n                menu-delete)\n                    API answerCallbackQuery -F callback_query_id=\"$tg_callback_query_id\"\n                    API deleteMessage -F chat_id=$tg_callback_query_message_chat_id \\\n                        -F message_id=$tg_callback_query_message_message_id\n                    ;;\n                menu-notification)\n                    API answerCallbackQuery -F callback_query_id=\"$tg_callback_query_id\" -F text=\"Notification text (200 chars maximum)\"\n                    ;;\n                menu-alert)\n                    API answerCallbackQuery -F callback_query_id=\"$tg_callback_query_id\" -F text=\"Notification text shown as alert\" -F show_alert=true\n                    ;;\n            esac\n        else\n            API sendMessage -F chat_id=$FROM -F text='Invalid command. Say `help`.' -F parse_mode=Markdown\n        fi\n        ;;\nesac\n```\n\nLet's add script for long-running tasks `mybot_long.sh` (it's [demo script](demo/demo_bot_long.sh)):\n\n```sh\n#!/bin/sh\n\nLOG=logs/log_long.log # /dev/null\n\nFROM=\"$tg_x_to\"\n\nAPI() {\n    API_STDOUT \"$@\" \u003e\u003e\"$LOG\"\n}\n\nAPI_STDOUT() {\n    url=\"http://localhost$tg_x_ctrl_addr/$1\"\n    shift\n    echo \"====== curl $url $@\" \u003e\u003e\"$LOG\"\n    curl -qs \"$url\" \"$@\" 2\u003e\u003e\"$LOG\"\n    echo \u003e\u003e\"$LOG\"\n    echo \u003e\u003e\"$LOG\"\n}\n\ncase \"$1\" in\n    reactions)\n        MESSAGE_ID=\"$2\"\n        for e in \"👾\" \"🤔\" \"😎\"\n        do\n            API setMessageReaction -F chat_id=$FROM -F message_id=$MESSAGE_ID -F reaction='[{\"type\":\"emoji\",\"emoji\":\"'\"$e\"'\"}]'\n            sleep 1\n        done\n        API setMessageReaction -F chat_id=$FROM -F message_id=$MESSAGE_ID -F reaction='[]'\n        ;;\n    editing)\n        MESSAGE_ID=\"$(API_STDOUT sendMessage -F chat_id=$FROM -F text='Starting...' | jq .result.message_id)\"\n        if test -n \"$MESSAGE_ID\"\n        then\n            for i in 2 4 6 8\n            do\n                sleep 1\n                API editMessageText -F chat_id=$FROM -F message_id=\"$MESSAGE_ID\" -F text=\"Doing... ${i}0% complete...\"\n            done\n            sleep 1\n            API editMessageText -F chat_id=$FROM -F message_id=\"$MESSAGE_ID\" -F text='Done.'\n        else\n            echo \"cannot obtain message id\"\n        fi\n        ;;\n    *)\n        echo 'invalid mode'\n        ;;\nesac\n```\n\nRestart bot with this configuration (`mybot.env`):\n\n```ini\ntb_token               = 'TOKEN'\ntb_script              = ./mybot.sh\ntb_long_running_script = ./mybot_long.sh\ntb_ctrl_addr           = :9999\n```\n\nLike that:\n\n```sh\n# if you install it\ncnbot mybot.env\n# if you start it without installing, just from sources\ngo run ./cmd/cnbot/... mybot.env\n```\n\n\u003e [!NOTE]\n\u003e Please note when you are modifying script, all changes takes effect immediately. You don't need to restart the bot engine.\n\u003e You have to restart the bot engine if you want to change its environment variables only.\n\nTry to talk to your bot. Now it recognizes commands and shows you many different possibilities.\n\nLet me explain what is happening in this examples step by step.\n\n### Script structure\n\nYou wouldn't be mistaken for thinking that this script is slightly awkward. It is written that way\nto be more splittable. We will consider better structure further.\n\n### Helpers overview\n\nLet's briefly touch on two helpers functions we are using in this scripts.\n\nBoth of them helps you to call bot engine API (not Telegram API, but bot engine).\n\n`API_STDOUT()` takes it's first argument as a tail of API URL and consider all the rest of arguments\nas `curl`'s arguments. For example, `API_STDOUT getMe` means literally\n`curl -qs \"http://localhost$tg_x_ctrl_addr/getMe\"`.\n\n`API_STDOUT()` throws it's output to `stdout`, `API()` doesn't though.\n`API \"?to=$FROM\" -d 'OK'` means `curl -qs \"http://localhost$tg_x_ctrl_addr/?to=$FROM -d 'OK'`\n\nBoth of them logs their output to `$LOG` file.\n\n### Commands\n\nThis script recognizes several commands. We already consider the following commands:\n\n- `debug` — it's our first script\n- `about` — just call `getMe` API method. You can also see how we use `API_STDOUT` helper\n- `two` — shows how to send asynchronous message from script. We saw how to do it from command line before. You can also see how we use `API` helper\n- `buttons` — message with buttons as we saw before\n- `image` — shows how to send image. Just throw it to `stdout` and bot engine will recognize that it is image and send it in proper way\n\nAll the rest commands we will consider further.\n\n## Advanced topics\n\n### Configuration details and driving multiple bots\n\nYou are already seeing the bot can be configured by configuration file and directory by environment variable.\n\nEnvironment has higher priority.\n\nAll variables have the same structure: `tb_{MEANING}` or `tb_{BOTNAME}_{MEANING}` if you need to start several bots.\n\nTo configure bot `x` and `y`, you need to pass this variable to `cnbot`:\n\n```sh\ntb_x_token='TOKEN_X'\ntb_x_script=/usr/bin/echo\ntb_x_long_running_script=/usr/bin/echo\ntb_x_ctrl_addr=:9999\n\ntb_y_token='TOKEN_Y'\ntb_y_script=/usr/bin/echo\ntb_y_long_running_script=/usr/bin/echo\ntb_y_ctrl_addr=:9998\n```\n\n### Arguments processing\n\nBot engine runs your scripts with command line arguments. It can be useful for small bots.\n\nArguments prepared from messages, captions and callback's data. Strings are cast to lower-case, cleaned of control characters and split by white spaces.\n\nFor example the message `$Hello world!` will be represented as two arguments `hello` and `world`.\n\nFollowing characters will be removed from the arguments: ``!\"#$\u0026'()*+-./:;\u003c=\u003e?@[\\]`|``.\n\n### Environment details\n\n#### Turning telegram payload to environment variables\n\nBot engine converts every [JSON-update](https://core.telegram.org/bots/api#update) to flat set of environment variables this way:\n\n```json\n{\n  \"ok\": true,\n  \"result\": [\n    {\n      \"message\": {\n        \"caption\": \"Hi!\",\n        \"chat\": {\n          \"first_name\": \"Alexey\",\n          \"id\": 150000000,\n          \"last_name\": \"Michurin\",\n          \"type\": \"private\",\n          \"username\": \"AlexeyMichurin\"\n        },\n        \"date\": 1600000000,\n        \"from\": {\n          \"first_name\": \"Alexey\",\n          \"id\": 150000000,\n          \"is_bot\": false,\n          \"language_code\": \"en\",\n          \"last_name\": \"Michurin\",\n          \"username\": \"AlexeyMichurin\"\n        },\n        \"message_id\": 2222,\n        \"photo\": [\n          {\n            \"file_id\": \"aaa0\",\n            \"file_size\": 2444,\n            \"file_unique_id\": \"id0\",\n            \"height\": 90,\n            \"width\": 90\n          },\n          {\n            \"file_id\": \"aaa1\",\n            \"file_size\": 4888,\n            \"file_unique_id\": \"id1\",\n            \"height\": 128,\n            \"width\": 128\n          }\n        ]\n      },\n      \"update_id\": 500000000\n    }\n  ]\n}\n```\n\nturns to the following environment variables:\n\n```ini\ntg_message_caption=Hi!\ntg_message_chat_first_name=Alexey\ntg_message_chat_id=150000000\ntg_message_chat_last_name=Michurin\ntg_message_chat_type=private\ntg_message_chat_username=AlexeyMichurin\ntg_message_date=1600000000\ntg_message_from_first_name=Alexey\ntg_message_from_id=150000000\ntg_message_from_is_bot=false\ntg_message_from_language_code=en\ntg_message_from_last_name=Michurin\ntg_message_from_username=AlexeyMichurin\ntg_message_message_id=2222\ntg_message_photo=tg_message_photo_0 tg_message_photo_1\ntg_message_photo_0_file_id=aaa0\ntg_message_photo_0_file_size=2444\ntg_message_photo_0_file_unique_id=id0\ntg_message_photo_0_height=90\ntg_message_photo_0_width=90\ntg_message_photo_1_file_id=aaa1\ntg_message_photo_1_file_size=4888\ntg_message_photo_1_file_unique_id=id1\ntg_message_photo_1_height=128\ntg_message_photo_1_width=128\ntg_update_id=500000000\n```\n\n#### Build-in variables (`x`-variables)\n\nEngine provides the following additional variables:\n\n- `tg_x_build`\n- `tg_x_ctrl_addr`\n- `tg_x_to` (long-running scripts only)\n\n#### System variables\n\n\u003e [!NOTE]\n\u003e Beware. Bot engine does *NOT* convey its environment to child scripts.\n\nBot engine does not transfer environment to child scripts. It is conscious decision cause it helps to\nmake script's behavior more predictable and reproducible. Variables like `$PATH`, `$LANG`, `$LS_ALL` can\nchange behavior of many commands and functions. It can lead to hard to debug behavior.\n\nIf you need to have some environment variables, just set them in you script explicitly.\n\n### Working directory\n\nCurrent working directory is directory, where the script is located in.\n\n### Process management: concurrency, timeouts, signals, long-running tasks\n\n#### Ordinary tasks\n\nBot engine generates all tasks of the same bot run strictly concurrently. It means you can use\nshared resources like files without any doubts. And your tasks have to finish in short time.\n\nBot engine will send `SIGTERM` to task after 10 seconds, and `SIGKILL` after next 10 seconds.\n\n#### Long-running tasks\n\nLong-running tasks can be executed simultaneously though.\n\nThey also have timeouts: 10 minutes.\n\n### Uploading and downloading\n\nTo upload something (image, video, audio, etc) you can just throw it stdout of your script.\nIf you need to add capture or group multimedia files in one message, you need to call\nTelegram API. As usual, you don't need to care about secrets etc just use `cnbot` control handler as we did above.\n\nTo download attachments (file, video, audio, photos, etc) you have to use `file_id` from message and\njust perform `GET` request to control handler with `file_id=...` in query string. See action `invert`\nin example above.\n\n## Tips and tricks\n\n### Improved script structure and security aspects\n\n```sh\n# --- global variables\n...\n# --- helper variables\n...\n# --- must have commands\ncase $1 in\nstart)\n    echo \"Hello message\"\n    exit\n    ;;\nprivacy) # https://telegram.org/tos/bot-developers#4-privacy\n    echo \"This bot does not collect or share any personal information.\"\n    exit\nesac\n# --- whitelist checks for user_id\n# it is just example:\n# - allows.list have contains strings line \"_${ID}_\" (it makes you able to write comments and things like that)\n# - we consider messages and callbacks\nif grep \"_${tg_message_from_id}${tg_callback_query_from_id}_\" allows.list 2\u003e\u00261 \u003e/dev/null\nthen\n    : # pass this user, you may want to log it\nelse\n    echo 'You are not allowd'\n    exit\nfi\n# --- process text messages\nif [ -n \"$tg_message_text\" ]\nthen\n    case \"$1\" in\n        ...\n    esac\n    exit\nfi\n# --- process images\nif [ -n \"$tg_message_photo\" ]\nthen\n    case \"$1\" in\n        ...\n    esac\n    exit\nfi\n# --- process voices (for instance)\nif [ -n \"$tg_message_voice_file_id\" ]\nthen\n    ...\n    exit # don't forget to exit\nfi\n# --- process callbacks\nif [ -n \"$tg_callback_query_data\" ]\nthen\n    ...\n    exit\nfi\n# process... whatever you want\nif ...\n    ...\n    exit\nfi\n```\n\nOf course, it is good idea to split script, using `source file.sh` instruction.\nAnd you are still able to use other languages and approaches for sure.\n\n\u003e [!CAUTION]\n\u003e Just don't forget to be careful, keep in mind that anybody in internet can send anything to your bot.\n\u003e\n\u003e Keep reading. We will consider how to protect your bot.\n\n### Debugging wrapper\n\nTo debug your scripts, you can use this wrapper. Tune `$CMD`, and enjoy\nfull logging: arguments, environment, out and err streams, exit code.\n\n```sh\n#!/bin/sh\n\n# put your command here\nCMD=./mybot.py\n\n# tune naming for your taste\nbase=\"logs/$(date +%s-)_${$}_\"\next='.log'\n\nn=0\nfor a in \"$@\"\ndo\n    echo \"$a\" \u003e\"${base}arg_${n}${ext}\"\n    n=\"$(($n+1))\"\ndone\n\nenv | sort \u003e\"${base}env${ext}\"\n\nset -o pipefail\n\n\"$CMD\" \"$@\" 2\u003e\"${base}err${ext}\" | tee \"${base}out${ext}\"\n\ncode=\"$?\"\n\necho \"$code\" \u003e\"${base}status${ext}\"\nexit \"$code\"\n```\n\n## System administration topics\n\n### Installation\n\n```sh\n./build.sh\nsudo install ./cnbot /usr/bin\n```\n\n### Running\n\nThe process itself does not try to be immortal. It dies on fatal issues that can not be solved by process itself. Like network problems.\nIt is believed that the process will be restart by `systemd` or stuff like that according the proper way with timeouts, logging, notifications, alerting.\n\nSystemd unit file example (`/etc/systemd/system/cnbot.service`):\n\n```ini\n[Unit]\nDescription=Telegram bot (cnbot) service\nDocumentation=https://github.com/michurin/cnbot\nAfter=network.target nss-lookup.target\n\n[Service]\nType=simple\nRestart=always\nRestartSec=1\nUser=nobody\nExecStart=/usr/bin/cnbot /etc/cnbot-config.env\n\n[Install]\nWantedBy=multi-user.target\n```\n\n## Known issues\n\n- Some engine API methods are using both POST-body and query parameters. It's against standards. However I haven't invented something more convenient and standard yet.\n- Engine API uses non-standard method `RUN`. It allows by standards, however it doesn't seem inevitable.\n- Engine uses [`mime.ExtensionsByType()`](https://pkg.go.dev/mime#ExtensionsByType) to detect extensions for multimedia attachments. This function relies on the system configuration. It's highly recommended to install package like `shared-mime-info`. Pleas keep it in mind when you build production docker images and deploy the engine to remote servers.\n- Integration tests rely exclusively on `bash` rather than any other shell. Simple `sh` won't work in most cases.\n- Tests also rely on `curl`.\n- Engine doesn't retry any requests to Telegram API. Looks like issue. However, Telegram API doesn't provide any idempotency keys, and engine doesn't save state between restarts. It seems you have to solve this issue somehow else.\n- It hasn't been tested on MS Windows and FreeBSD.\n- The engine doesn't support persistent storage. You have to save state if you need by yourself.\n- Engine consider kill signals as errors. So it's final log message is error mostly. It is confusing.\n- Right now code has a lot of public types, methods and functions. I want this code to be able to be embedded and integrated. However, public API needs to be reviewed.\n\n## Developing and contributing\n\n### Main ideas\n\n- Contract must be simple and flexible\n- New features of [Telegram bot API](https://core.telegram.org/bots/api) has to be available instantly without changing of code of the bot\n- Bot has to manage subprocesses: timeouts, etc\n- Bot has to manage API call: [rate limits](https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this), etc\n- Configuration must be simple\n- Code must be testable and has to be covered\n- Functionality has to be observable and has to provide ability to add metrics and monitoring by adding middleware without code changing\n- The engine tries to be case insensitive considering environment variables. It can lead to false warnings\n\n### Deep debugging\n\nRun proxy. For example [mitmproxy](https://mitmproxy.org/):\n\n```sh\nmitmdump --flow-detail 4 -p 9001 --mode reverse:https://api.telegram.org\n```\n\nInstruct the bot to use proxy and run it:\n\n```sh\nexport tb_api_origin=http://localhost:9001\n./cnbot ... # run bot, it will deal with Telegram API through the proxy and you will see everything\n```\n\n### Application structure\n\n(horrible ASCII art warning)\n\n```\n   Telegram infrastructure\n             ^                             ............. crons\n        HTTP :                        HTTP :             scripts\n             :                             v             any other\n.=BOT================================================.   asynchronous\n|            API           | HTTP server for         |\n|..........................| asynchronous messaging  |\n| polling for : sending    |                         |\n| updates     : messages  \u003c-- send data from req     |\n`===================================================='\n    |             ^    ^  send stdout     |\n    |             |    `---------.        | request params\n    | message     | send         |        | as command line positional args\n    v data        | stdout       |        v\n........................        ......................\n: run script for every :        : long-running       :\n: message              :        : script             :\n:......................:        :....................:\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichurin%2Fcnbot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichurin%2Fcnbot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichurin%2Fcnbot/lists"}