[
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Publish Docker image\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  push_to_registry:\n    name: Push Docker image to Docker Hub\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            msoap/shell2telegram\n          tags: |\n            type=semver,pattern={{version}}\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_TOKEN }}\n\n      - name: Push to Docker Hub\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64,linux/arm/v6\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "name: Go\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n  schedule:\n    - cron: '0 12 * * 0'\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go: ['1.24.x', '1.25.x']\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Set up Go\n      uses: actions/setup-go@v5\n      with:\n        go-version: ${{ matrix.go }}\n\n    - name: Install errcheck\n      run: go install github.com/kisielk/errcheck@latest\n\n    - name: errcheck\n      run: errcheck -verbose ./...\n\n    - name: gofmt check\n      run: diff <(gofmt -d .) <(echo -n \"\")\n\n    - name: Test\n      run: go test -race -v ./...\n\n    - name: Coveralls\n      if: ${{ startsWith(matrix.go, '1.25') && github.event_name == 'push' }}\n      env:\n          COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      run: |\n        go install github.com/mattn/goveralls@latest && \\\n        go test -covermode=count -coverprofile=profile.cov ./... && \\\n        goveralls -coverprofile=profile.cov -service=github\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: goreleaser\n\non:\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: 1.x\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v5\n        with:\n          distribution: goreleaser\n          version: latest\n          args: release --rm-dist\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Post release\n        run: ls -l ./dist/*\n"
  },
  {
    "path": ".gitignore",
    "content": "tags\nshell2telegram\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "release:\n  name_template: \"{{ .Version }} - {{ .Date }}\"\n  draft: true\n  header: |\n    [![Github Releases ({{ .Tag }})](https://img.shields.io/github/downloads/msoap/shell2telegram/{{ .Tag }}/total.svg)](https://github.com/msoap/shell2telegram/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/msoap/shell2telegram/total.svg)](https://github.com/msoap/shell2telegram/releases)\n\nbuilds:\n  - env:\n      - CGO_ENABLED=0\n    goos:\n      - linux\n      - darwin\n      - windows\n    goarch:\n      - 386\n      - amd64\n      - arm\n      - arm64\n    ignore:\n      - goos: windows\n        goarch: arm\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X main.version={{ .Version }}\n\nnfpms:\n  - \n    homepage: https://github.com/msoap/{{ .ProjectName }}\n    description: Create Telegram bot from command-line.\n    license: MIT\n    formats:\n      - deb\n      - rpm\n    bindir: /usr/bin\n    contents:\n      - src: shell2telegram.1\n        dst: /usr/share/man/man1/shell2telegram.1\n      - src: LICENSE\n        dst: /usr/share/doc/shell2telegram/copyright\n      - src: README.md\n        dst: /usr/share/doc/shell2telegram/README.md\n\narchives:\n  -\n    format_overrides:\n      - goos: windows\n        format: zip\n    files:\n      - README*\n      - LICENSE*\n      - \"*.1\"\n\nchecksum:\n  name_template: 'checksums.txt'\n\nsnapshot:\n  name_template: \"{{ .Tag }}\"\n\nchangelog:\n  sort: desc\n  filters:\n    exclude:\n      - '^docs:'\n      - '^test:'\n      - '^Merge branch'\n      - '^go fmt'\n"
  },
  {
    "path": "Dockerfile",
    "content": "# docker build -t msoap/shell2telegram .\n\n# build image\nFROM --platform=$BUILDPLATFORM golang:alpine as go_builder\n\nARG TARGETPLATFORM\nARG BUILDPLATFORM\nARG TARGETOS\nARG TARGETARCH\n\nRUN apk add --no-cache git\n\nADD . $GOPATH/src/github.com/msoap/shell2telegram\nWORKDIR $GOPATH/src/github.com/msoap/shell2telegram\n\nENV CGO_ENABLED=0\n# GOARM=6 affects only \"arm\" builds\nENV GOARM=6\n# \"amd64\", \"arm64\" or \"arm\" (--platform=linux/amd64,linux/arm64,linux/arm/v6)\nENV GOARCH=$TARGETARCH\nENV GOOS=linux\n\nRUN echo \"Building for $GOOS/$GOARCH\"\nRUN go build -v -trimpath -ldflags=\"-w -s -X 'main.version=$(git describe --abbrev=0 --tags | sed s/v//)'\" -o /go/bin/shell2telegram .\n\n# final image\nFROM alpine\n\nRUN apk add --no-cache ca-certificates\nCOPY --from=go_builder /go/bin/shell2telegram /app/shell2telegram\nENTRYPOINT [\"/app/shell2telegram\"]\nCMD [\"-help\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Serhii Mudryk\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "Makefile",
    "content": "APP_NAME := shell2telegram\nAPP_DESCRIPTION := $$(awk 'NR == 11, NR == 13' README.md)\nAPP_URL := https://github.com/msoap/$(APP_NAME)\nAPP_MAINTAINER := $$(git show HEAD | awk '$$1 == \"Author:\" {print $$2 \" \" $$3 \" \" $$4}')\nGIT_TAG := $$(git describe --tags --abbrev=0)\n\nrun:\n\tgo run . $${TB_ROOT:+-root-users=$$TB_ROOT} \\\n        -add-exit \\\n        -log-commands \\\n        -persistent-users \\\n        /date:desc=\"Get current date\" date \\\n        /:plain_text:desc=\"Numbers via cat -n\" 'cat -n' \\\n        /alarm:vars=SLEEP,MSG 'sleep $$SLEEP; echo Hello $$S2T_USERNAME, $$MSG'\n\nbuild:\n\tgo build\n\ntest:\n\tgo test -race -cover -v ./...\n\nlint:\n\tgolint ./...\n\tgo vet ./...\n\terrcheck ./...\n\nupdate-from-github:\n\tgo get -u github.com/msoap/$(APP_NAME)\n\nbuild-docker-image:\n\tdocker run --rm -v $$PWD:/go/src/$(APP_NAME) -w /go/src/$(APP_NAME) golang:alpine sh -c \"apk add --no-cache git && go get ./... && go build -ldflags='-w -s' -o $(APP_NAME)\"\n\tdocker build -t msoap/$(APP_NAME):latest .\n\trm -f $(APP_NAME)\n\ngometalinter:\n\tgometalinter --vendor --cyclo-over=25 --line-length=150 --dupl-threshold=150 --min-occurrences=3 --enable=misspell --deadline=10m --exclude=SA1022\n\ngenerate-manpage:\n\tcat README.md | grep -v \"^\\[\" | perl -pe 's/\\<img.+\\>//' > $(APP_NAME).md\n\tdocker run --rm -v $$PWD:/app -w /app msoap/ruby-ronn ronn $(APP_NAME).md\n\tmv ./$(APP_NAME) ./$(APP_NAME).1\n\trm ./$(APP_NAME).{md,html}\n\ncreate-debian-amd64-package:\n\tGOOS=linux GOARCH=amd64 go build -ldflags=\"-w -s\" -o $(APP_NAME)\n\tdocker run --rm -v $$PWD:/app -w /app msoap/ruby-fpm \\\n\t\tfpm -s dir -t deb --force --name $(APP_NAME) -v $(GIT_TAG) \\\n\t\t\t--license=\"$$(head -1 LICENSE)\" \\\n\t\t\t--url=$(APP_URL) \\\n\t\t\t--description=\"$(APP_DESCRIPTION)\" \\\n\t\t\t--maintainer=\"$(APP_MAINTAINER)\" \\\n\t\t\t--category=network \\\n\t\t\t./$(APP_NAME)=/usr/bin/ \\\n\t\t\t./$(APP_NAME).1=/usr/share/man/man1/ \\\n\t\t\tLICENSE=/usr/share/doc/$(APP_NAME)/copyright \\\n\t\t\tREADME.md=/usr/share/doc/$(APP_NAME)/\n\trm $(APP_NAME)\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://raw.githubusercontent.com/msoap/shell2telegram/misc/img/shell2telegram_icon.png\" width=\"32\" height=\"32\"> shell2telegram\n-----------------------------------------------------------------------------------------------------------------------------------------\n\n[![Go build status](https://github.com/msoap/shell2telegram/actions/workflows/go.yml/badge.svg)](https://github.com/msoap/shell2telegram/actions/workflows/go.yml)\n[![Coverage Status](https://coveralls.io/repos/github/msoap/shell2telegram/badge.svg?branch=master)](https://coveralls.io/github/msoap/shell2telegram?branch=master)\n[![Docker Pulls](https://img.shields.io/docker/pulls/msoap/shell2telegram.svg?maxAge=3600)](https://hub.docker.com/r/msoap/shell2telegram/)\n[![Homebrew formula exists](https://img.shields.io/badge/homebrew-🍺-d7af72.svg)](https://github.com/msoap/shell2telegram#install)\n[![Report Card](https://goreportcard.com/badge/github.com/msoap/shell2telegram)](https://goreportcard.com/report/github.com/msoap/shell2telegram)\n\nCreate Telegram bot from command-line\n\nInstall\n-------\n\nMacOS:\n\n    brew tap msoap/tools\n    brew install shell2telegram\n    # update:\n    brew upgrade shell2telegram\n\nOr download binaries from: [releases](https://github.com/msoap/shell2telegram/releases) (OS X/Linux/Windows/RaspberryPi)\n\nOr build from source:\n\n    # set $GOPATH if needed\n    go install github.com/msoap/shell2telegram@latest\n    ln -s $GOPATH/bin/shell2telegram ~/bin/shell2telegram # or add $GOPATH/bin to $PATH\n\nOr build image and run with Docker.\nExample of `test-bot.Dockerfile` for bot who say current date:\n\n    FROM msoap/shell2telegram\n    # may be install some alpine packages:\n    # RUN apk add --no-cache ...\n    ENV TB_TOKEN=*******\n    CMD [\"/date\", \"date\"]\n\nAnd build and run:\n\n    docker build -f test-bot.Dockerfile -t test-bot .\n    docker run --rm test-bot\n    # or run with set token from command line\n    docker run -e TB_TOKEN=******* --rm test-bot\n\nUsing snap (Ubuntu or any Linux distribution with snap):\n\n    # install stable version:\n    sudo snap install shell2telegram\n    \n    # install the latest version:\n    sudo snap install --edge shell2telegram\n    \n    # update\n    sudo snap refresh shell2telegram\n\nNotice: the snap-package has its own sandbox with the `/bin`, `/usr/bin` directories which are not equal to system-wide `PATH` directories.\n\nUsage\n-----\n\nGet token from [BotFather bot](https://telegram.me/BotFather), and set TB_TOKEN var in shell\n\n    export TB_TOKEN=*******\n    shell2telegram [options] /chat_command 'shell command' /chat_command2 'shell command2'\n    options:\n        -allow-users=<NAMES> : telegram users who are allowed to chat with the bot (\"user1,user2\")\n        -root-users=<NAMES>  : telegram users, who confirms new users in their private chat (\"user1,user2\")\n        -allow-all           : allow all users (DANGEROUS!)\n        -add-exit            : adding \"/shell2telegram exit\" command for terminate bot (for roots only)\n        -log-commands        : logging all commands\n        -tb-token=<TOKEN>    : setting bot token (or set TB_TOKEN variable)\n        -timeout=N           : setting timeout for bot (default 60 sec)\n        -description=<TITLE> : setting description of bot\n        -bind-addr=<ADDRESS> : address to listen incoming webhook requests\n        -webhook=<URL>       : url for registering a webhook\n        -persistent-users    : load/save users from file (default ~/.config/shell2telegram.json)\n        -users-db=<FILENAME> : file for store users\n        -cache=N             : caching command out for N seconds\n        -one-thread          : run each shell command in one thread\n        -public              : bot is public (don't add /auth* commands)\n        -sh-timeout=N        : set timeout for execute shell command (in seconds)\n        -shell=\"shell\"       : shell for execute command, \"\" - without shell (default \"sh\")\n        -version\n        -help\n\nIf not define -allow-users/-root-users options - authorize users via secret code from console or via chat with exists root users.\n\nAll text after /chat_command will be sent to STDIN of shell command.\n\nSpecial chat commands\n---------------------\n\nfor private chats only:\n\n  * `/:plain_text` - get user message without any /command.\n\nTODO:\n\n  * `/:image` - for get image from user. Example: `/:image 'cat > file.jpg; echo ok'`\n  * `/:file`  - for get file from user\n  * `/:location`  - for get geo-location from user\n\nPossible long-running shell processes (for example alarm/timer bot).\n\nAutodetect images (png/jpg/gif/bmp) out from shell command, for example: `/get_image 'cat file.png'`\n\nSetting environment variables for shell commands:\n\n  * S2T_LOGIN - telegram @login (may be empty)\n  * S2T_USERID - telegram user ID\n  * S2T_USERNAME - telegram user name\n  * S2T_CHATID - chat ID\n\nModificators for bot commands\n-----------------------------\n\n  * `:desc` - setting the description of command, `/cmd:desc=\"Command name\" 'shell cmd'`\n  * `:vars` - to create environment variables instead of text output to STDIN, `/cmd:vars=VAR1,VAR2 'echo $VAR1 / $VAR2'`\n  * `:md` - to send message as markdown text, `/cmd:md 'echo \"*bold* and _italic_\"'`\n\nTODO:\n\n  * `/cmd:cron=3600` — periodic exec command, `/cmd:on args` - on, `/cmd:off` - off\n\nPredefined bot commands\n-----------------------\n\n  * `/help` - list available commands\n  * `/auth` - begin authorize new user\n  * `/auth <CODE>` - authorize with code from console or from exists root user\n  * `/authroot` - same for new root user\n  * `/authroot <CODE>` - same for new root user\n\nfor root users only:\n\n  * `/shell2telegram stat` - show users statistics\n  * `/shell2telegram search <query>` - search users by name/id\n  * `/shell2telegram ban <user_id|@username>` - ban user\n  * `/shell2telegram exit` - terminate bot (for run with -add-exit)\n  * `/shell2telegram desc <description>` - set bot description\n  * `/shell2telegram rm </command>` - delete command\n  * `/shell2telegram broadcast_to_root <message>` - send message to all root users in private chat\n  * `/shell2telegram message_to_user <user_id|@username> <message>` - send message to user in private chat\n  * `/shell2telegram version` - show version\n\nExamples\n--------\n\n    # system information\n    shell2telegram /top:desc=\"System information\" 'top -l 1 | head -10' /date 'date' /ps 'ps aux -m | head -20'\n    \n    # sort any input\n    shell2telegram /:plain_text sort\n    \n    # alarm bot:\n    # /alarm time_in_seconds message\n    shell2telegram /alarm:vars=SLEEP,MSG 'sleep $SLEEP; echo Hello $S2T_USERNAME; echo Alarm: $MSG'\n    \n    # sound volume control via telegram (Mac OS)\n    shell2telegram /get  'osascript -e \"output volume of (get volume settings)\"' \\\n                   /up   'osascript -e \"set volume output volume (($(osascript -e \"output volume of (get volume settings)\")+10))\"' \\\n                   /down 'osascript -e \"set volume output volume (($(osascript -e \"output volume of (get volume settings)\")-10))\"'\n\n    # using with webhook instead of poll\n    shell2telegram -bind-addr=0.0.0.0:8080 -webhook=https://bot.example.com/path/to/bot \\\n                   /date /date\n\n    # command with Markdown formating, calendar in monospace font\n    shell2telegram /cal:md 'echo \"\\`\\`\\`$(ncal)\\`\\`\\`\"'\n\nLinks\n-----\n\n  * [Telegram channel about shell2telegram](https://telegram.me/shell2telegram)\n  * [About Telegram bots](https://core.telegram.org/bots)\n  * [Golang bindings for the Telegram Bot API](https://github.com/go-telegram-bot-api/telegram-bot-api)\n  * [shell2http - shell commands as http-server](https://github.com/msoap/shell2http)\n"
  },
  {
    "path": "commands.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/msoap/raphanus\"\n)\n\n// Ctx - context for bot command function (users, command, args, ...)\ntype Ctx struct {\n\tappConfig      *Config           // configuration\n\tusers          *Users            // all users\n\tcommands       Commands          // all chat commands\n\tuserID         int               // current user\n\tallowExec      bool              // is user authorized\n\tmessageCmd     string            // command name\n\tmessageArgs    string            // command arguments\n\tmessageSignal  chan<- BotMessage // for send telegram messages\n\tchatID         int               // chat for send replay\n\texitSignal     chan<- struct{}   // for signal for terminate bot\n\tcache          *raphanus.DB      // cache for commands output\n\tcacheTTL       int               // cache timeout\n\toneThreadMutex *sync.Mutex       // mutex for run shell commands in one thread\n}\n\n// /auth and /authroot - authorize users\nfunc cmdAuth(ctx Ctx) (replayMsg string) {\n\tforRoot := ctx.messageCmd == \"/authroot\"\n\n\tif ctx.messageArgs == \"\" {\n\n\t\treplayMsg = \"See code in terminal with shell2telegram or ask code from root user and type:\\n\" + ctx.messageCmd + \" code\"\n\t\tauthCode := ctx.users.DoLogin(ctx.userID, forRoot)\n\n\t\trootRoleStr := \"\"\n\t\tif forRoot {\n\t\t\trootRoleStr = \"root \"\n\t\t}\n\t\tsecretCodeMsg := fmt.Sprintf(\"Request %saccess for %s. Code: %s\\n\", rootRoleStr, ctx.users.String(ctx.userID), authCode)\n\t\tfmt.Print(secretCodeMsg)\n\t\tctx.users.BroadcastForRoots(ctx.messageSignal, secretCodeMsg, 0)\n\n\t} else {\n\t\tif ctx.users.IsValidCode(ctx.userID, ctx.messageArgs, forRoot) {\n\t\t\tctx.users.SetAuthorized(ctx.userID, forRoot)\n\t\t\tif forRoot {\n\t\t\t\treplayMsg = fmt.Sprintf(\"You (%s) authorized as root.\", ctx.users.String(ctx.userID))\n\t\t\t\tlog.Print(\"root authorized: \", ctx.users.String(ctx.userID))\n\t\t\t} else {\n\t\t\t\treplayMsg = fmt.Sprintf(\"You (%s) authorized.\", ctx.users.String(ctx.userID))\n\t\t\t\tlog.Print(\"authorized: \", ctx.users.String(ctx.userID))\n\t\t\t}\n\t\t} else {\n\t\t\treplayMsg = \"Code is not valid.\"\n\t\t}\n\t}\n\n\treturn replayMsg\n}\n\n// /help\nfunc cmdHelp(ctx Ctx) (replayMsg string) {\n\thelpMsg := []string{}\n\n\tif ctx.allowExec {\n\t\tfor cmd, shellCmdRow := range ctx.commands {\n\t\t\tdescription := shellCmdRow.description\n\t\t\tif description == \"\" {\n\t\t\t\tdescription = shellCmdRow.shellCmd\n\t\t\t}\n\t\t\thelpMsg = append(helpMsg, cmd+\" → \"+description)\n\t\t}\n\t}\n\tsort.Strings(helpMsg)\n\n\tif !ctx.appConfig.isPublicBot {\n\t\thelpMsg = append(helpMsg,\n\t\t\t\"/auth [code] → authorize user\",\n\t\t\t\"/authroot [code] → authorize user as root\",\n\t\t)\n\t}\n\n\tif ctx.users.IsRoot(ctx.userID) {\n\t\thelpMsgForRoot := []string{\n\t\t\t\"/shell2telegram ban <user_id|username> → ban user\",\n\t\t\t\"/shell2telegram broadcast_to_root <message> → send message to all root users in private chat\",\n\t\t\t\"/shell2telegram desc <bot description> → set bot description\",\n\t\t\t\"/shell2telegram message_to_user <user_id|username> <message> → send message to user in private chat\",\n\t\t\t\"/shell2telegram rm </command> → delete command\",\n\t\t\t\"/shell2telegram search <query> → search users by name/id\",\n\t\t\t\"/shell2telegram stat → get stat about users\",\n\t\t\t\"/shell2telegram version → show version\",\n\t\t}\n\t\tif ctx.appConfig.addExit {\n\t\t\thelpMsgForRoot = append(helpMsgForRoot, \"/shell2telegram exit → terminate bot\")\n\t\t}\n\t\tsort.Strings(helpMsgForRoot)\n\n\t\thelpMsg = append(helpMsg, helpMsgForRoot...)\n\t}\n\n\tif ctx.appConfig.description != \"\" {\n\t\treplayMsg = ctx.appConfig.description\n\t} else {\n\t\treplayMsg = \"This bot created with shell2telegram\"\n\t}\n\treplayMsg += \"\\n\\n\" +\n\t\t\"available commands:\\n\" +\n\t\tstrings.Join(helpMsg, \"\\n\")\n\n\treturn replayMsg\n}\n\n// all commands from command-line\nfunc cmdUser(ctx Ctx) {\n\tif cmd, found := ctx.commands[ctx.messageCmd]; found {\n\t\tgo func() {\n\t\t\tif ctx.appConfig.oneThread {\n\t\t\t\tctx.oneThreadMutex.Lock()\n\t\t\t}\n\t\t\treplayMsgRaw := execShell(\n\t\t\t\tcmd.shellCmd,\n\t\t\t\tctx.messageArgs,\n\t\t\t\tctx.commands[ctx.messageCmd].vars,\n\t\t\t\tctx.userID,\n\t\t\t\tctx.chatID,\n\t\t\t\tctx.users.list[ctx.userID].UserName,\n\t\t\t\tctx.users.list[ctx.userID].FirstName+\" \"+ctx.users.list[ctx.userID].LastName,\n\t\t\t\tctx.cache,\n\t\t\t\tctx.cacheTTL,\n\t\t\t\tctx.appConfig,\n\t\t\t)\n\t\t\tif ctx.appConfig.oneThread {\n\t\t\t\tctx.oneThreadMutex.Unlock()\n\t\t\t}\n\n\t\t\tsendMessage(ctx.messageSignal, ctx.chatID, replayMsgRaw, cmd.isMarkdown)\n\t\t}()\n\t}\n}\n\n// /shell2telegram stat\nfunc cmdShell2telegramStat(ctx Ctx) (replayMsg string) {\n\tfor userID := range ctx.users.list {\n\t\treplayMsg += ctx.users.StringVerbose(userID) + \"\\n\"\n\t}\n\n\treturn replayMsg\n}\n\n// /shell2telegram search\nfunc cmdShell2telegramSearch(ctx Ctx) (replayMsg string) {\n\tquery := ctx.messageArgs\n\n\tif query == \"\" {\n\t\treturn \"Please set query: /shell2telegram search <query>\"\n\t}\n\n\tfor _, userID := range ctx.users.Search(query) {\n\t\treplayMsg += ctx.users.StringVerbose(userID) + \"\\n\"\n\t}\n\n\treturn replayMsg\n}\n\n// /shell2telegram ban\nfunc cmdShell2telegramBan(ctx Ctx) (replayMsg string) {\n\tuserName := ctx.messageArgs\n\n\tif userName == \"\" {\n\t\treturn \"Please set user_id or login: /shell2telegram ban <user_id|username>\"\n\t}\n\n\tuserID := ctx.users.FindByIDOrUserName(userName)\n\n\tif userID > 0 && ctx.users.BanUser(userID) {\n\t\treplayMsg = fmt.Sprintf(\"User %s banned\", ctx.users.String(userID))\n\t} else {\n\t\treplayMsg = \"User not found\"\n\t}\n\n\treturn replayMsg\n}\n\n// set bot description\nfunc cmdShell2telegramDesc(ctx Ctx) (replayMsg string) {\n\tdescription := ctx.messageArgs\n\n\tif description == \"\" {\n\t\treturn \"Please set description: /shell2telegram desc <bot description>\"\n\t}\n\n\tctx.appConfig.description = description\n\treplayMsg = \"Bot description set to: \" + description\n\n\treturn replayMsg\n}\n\n// /shell2telegram rm \"/command\" - delete command\nfunc cmdShell2telegramRm(ctx Ctx) (replayMsg string) {\n\tcommandName := ctx.messageArgs\n\n\tif commandName == \"\" {\n\t\treturn \"Please set command for delete: /shell2telegram rm </command>\"\n\t}\n\tif _, ok := ctx.commands[commandName]; ok {\n\t\tdelete(ctx.commands, commandName)\n\t\treplayMsg = \"Deleted command: \" + commandName\n\t} else {\n\t\treplayMsg = fmt.Sprintf(\"Command %s not found\", commandName)\n\t}\n\n\treturn replayMsg\n}\n\n// /shell2telegram version - get version\nfunc cmdShell2telegramVersion(_ Ctx) (replayMsg string) {\n\treplayMsg = fmt.Sprintf(\"shell2telegram %s\", version)\n\treturn replayMsg\n}\n\n// /shell2telegram exit - terminate bot\nfunc cmdShell2telegramExit(ctx Ctx) (replayMsg string) {\n\tif ctx.appConfig.addExit {\n\t\treplayMsg = \"bye...\"\n\t\tgo func() {\n\t\t\tctx.exitSignal <- struct{}{}\n\t\t}()\n\t}\n\treturn replayMsg\n}\n\n// /shell2telegram broadcast_to_root - broadcast message to root users in private chat\nfunc cmdShell2telegramBroadcastToRoot(ctx Ctx) (replayMsg string) {\n\tmessage := ctx.messageArgs\n\n\tif message == \"\" {\n\t\treplayMsg = \"Please set message: /shell2telegram broadcast_to_root <message>\"\n\t} else {\n\t\tctx.users.BroadcastForRoots(ctx.messageSignal,\n\t\t\tfmt.Sprintf(\"Message from %s:\\n%s\", ctx.users.String(ctx.userID), message),\n\t\t\tctx.userID, // don't send self\n\t\t)\n\t\treplayMsg = \"Message sent\"\n\t}\n\n\treturn replayMsg\n}\n\n// /shell2telegram message_to_user user_id|username \"message\" - send message to user in private chat\nfunc cmdShell2telegramMessageToUser(ctx Ctx) (replayMsg string) {\n\tuserName, message := splitStringHalfBySpace(ctx.messageArgs)\n\n\tif userName == \"\" || message == \"\" {\n\t\treplayMsg = \"Please set user_name and message: /shell2telegram message_to_user <user_id|username> <message>\"\n\t} else {\n\t\tuserID := ctx.users.FindByIDOrUserName(userName)\n\n\t\tif userID > 0 {\n\t\t\tctx.users.SendMessageToPrivate(ctx.messageSignal, userID, message)\n\t\t\treplayMsg = \"Message sent\"\n\t\t} else {\n\t\t\treplayMsg = \"User not found\"\n\t\t}\n\t}\n\n\treturn replayMsg\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/msoap/shell2telegram\n\ngo 1.14\n\nrequire (\n\tgithub.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect\n\tgithub.com/mattn/go-shellwords v1.0.12\n\tgithub.com/msoap/raphanus v0.14.0\n\tgithub.com/technoweenie/multipartstreamer v1.0.1 // indirect\n\tgopkg.in/telegram-bot-api.v2 v2.2.1\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=\ngithub.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=\ngithub.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=\ngithub.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=\ngithub.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=\ngithub.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=\ngithub.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=\ngithub.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9 h1:ViNuGS149jgnttqhc6XQNPwdupEMBXqCx9wtlW7P3sA=\ngithub.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9/go.mod h1:fLRUbhbSd5Px2yKUaGYYPltlyxi1guJz1vCmo1RQL50=\ngithub.com/msoap/raphanus v0.14.0 h1:g499/ayslkqV7H9dKhADgOEQV1nCuAfMOMIkzazsB6s=\ngithub.com/msoap/raphanus v0.14.0/go.mod h1:p3GKFEnntq4DvX4hT3Vg2GMHeXE2oyb1+kcB+WOF3ZM=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=\ngithub.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=\ngithub.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngolang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/telegram-bot-api.v2 v2.2.1 h1:986tIxlvgcNDRh47hv0TapZu86Ejb6HEZG7oJWDynKU=\ngopkg.in/telegram-bot-api.v2 v2.2.1/go.mod h1:6qHx+TxEVOINu9hi66EARcbgYPcJnvFK7eE1zWsXhlU=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "shell2telegram.1",
    "content": ".\\\" generated with Ronn/v0.7.3\n.\\\" http://github.com/rtomayko/ronn/tree/0.7.3\n.\n.TH \"SHELL2TELEGRAM\" \"\" \"January 2022\" \"\" \"\"\n.\n.SH \"NAME\"\n\\fBshell2telegram\\fR\n.\n.SH \"shell2telegram\"\nCreate Telegram bot from command\\-line\n.\n.SH \"Install\"\nMacOS:\n.\n.IP \"\" 4\n.\n.nf\n\nbrew tap msoap/tools\nbrew install shell2telegram\n# update:\nbrew upgrade shell2telegram\n.\n.fi\n.\n.IP \"\" 0\n.\n.P\nOr download binaries from: releases \\fIhttps://github\\.com/msoap/shell2telegram/releases\\fR (OS X/Linux/Windows/RaspberryPi)\n.\n.P\nOr build from source:\n.\n.IP \"\" 4\n.\n.nf\n\n# set $GOPATH if needed\ngo install github\\.com/msoap/shell2telegram@latest\nln \\-s $GOPATH/bin/shell2telegram ~/bin/shell2telegram # or add $GOPATH/bin to $PATH\n.\n.fi\n.\n.IP \"\" 0\n.\n.P\nOr build image and run with Docker\\. Example of \\fBtest\\-bot\\.Dockerfile\\fR for bot who say current date:\n.\n.IP \"\" 4\n.\n.nf\n\nFROM msoap/shell2telegram\n# may be install some alpine packages:\n# RUN apk add \\-\\-no\\-cache \\.\\.\\.\nENV TB_TOKEN=*******\nCMD [\"/date\", \"date\"]\n.\n.fi\n.\n.IP \"\" 0\n.\n.P\nAnd build and run:\n.\n.IP \"\" 4\n.\n.nf\n\ndocker build \\-f test\\-bot\\.Dockerfile \\-t test\\-bot \\.\ndocker run \\-\\-rm test\\-bot\n# or run with set token from command line\ndocker run \\-e TB_TOKEN=******* \\-\\-rm test\\-bot\n.\n.fi\n.\n.IP \"\" 0\n.\n.P\nUsing snap (Ubuntu or any Linux distribution with snap):\n.\n.IP \"\" 4\n.\n.nf\n\n# install stable version:\nsudo snap install shell2telegram\n\n# install the latest version:\nsudo snap install \\-\\-edge shell2telegram\n\n# update\nsudo snap refresh shell2telegram\n.\n.fi\n.\n.IP \"\" 0\n.\n.P\nNotice: the snap\\-package has its own sandbox with the \\fB/bin\\fR, \\fB/usr/bin\\fR directories which are not equal to system\\-wide \\fBPATH\\fR directories\\.\n.\n.SH \"Usage\"\nGet token from BotFather bot \\fIhttps://telegram\\.me/BotFather\\fR, and set TB_TOKEN var in shell\n.\n.IP \"\" 4\n.\n.nf\n\nexport TB_TOKEN=*******\nshell2telegram [options] /chat_command \\'shell command\\' /chat_command2 \\'shell command2\\'\noptions:\n    \\-allow\\-users=<NAMES> : telegram users who are allowed to chat with the bot (\"user1,user2\")\n    \\-root\\-users=<NAMES>  : telegram users, who confirms new users in their private chat (\"user1,user2\")\n    \\-allow\\-all           : allow all users (DANGEROUS!)\n    \\-add\\-exit            : adding \"/shell2telegram exit\" command for terminate bot (for roots only)\n    \\-log\\-commands        : logging all commands\n    \\-tb\\-token=<TOKEN>    : setting bot token (or set TB_TOKEN variable)\n    \\-timeout=N           : setting timeout for bot (default 60 sec)\n    \\-description=<TITLE> : setting description of bot\n    \\-bind\\-addr=<ADDRESS> : address to listen incoming webhook requests\n    \\-webhook=<URL>       : url for registering a webhook\n    \\-persistent\\-users    : load/save users from file (default ~/\\.config/shell2telegram\\.json)\n    \\-users\\-db=<FILENAME> : file for store users\n    \\-cache=N             : caching command out for N seconds\n    \\-one\\-thread          : run each shell command in one thread\n    \\-public              : bot is public (don\\'t add /auth* commands)\n    \\-sh\\-timeout=N        : set timeout for execute shell command (in seconds)\n    \\-shell=\"shell\"       : shell for execute command, \"\" \\- without shell (default \"sh\")\n    \\-version\n    \\-help\n.\n.fi\n.\n.IP \"\" 0\n.\n.P\nIf not define \\-allow\\-users/\\-root\\-users options \\- authorize users via secret code from console or via chat with exists root users\\.\n.\n.P\nAll text after /chat_command will be sent to STDIN of shell command\\.\n.\n.SH \"Special chat commands\"\nfor private chats only:\n.\n.IP \"\\(bu\" 4\n\\fB/:plain_text\\fR \\- get user message without any /command\\.\n.\n.IP \"\" 0\n.\n.P\nTODO:\n.\n.IP \"\\(bu\" 4\n\\fB/:image\\fR \\- for get image from user\\. Example: \\fB/:image \\'cat > file\\.jpg; echo ok\\'\\fR\n.\n.IP \"\\(bu\" 4\n\\fB/:file\\fR \\- for get file from user\n.\n.IP \"\\(bu\" 4\n\\fB/:location\\fR \\- for get geo\\-location from user\n.\n.IP \"\" 0\n.\n.P\nPossible long\\-running shell processes (for example alarm/timer bot)\\.\n.\n.P\nAutodetect images (png/jpg/gif/bmp) out from shell command, for example: \\fB/get_image \\'cat file\\.png\\'\\fR\n.\n.P\nSetting environment variables for shell commands:\n.\n.IP \"\\(bu\" 4\nS2T_LOGIN \\- telegram @login (may be empty)\n.\n.IP \"\\(bu\" 4\nS2T_USERID \\- telegram user ID\n.\n.IP \"\\(bu\" 4\nS2T_USERNAME \\- telegram user name\n.\n.IP \"\\(bu\" 4\nS2T_CHATID \\- chat ID\n.\n.IP \"\" 0\n.\n.SH \"Modificators for bot commands\"\n.\n.IP \"\\(bu\" 4\n\\fB:desc\\fR \\- setting the description of command, \\fB/cmd:desc=\"Command name\" \\'shell cmd\\'\\fR\n.\n.IP \"\\(bu\" 4\n\\fB:vars\\fR \\- to create environment variables instead of text output to STDIN, \\fB/cmd:vars=VAR1,VAR2 \\'echo $VAR1 / $VAR2\\'\\fR\n.\n.IP \"\\(bu\" 4\n\\fB:md\\fR \\- to send message as markdown text, \\fB/cmd:md \\'echo \"*bold* and _italic_\"\\'\\fR\n.\n.IP \"\" 0\n.\n.P\nTODO:\n.\n.IP \"\\(bu\" 4\n\\fB/cmd:cron=3600\\fR — periodic exec command, \\fB/cmd:on args\\fR \\- on, \\fB/cmd:off\\fR \\- off\n.\n.IP \"\" 0\n.\n.SH \"Predefined bot commands\"\n.\n.IP \"\\(bu\" 4\n\\fB/help\\fR \\- list available commands\n.\n.IP \"\\(bu\" 4\n\\fB/auth\\fR \\- begin authorize new user\n.\n.IP \"\\(bu\" 4\n\\fB/auth <CODE>\\fR \\- authorize with code from console or from exists root user\n.\n.IP \"\\(bu\" 4\n\\fB/authroot\\fR \\- same for new root user\n.\n.IP \"\\(bu\" 4\n\\fB/authroot <CODE>\\fR \\- same for new root user\n.\n.IP \"\" 0\n.\n.P\nfor root users only:\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram stat\\fR \\- show users statistics\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram search <query>\\fR \\- search users by name/id\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram ban <user_id|@username>\\fR \\- ban user\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram exit\\fR \\- terminate bot (for run with \\-add\\-exit)\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram desc <description>\\fR \\- set bot description\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram rm </command>\\fR \\- delete command\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram broadcast_to_root <message>\\fR \\- send message to all root users in private chat\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram message_to_user <user_id|@username> <message>\\fR \\- send message to user in private chat\n.\n.IP \"\\(bu\" 4\n\\fB/shell2telegram version\\fR \\- show version\n.\n.IP \"\" 0\n.\n.SH \"Examples\"\n.\n.nf\n\n# system information\nshell2telegram /top:desc=\"System information\" \\'top \\-l 1 | head \\-10\\' /date \\'date\\' /ps \\'ps aux \\-m | head \\-20\\'\n\n# sort any input\nshell2telegram /:plain_text sort\n\n# alarm bot:\n# /alarm time_in_seconds message\nshell2telegram /alarm:vars=SLEEP,MSG \\'sleep $SLEEP; echo Hello $S2T_USERNAME; echo Alarm: $MSG\\'\n\n# sound volume control via telegram (Mac OS)\nshell2telegram /get  \\'osascript \\-e \"output volume of (get volume settings)\"\\' \\e\n               /up   \\'osascript \\-e \"set volume output volume (($(osascript \\-e \"output volume of (get volume settings)\")+10))\"\\' \\e\n               /down \\'osascript \\-e \"set volume output volume (($(osascript \\-e \"output volume of (get volume settings)\")\\-10))\"\\'\n\n# using with webhook instead of poll\nshell2telegram \\-bind\\-addr=0\\.0\\.0\\.0:8080 \\-webhook=https://bot\\.example\\.com/path/to/bot \\e\n               /date /date\n\n# command with Markdown formating, calendar in monospace font\nshell2telegram /cal:md \\'echo \"\\e`\\e`\\e`$(ncal)\\e`\\e`\\e`\"\\'\n.\n.fi\n.\n.SH \"Links\"\n.\n.IP \"\\(bu\" 4\nTelegram channel about shell2telegram \\fIhttps://telegram\\.me/shell2telegram\\fR\n.\n.IP \"\\(bu\" 4\nAbout Telegram bots \\fIhttps://core\\.telegram\\.org/bots\\fR\n.\n.IP \"\\(bu\" 4\nGolang bindings for the Telegram Bot API \\fIhttps://github\\.com/go\\-telegram\\-bot\\-api/telegram\\-bot\\-api\\fR\n.\n.IP \"\\(bu\" 4\nshell2http \\- shell commands as http\\-server \\fIhttps://github\\.com/msoap/shell2http\\fR\n.\n.IP \"\" 0\n\n"
  },
  {
    "path": "shell2telegram.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/msoap/raphanus\"\n\ttgbotapi \"gopkg.in/telegram-bot-api.v2\"\n)\n\nconst (\n\t// version - current version\n\tversion = \"1.10\"\n\n\t// DefaultBotTimeout - bot default timeout\n\tDefaultBotTimeout = 60\n\n\t// MessagesQueueSize - size of channel for bot messages\n\tMessagesQueueSize = 10\n\n\t// MaxMessageLength - max length of one bot message\n\tMaxMessageLength = 4096\n\n\t// SecondsForAutoSaveUsersToDB - save users to file every 1 min (if need)\n\tSecondsForAutoSaveUsersToDB = 60\n\n\t// DBFileName - DB json name\n\tDBFileName = \"shell2telegram.json\"\n\n\t// shell2telegram command name for get plain text without /command\n\tcmdPlainText = \"/:plain_text\"\n)\n\n// Command - one user command\ntype Command struct {\n\tshellCmd    string   // shell command\n\tdescription string   // command description for list in /help (/cmd:desc=\"Command name\")\n\tvars        []string // environment vars for user text, split by `/s+` to vars (/cmd:vars=SUBCOMMAND,ARGS)\n\tisMarkdown  bool     // send message in markdown format\n}\n\n// Commands - list of all commands\ntype Commands map[string]Command\n\n// Config - config struct\ntype Config struct {\n\ttoken                  string   // bot token\n\tbotTimeout             int      // bot timeout\n\tpredefinedAllowedUsers []string // telegram users who are allowed to chat with the bot\n\tpredefinedRootUsers    []string // telegram users, who confirms new users in their private chat\n\tdescription            string   // description of bot\n\tbindAddr               string   // bind address to listen webhook requests\n\twebhookURL             url.URL  // url for the webhook\n\tusersDB                string   // file for store users\n\tshell                  string   // custom shell\n\tcache                  int      // caching command out (in seconds)\n\tshTimeout              int      // timeout for execute shell command (in seconds)\n\taddExit                bool     // adding /shell2telegram exit command\n\tallowAll               bool     // allow all user (DANGEROUS!)\n\tlogCommands            bool     // logging all commands\n\tpersistentUsers        bool     // load/save users from file\n\tisPublicBot            bool     // bot is public (don't add /auth* commands)\n\toneThread              bool     // run each shell commands in one thread\n}\n\n// message types\nconst (\n\tmsgIsText int8 = iota\n\tmsgIsPhoto\n)\n\n// BotMessage - record for send via channel for send message to telegram chat\ntype BotMessage struct {\n\tmessage     string\n\tfileName    string\n\tphoto       []byte\n\tchatID      int\n\tmessageType int8\n\tisMarkdown  bool\n}\n\n// ----------------------------------------------------------------------------\n// get config\nfunc getConfig() (commands Commands, appConfig Config, err error) {\n\tflag.StringVar(&appConfig.token, \"tb-token\", \"\", \"setting bot `token` (or set TB_TOKEN variable)\")\n\tflag.BoolVar(&appConfig.addExit, \"add-exit\", false, \"adding \\\"/shell2telegram exit\\\" command for terminate bot (for roots only)\")\n\tflag.IntVar(&appConfig.botTimeout, \"timeout\", DefaultBotTimeout, \"setting timeout for bot (in `seconds`)\")\n\tflag.StringVar(&appConfig.bindAddr, \"bind-addr\", \"\", \"bind address to listen webhook requests, like: `0.0.0.0:8080`\")\n\tflag.Var(&urlValue{&appConfig.webhookURL}, \"webhook\", \"`url` of bot's webhook\")\n\tflag.BoolVar(&appConfig.allowAll, \"allow-all\", false, \"allow all users (DANGEROUS!)\")\n\tflag.BoolVar(&appConfig.logCommands, \"log-commands\", false, \"logging all commands\")\n\tflag.StringVar(&appConfig.description, \"description\", \"\", \"setting description of bot\")\n\tflag.BoolVar(&appConfig.persistentUsers, \"persistent-users\", false, \"load/save users from file (default ~/.config/shell2telegram.json)\")\n\tflag.StringVar(&appConfig.usersDB, \"users-db\", \"\", \"`file` for store users\")\n\tflag.IntVar(&appConfig.cache, \"cache\", 0, \"caching command out (in `seconds`)\")\n\tflag.BoolVar(&appConfig.isPublicBot, \"public\", false, \"bot is public (don't add /auth* commands)\")\n\tflag.IntVar(&appConfig.shTimeout, \"sh-timeout\", 0, \"set timeout for execute shell command (in `seconds`)\")\n\tflag.StringVar(&appConfig.shell, \"shell\", \"sh\", \"custom shell or \\\"\\\" for execute without shell\")\n\tflag.BoolVar(&appConfig.oneThread, \"one-thread\", false, \"run each shell command in one thread\")\n\tlogFilename := flag.String(\"log\", \"\", \"log `filename`, default - STDOUT\")\n\tpredefinedAllowedUsers := flag.String(\"allow-users\", \"\", \"telegram users who are allowed to chat with the bot (\\\"user1,user2\\\")\")\n\tpredefinedRootUsers := flag.String(\"root-users\", \"\", \"telegram users, who confirms new users in their private chat (\\\"user1,user2\\\")\")\n\tshowVersion := flag.Bool(\"version\", false, \"get version\")\n\n\tflag.Usage = func() {\n\t\tfmt.Printf(\"usage: %s [options] %s\\n%s\\n%s\\n\\noptions:\\n\",\n\t\t\tos.Args[0],\n\t\t\t`/chat_command \"shell command\" /chat_command2 \"shell command2\"`,\n\t\t\t\"All text after /chat_command will be sent to STDIN of shell command.\",\n\t\t\t\"If chat command is /:plain_text - get user message without any /command (for private chats only)\",\n\t\t)\n\t\tflag.PrintDefaults()\n\t\tos.Exit(0)\n\t}\n\tflag.Parse()\n\n\tif *showVersion {\n\t\tfmt.Println(version)\n\t\tos.Exit(0)\n\t}\n\n\t// setup log file\n\tif len(*logFilename) > 0 {\n\t\tfhLog, err := os.OpenFile(*logFilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"error opening log file: %v\", err)\n\t\t}\n\t\tlog.SetOutput(fhLog)\n\t}\n\n\t// setup users and roots\n\tif *predefinedAllowedUsers != \"\" {\n\t\tappConfig.predefinedAllowedUsers = strings.Split(*predefinedAllowedUsers, \",\")\n\t}\n\tif *predefinedRootUsers != \"\" {\n\t\tappConfig.predefinedRootUsers = strings.Split(*predefinedRootUsers, \",\")\n\t}\n\n\tcommands = Commands{}\n\t// need >= 2 arguments and count of it must be even\n\targs := flag.Args()\n\tif len(args) < 2 || len(args)%2 == 1 {\n\t\treturn commands, appConfig, fmt.Errorf(\"error: need pairs of /chat-command and shell-command\")\n\t}\n\n\tfor i := 0; i < len(args); i += 2 {\n\t\tpath, command, err := parseBotCommand(args[i], args[i+1]) // (/path, shell_command)\n\t\tif err != nil {\n\t\t\treturn commands, appConfig, err\n\t\t}\n\t\tcommands[path] = command\n\t}\n\n\tif appConfig.token == \"\" {\n\t\tif appConfig.token = os.Getenv(\"TB_TOKEN\"); appConfig.token == \"\" {\n\t\t\treturn commands, appConfig, fmt.Errorf(\"TB_TOKEN environment var not found. See https://core.telegram.org/bots#botfather for more information\")\n\t\t}\n\t}\n\n\treturn commands, appConfig, nil\n}\n\n// ----------------------------------------------------------------------------\nfunc sendMessage(messageSignal chan<- BotMessage, chatID int, message []byte, isMarkdown bool) {\n\tgo func() {\n\t\tvar fileName string\n\t\tfileType := http.DetectContentType(message)\n\t\tswitch fileType {\n\t\tcase \"image/png\":\n\t\t\tfileName = \"file.png\"\n\t\tcase \"image/jpeg\":\n\t\t\tfileName = \"file.jpeg\"\n\t\tcase \"image/gif\":\n\t\t\tfileName = \"file.gif\"\n\t\tcase \"image/bmp\":\n\t\t\tfileName = \"file.bmp\"\n\t\tcase \"video/mp4\":\n\t\t\t// TODO: nedded migrate to new telegram-bot-api library\n\t\t\tlog.Printf(\"not supported\")\n\t\t\treturn\n\t\tdefault:\n\t\t\tfileName = \"message\"\n\t\t}\n\n\t\tif fileName == \"message\" {\n\t\t\t// is text message\n\t\t\tmessageString := string(message)\n\t\t\tvar messagesList []string\n\n\t\t\tif len(messageString) <= MaxMessageLength {\n\t\t\t\tmessagesList = []string{messageString}\n\t\t\t} else {\n\t\t\t\tmessagesList = splitStringLinesBySize(messageString, MaxMessageLength)\n\t\t\t}\n\n\t\t\tfor _, messageChunk := range messagesList {\n\t\t\t\tmessageSignal <- BotMessage{\n\t\t\t\t\tchatID:      chatID,\n\t\t\t\t\tmessageType: msgIsText,\n\t\t\t\t\tmessage:     messageChunk,\n\t\t\t\t\tisMarkdown:  isMarkdown,\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\t\t\t// is image\n\t\t\tmessageSignal <- BotMessage{\n\t\t\t\tchatID:      chatID,\n\t\t\t\tmessageType: msgIsPhoto,\n\t\t\t\tfileName:    fileName,\n\t\t\t\tphoto:       message,\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// ----------------------------------------------------------------------------\nfunc main() {\n\tcommands, appConfig, err := getConfig()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tbot, err := tgbotapi.NewBotAPI(appConfig.token)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tlog.Printf(\"Authorized on bot account: @%s\", bot.Self.UserName)\n\n\ttgbotConfig := tgbotapi.NewUpdate(0)\n\ttgbotConfig.Timeout = appConfig.botTimeout\n\tvar botUpdatesChan <-chan tgbotapi.Update\n\tvar server *http.Server\n\n\tif appConfig.bindAddr != \"\" {\n\t\t_, err = bot.SetWebhook(tgbotapi.WebhookConfig{URL: &appConfig.webhookURL})\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\tbotUpdatesChan = bot.ListenForWebhook(appConfig.webhookURL.Path)\n\t\tserver = &http.Server{Addr: appConfig.bindAddr}\n\t\tgo func() {\n\t\t\tlog.Println(\"Listening incoming requests at \", appConfig.bindAddr)\n\t\t\tlog.Fatal(server.ListenAndServe())\n\t\t}()\n\t} else {\n\t\tbotUpdatesChan, err = bot.GetUpdatesChan(tgbotConfig)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n\n\tusers := NewUsers(appConfig)\n\tmessageSignal := make(chan BotMessage, MessagesQueueSize)\n\tvacuumTicker := time.Tick(SecondsForOldUsersBeforeVacuum * time.Second)\n\tsaveToBDTicker := make(<-chan time.Time)\n\toneThreadMutex := sync.Mutex{}\n\texitSignal := make(chan struct{})\n\tsystemExitSignal := make(chan os.Signal, 1)\n\tsignal.Notify(systemExitSignal, os.Interrupt)\n\n\tif appConfig.persistentUsers {\n\t\tsaveToBDTicker = time.Tick(SecondsForAutoSaveUsersToDB * time.Second)\n\t}\n\n\tvar cache raphanus.DB\n\tif appConfig.cache > 0 {\n\t\tcache = raphanus.New()\n\t}\n\n\t// all /shell2telegram sub-commands handlers\n\tinternalCommands := map[string]func(Ctx) string{\n\t\t\"stat\":              cmdShell2telegramStat,\n\t\t\"ban\":               cmdShell2telegramBan,\n\t\t\"search\":            cmdShell2telegramSearch,\n\t\t\"desc\":              cmdShell2telegramDesc,\n\t\t\"rm\":                cmdShell2telegramRm,\n\t\t\"exit\":              cmdShell2telegramExit,\n\t\t\"version\":           cmdShell2telegramVersion,\n\t\t\"broadcast_to_root\": cmdShell2telegramBroadcastToRoot,\n\t\t\"message_to_user\":   cmdShell2telegramMessageToUser,\n\t}\n\n\tdoExit := false\n\tfor !doExit {\n\t\tselect {\n\t\tcase telegramUpdate := <-botUpdatesChan:\n\n\t\t\tvar messageCmd, messageArgs string\n\t\t\tallUserMessage := telegramUpdate.Message.Text\n\t\t\tif len(allUserMessage) > 0 && allUserMessage[0] == '/' {\n\t\t\t\tmessageCmd, messageArgs = splitStringHalfBySpace(allUserMessage)\n\t\t\t} else {\n\t\t\t\tmessageCmd, messageArgs = cmdPlainText, allUserMessage\n\t\t\t}\n\n\t\t\tallowPlainText := false\n\t\t\tif _, ok := commands[cmdPlainText]; ok {\n\t\t\t\tallowPlainText = true\n\t\t\t}\n\n\t\t\treplayMsg := \"\"\n\n\t\t\tif len(messageCmd) > 0 && (messageCmd != cmdPlainText || allowPlainText) {\n\n\t\t\t\tusers.AddNew(telegramUpdate.Message)\n\t\t\t\tuserID := telegramUpdate.Message.From.ID\n\t\t\t\tallowExec := appConfig.allowAll || users.IsAuthorized(userID)\n\n\t\t\t\tctx := Ctx{\n\t\t\t\t\tappConfig:      &appConfig,\n\t\t\t\t\tusers:          &users,\n\t\t\t\t\tcommands:       commands,\n\t\t\t\t\tuserID:         userID,\n\t\t\t\t\tallowExec:      allowExec,\n\t\t\t\t\tmessageCmd:     messageCmd,\n\t\t\t\t\tmessageArgs:    messageArgs,\n\t\t\t\t\tmessageSignal:  messageSignal,\n\t\t\t\t\tchatID:         telegramUpdate.Message.Chat.ID,\n\t\t\t\t\texitSignal:     exitSignal,\n\t\t\t\t\tcache:          &cache,\n\t\t\t\t\toneThreadMutex: &oneThreadMutex,\n\t\t\t\t}\n\n\t\t\t\tswitch {\n\t\t\t\t// commands .................................\n\t\t\t\tcase !appConfig.isPublicBot && (messageCmd == \"/auth\" || messageCmd == \"/authroot\"):\n\t\t\t\t\treplayMsg = cmdAuth(ctx)\n\n\t\t\t\tcase messageCmd == \"/help\":\n\t\t\t\t\treplayMsg = cmdHelp(ctx)\n\n\t\t\t\tcase messageCmd == \"/shell2telegram\" && users.IsRoot(userID):\n\t\t\t\t\tvar messageSubCmd string\n\t\t\t\t\tmessageSubCmd, messageArgs = splitStringHalfBySpace(messageArgs)\n\t\t\t\t\tctx.messageArgs = messageArgs\n\t\t\t\t\tif cmdHandler, ok := internalCommands[messageSubCmd]; ok {\n\t\t\t\t\t\treplayMsg = cmdHandler(ctx)\n\t\t\t\t\t} else {\n\t\t\t\t\t\treplayMsg = \"Sub-command not found\"\n\t\t\t\t\t}\n\n\t\t\t\tcase allowExec && (allowPlainText && messageCmd == cmdPlainText || messageCmd[0] == '/'):\n\t\t\t\t\tcmdUser(ctx)\n\n\t\t\t\t} // switch for commands\n\n\t\t\t\tif appConfig.logCommands {\n\t\t\t\t\tlog.Printf(\"%s: %s\", users.String(userID), allUserMessage)\n\t\t\t\t}\n\n\t\t\t\tsendMessage(messageSignal, telegramUpdate.Message.Chat.ID, []byte(replayMsg), false)\n\t\t\t}\n\n\t\tcase botMessage := <-messageSignal:\n\t\t\tswitch {\n\t\t\tcase botMessage.messageType == msgIsText && !stringIsEmpty(botMessage.message):\n\t\t\t\tmessageConfig := tgbotapi.NewMessage(botMessage.chatID, botMessage.message)\n\t\t\t\tif botMessage.isMarkdown {\n\t\t\t\t\tmessageConfig.ParseMode = tgbotapi.ModeMarkdown\n\t\t\t\t}\n\t\t\t\t_, err = bot.Send(messageConfig)\n\t\t\tcase botMessage.messageType == msgIsPhoto && len(botMessage.photo) > 0:\n\t\t\t\tbytesPhoto := tgbotapi.FileBytes{Name: botMessage.fileName, Bytes: botMessage.photo}\n\t\t\t\t_, err = bot.Send(tgbotapi.NewPhotoUpload(botMessage.chatID, bytesPhoto))\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"failed to send message: %s\", err)\n\t\t\t}\n\n\t\tcase <-saveToBDTicker:\n\t\t\tusers.SaveToDB(appConfig.usersDB)\n\n\t\tcase <-vacuumTicker:\n\t\t\tusers.ClearOldUsers()\n\n\t\tcase <-systemExitSignal:\n\t\t\tgo func() {\n\t\t\t\texitSignal <- struct{}{}\n\t\t\t}()\n\n\t\tcase <-exitSignal:\n\t\t\tif appConfig.persistentUsers {\n\t\t\t\tusers.needSaveDB = true\n\t\t\t\tusers.SaveToDB(appConfig.usersDB)\n\t\t\t}\n\t\t\tif server != nil {\n\t\t\t\tlog.Println(server.Close())\n\t\t\t}\n\t\t\tdoExit = true\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "snapcraft.yaml",
    "content": "name: shell2telegram\nversion: '1.10'\nsummary: Telegram bot constructor from command-line\ndescription: |\n  Telegram bot constructor from command-line, written in Go.\n  Settings through two command line arguments, bot-command and shell command.\ngrade: stable\nconfinement: strict\nbase: core18\n\nparts:\n  shell2telegram:\n    plugin: go\n    go-importpath: github.com/msoap/shell2telegram\n    source: .\n    source-type: git\n\napps:\n  shell2telegram:\n    command: bin/shell2telegram\n    plugs: [network, home]\n"
  },
  {
    "path": "test-bot.Dockerfile",
    "content": "FROM msoap/shell2telegram\n\nENV TB_TOKEN=*******\nCMD [\"/date\", \"date\"]\n"
  },
  {
    "path": "users.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\ttgbotapi \"gopkg.in/telegram-bot-api.v2\"\n)\n\n// User - one telegram user who interact with bot\ntype User struct {\n\tUserID         int       `json:\"user_id\"`          // telegram UserID\n\tUserName       string    `json:\"user_name\"`        // telegram @login\n\tFirstName      string    `json:\"first_name\"`       // telegram name\n\tLastName       string    `json:\"last_name\"`        // -//-\n\tAuthCode       string    `json:\"auth_code\"`        // code for authorize\n\tAuthCodeRoot   string    `json:\"auth_code_root\"`   // code for authorize root\n\tIsAuthorized   bool      `json:\"is_authorized\"`    // user allow chat with bot\n\tIsRoot         bool      `json:\"is_root\"`          // user is root, allow authorize/ban other users, remove commands, stop bot\n\tPrivateChatID  int       `json:\"private_chat_id\"`  // last private chat with bot\n\tCounter        int       `json:\"counter\"`          // how many commands send\n\tLastAccessTime time.Time `json:\"last_access_time\"` // time of last command\n}\n\n// Users in chat\ntype Users struct {\n\tlist                   map[int]*User\n\tpredefinedAllowedUsers map[string]bool\n\tpredefinedRootUsers    map[string]bool\n\tneedSaveDB             bool // non-saved changes in list\n}\n\n// UsersDB -  save list of Users into JSON\ntype UsersDB struct {\n\tUsers    []User    `json:\"users\"`\n\tDateTime time.Time `json:\"date_time\"`\n}\n\n// SecondsForOldUsersBeforeVacuum - clear old users after 20 minutes after login\nconst SecondsForOldUsersBeforeVacuum = 1200\n\n// NewUsers - create Users object\nfunc NewUsers(appConfig Config) Users {\n\tusers := Users{\n\t\tpredefinedAllowedUsers: map[string]bool{},\n\t\tpredefinedRootUsers:    map[string]bool{},\n\t\tlist:                   map[int]*User{},\n\t\tneedSaveDB:             true,\n\t}\n\n\tif appConfig.persistentUsers {\n\t\tusers.LoadFromDB(appConfig.usersDB)\n\t}\n\n\tfor _, name := range appConfig.predefinedAllowedUsers {\n\t\tusers.predefinedAllowedUsers[name] = true\n\t}\n\tfor _, name := range appConfig.predefinedRootUsers {\n\t\tusers.predefinedAllowedUsers[name] = true\n\t\tusers.predefinedRootUsers[name] = true\n\t}\n\treturn users\n}\n\n// AddNew - add new user if not exists\nfunc (users *Users) AddNew(tgbotMessage tgbotapi.Message) {\n\tprivateChatID := 0\n\tif !tgbotMessage.Chat.IsGroup() {\n\t\tprivateChatID = tgbotMessage.Chat.ID\n\t}\n\n\tUserID := tgbotMessage.From.ID\n\tif _, ok := users.list[UserID]; ok && privateChatID > 0 && privateChatID != users.list[UserID].PrivateChatID {\n\t\tusers.list[UserID].PrivateChatID = privateChatID\n\t\tusers.needSaveDB = true\n\t} else if !ok {\n\t\tusers.list[UserID] = &User{\n\t\t\tUserID:        UserID,\n\t\t\tUserName:      tgbotMessage.From.UserName,\n\t\t\tFirstName:     tgbotMessage.From.FirstName,\n\t\t\tLastName:      tgbotMessage.From.LastName,\n\t\t\tIsAuthorized:  users.predefinedAllowedUsers[tgbotMessage.From.UserName],\n\t\t\tIsRoot:        users.predefinedRootUsers[tgbotMessage.From.UserName],\n\t\t\tPrivateChatID: privateChatID,\n\t\t}\n\t\tusers.needSaveDB = true\n\t}\n\n\t// collect stat\n\tusers.list[UserID].LastAccessTime = time.Now()\n\tif users.list[UserID].IsAuthorized {\n\t\tusers.list[UserID].Counter++\n\t}\n}\n\n// DoLogin - generate secret code\nfunc (users *Users) DoLogin(userID int, forRoot bool) string {\n\tcode := getRandomCode()\n\tif forRoot {\n\t\tusers.list[userID].IsRoot = false\n\t\tusers.list[userID].AuthCodeRoot = code\n\t} else {\n\t\tusers.list[userID].IsAuthorized = false\n\t\tusers.list[userID].AuthCode = code\n\t}\n\tusers.needSaveDB = true\n\n\treturn code\n}\n\n// SetAuthorized - set user authorized or authorized as root\nfunc (users *Users) SetAuthorized(userID int, forRoot bool) {\n\tusers.list[userID].IsAuthorized = true\n\tusers.list[userID].AuthCode = \"\"\n\tif forRoot {\n\t\tusers.list[userID].IsRoot = true\n\t\tusers.list[userID].AuthCodeRoot = \"\"\n\t}\n\tusers.needSaveDB = true\n}\n\n// IsValidCode - check secret code for user\nfunc (users Users) IsValidCode(userID int, code string, forRoot bool) bool {\n\tvar result bool\n\tif forRoot {\n\t\tresult = code != \"\" && code == users.list[userID].AuthCodeRoot\n\t} else {\n\t\tresult = code != \"\" && code == users.list[userID].AuthCode\n\t}\n\treturn result\n}\n\n// IsAuthorized - check user is authorized\nfunc (users Users) IsAuthorized(userID int) bool {\n\tisAuthorized := false\n\tif _, ok := users.list[userID]; ok && users.list[userID].IsAuthorized {\n\t\tisAuthorized = true\n\t}\n\n\treturn isAuthorized\n}\n\n// IsRoot - check user is root\nfunc (users Users) IsRoot(userID int) bool {\n\tisRoot := false\n\tif _, ok := users.list[userID]; ok && users.list[userID].IsRoot {\n\t\tisRoot = true\n\t}\n\n\treturn isRoot\n}\n\n// BroadcastForRoots - send message to all root users\nfunc (users Users) BroadcastForRoots(messageSignal chan<- BotMessage, message string, excludeID int) {\n\tfor userID, user := range users.list {\n\t\tif user.IsRoot && user.PrivateChatID > 0 && (excludeID == 0 || excludeID != userID) {\n\t\t\tsendMessage(messageSignal, user.PrivateChatID, []byte(message), false)\n\t\t}\n\t}\n}\n\n// String - format user name\nfunc (users Users) String(userID int) string {\n\tresult := fmt.Sprintf(\"%s %s\", users.list[userID].FirstName, users.list[userID].LastName)\n\tif users.list[userID].UserName != \"\" {\n\t\tresult += fmt.Sprintf(\" (@%s)\", users.list[userID].UserName)\n\t}\n\treturn result\n}\n\n// StringVerbose - format user name with all fields\nfunc (users Users) StringVerbose(userID int) string {\n\tuser := users.list[userID]\n\tresult := fmt.Sprintf(\"%s: id: %d, auth: %v, root: %v, count: %d, last: %v\",\n\t\tusers.String(userID),\n\t\tuserID,\n\t\tuser.IsAuthorized,\n\t\tuser.IsRoot,\n\t\tuser.Counter,\n\t\tuser.LastAccessTime.Format(\"2006-01-02 15:04:05\"),\n\t)\n\treturn result\n}\n\n// ClearOldUsers - clear old users without login\nfunc (users *Users) ClearOldUsers() {\n\tfor id, user := range users.list {\n\t\tif !user.IsAuthorized && !user.IsRoot && user.Counter == 0 &&\n\t\t\ttime.Since(user.LastAccessTime).Seconds() > SecondsForOldUsersBeforeVacuum {\n\t\t\tlog.Printf(\"Vacuum: %d, %s\", id, users.String(id))\n\t\t\tdelete(users.list, id)\n\t\t\tusers.needSaveDB = true\n\t\t}\n\t}\n}\n\n// GetUserIDByName - find user by login\nfunc (users Users) GetUserIDByName(userName string) int {\n\tuserID := 0\n\tfor id, user := range users.list {\n\t\tif userName == user.UserName {\n\t\t\tuserID = id\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn userID\n}\n\n// BanUser - ban user by ID\nfunc (users *Users) BanUser(userID int) bool {\n\n\tif _, ok := users.list[userID]; ok {\n\t\tusers.list[userID].IsAuthorized = false\n\t\tusers.list[userID].IsRoot = false\n\t\tif users.list[userID].UserName != \"\" {\n\t\t\tdelete(users.predefinedAllowedUsers, users.list[userID].UserName)\n\t\t\tdelete(users.predefinedRootUsers, users.list[userID].UserName)\n\t\t}\n\t\tusers.needSaveDB = true\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// Search - search users\nfunc (users Users) Search(query string) (result []int) {\n\tqueryUserID, _ := strconv.Atoi(query)\n\tquery = strings.ToLower(query)\n\tqueryAsLogin := cleanUserName(query)\n\n\tfor userID, user := range users.list {\n\t\tif queryUserID == userID ||\n\t\t\tstrings.Contains(strings.ToLower(user.UserName), queryAsLogin) ||\n\t\t\tstrings.Contains(strings.ToLower(user.FirstName), query) ||\n\t\t\tstrings.Contains(strings.ToLower(user.LastName), query) {\n\t\t\tresult = append(result, userID)\n\t\t}\n\t}\n\n\treturn result\n}\n\n// FindByIDOrUserName - find users or by ID or by @name\nfunc (users Users) FindByIDOrUserName(userName string) int {\n\tuserID, err := strconv.Atoi(userName)\n\tif err == nil {\n\t\tif _, ok := users.list[userID]; !ok {\n\t\t\tuserID = 0\n\t\t}\n\t} else {\n\t\tuserName = cleanUserName(userName)\n\t\tuserID = users.GetUserIDByName(userName)\n\t}\n\n\treturn userID\n}\n\n// SendMessageToPrivate - send message to user to private chat\nfunc (users Users) SendMessageToPrivate(messageSignal chan<- BotMessage, userID int, message string) bool {\n\tif user, ok := users.list[userID]; ok && user.PrivateChatID > 0 {\n\t\tsendMessage(messageSignal, user.PrivateChatID, []byte(message), false)\n\t\treturn true\n\t}\n\treturn false\n}\n\n// LoadFromDB - load users list from json file\nfunc (users *Users) LoadFromDB(usersDBFile string) {\n\tusersList := UsersDB{}\n\n\tfileNamePath := getDBFilePath(usersDBFile, false)\n\tusersJSON, err := ioutil.ReadFile(fileNamePath)\n\tif err == nil {\n\t\tif err = json.Unmarshal(usersJSON, &usersList); err == nil {\n\t\t\tfor _, user := range usersList.Users {\n\t\t\t\tuser := user\n\t\t\t\tusers.list[user.UserID] = &user\n\t\t\t}\n\t\t}\n\t}\n\tif err == nil {\n\t\tlog.Printf(\"Loaded usersDB json from: %s, %d users\", fileNamePath, len(usersList.Users))\n\t} else {\n\t\tlog.Printf(\"Load usersDB (%s) error: %s\", fileNamePath, err)\n\t}\n\n\tusers.needSaveDB = false\n}\n\n// SaveToDB - save users list to json file\nfunc (users *Users) SaveToDB(usersDBFile string) {\n\tif users.needSaveDB {\n\t\tusersList := UsersDB{\n\t\t\tUsers:    []User{},\n\t\t\tDateTime: time.Now(),\n\t\t}\n\t\tfor _, user := range users.list {\n\t\t\tusersList.Users = append(usersList.Users, *user)\n\t\t}\n\n\t\tfileNamePath := getDBFilePath(usersDBFile, true)\n\t\tjsonBytes, err := json.MarshalIndent(usersList, \"\", \"  \")\n\t\tif err == nil {\n\t\t\terr = ioutil.WriteFile(fileNamePath, jsonBytes, 0644)\n\t\t}\n\n\t\tif err == nil {\n\t\t\tlog.Printf(\"Saved usersDB json to: %s\", fileNamePath)\n\t\t\tusers.needSaveDB = false\n\t\t} else {\n\t\t\tlog.Printf(\"Save usersDB (%s) error: %s\", fileNamePath, err)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "utils.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\tshellwords \"github.com/mattn/go-shellwords\"\n\t\"github.com/msoap/raphanus\"\n\traphanuscommon \"github.com/msoap/raphanus/common\"\n)\n\n// codeBytesLength - length of random code in bytes\nconst codeBytesLength = 15\n\n// exec shell commands with text to STDIN\nfunc execShell(shellCmd, input string, varsNames []string, userID, chatID int, userName, userDisplayName string, cache *raphanus.DB, cacheTTL int, config *Config) (result []byte) {\n\tcacheKey := shellCmd + \"/\" + input\n\tif cacheTTL > 0 {\n\t\tif cacheData, err := cache.GetBytes(cacheKey); err != raphanuscommon.ErrKeyNotExists && err != nil {\n\t\t\tlog.Printf(\"get from cache failed: %s\", err)\n\t\t} else if err == nil {\n\t\t\t// cache hit\n\t\t\treturn cacheData\n\t\t}\n\t}\n\n\tshell, params, err := getShellAndParams(shellCmd, config.shell, runtime.GOOS == \"windows\")\n\tif err != nil {\n\t\tlog.Print(\"parse shell failed: \", err)\n\t\treturn nil\n\t}\n\n\tctx := context.Background()\n\tif config.shTimeout > 0 {\n\t\tvar cancelFn context.CancelFunc\n\t\tctx, cancelFn = context.WithTimeout(ctx, time.Duration(config.shTimeout)*time.Second)\n\t\tdefer cancelFn()\n\t}\n\n\tosExecCommand := exec.CommandContext(ctx, shell, params...) // #nosec\n\tosExecCommand.Stderr = os.Stderr\n\n\t// copy variables from parent process\n\tosExecCommand.Env = append(osExecCommand.Env, os.Environ()...)\n\n\tif input != \"\" {\n\t\tif len(varsNames) > 0 {\n\t\t\t// set user input to shell vars\n\t\t\targuments := regexp.MustCompile(`\\s+`).Split(input, len(varsNames))\n\t\t\tfor i, arg := range arguments {\n\t\t\t\tosExecCommand.Env = append(osExecCommand.Env, fmt.Sprintf(\"%s=%s\", varsNames[i], arg))\n\t\t\t}\n\t\t} else {\n\t\t\tvar stdin io.WriteCloser\n\t\t\terrExec := errChain(func() (err error) {\n\t\t\t\tstdin, err = osExecCommand.StdinPipe()\n\t\t\t\treturn err\n\t\t\t}, func() error {\n\t\t\t\t_, err = io.WriteString(stdin, input)\n\t\t\t\treturn err\n\t\t\t}, func() error {\n\t\t\t\treturn stdin.Close()\n\t\t\t})\n\t\t\tif errExec != nil {\n\t\t\t\tlog.Print(\"get STDIN error: \", err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// set S2T_* env vars\n\ts2tVariables := [...]struct{ name, value string }{\n\t\t{\"S2T_LOGIN\", userName},\n\t\t{\"S2T_USERID\", strconv.Itoa(userID)},\n\t\t{\"S2T_USERNAME\", userDisplayName},\n\t\t{\"S2T_CHATID\", strconv.Itoa(chatID)},\n\t}\n\tfor _, row := range s2tVariables {\n\t\tosExecCommand.Env = append(osExecCommand.Env, fmt.Sprintf(\"%s=%s\", row.name, row.value))\n\t}\n\n\tshellOut, err := osExecCommand.Output()\n\tif err != nil {\n\t\tlog.Print(\"exec error: \", err)\n\t\tresult = []byte(fmt.Sprintf(\"exec error: %s\", err))\n\t} else {\n\t\tresult = shellOut\n\t}\n\n\tif cacheTTL > 0 {\n\t\tif err := cache.SetBytes(cacheKey, result, cacheTTL); err != nil {\n\t\t\tlog.Printf(\"set to cache failed: %s\", err)\n\t\t}\n\t}\n\n\treturn result\n}\n\n// errChain - handle errors on few functions\nfunc errChain(chainFuncs ...func() error) error {\n\tfor _, fn := range chainFuncs {\n\t\tif err := fn(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// return 2 strings, second=\"\" if string don't contain space\nfunc splitStringHalfBySpace(str string) (one, two string) {\n\tarray := regexp.MustCompile(`\\s+`).Split(str, 2)\n\tone, two = array[0], \"\"\n\tif len(array) > 1 {\n\t\ttwo = array[1]\n\t}\n\n\treturn one, two\n}\n\n// cleanUserName - remove @ from telegram username\nfunc cleanUserName(in string) string {\n\treturn regexp.MustCompile(\"@\").ReplaceAllLiteralString(in, \"\")\n}\n\n// getRandomCode - generate random code for authorize user\nfunc getRandomCode() string {\n\tbuffer := make([]byte, codeBytesLength)\n\t_, err := rand.Read(buffer)\n\tif err != nil {\n\t\tlog.Fatalf(\"Get code error: %s\", err)\n\t}\n\n\treturn base64.URLEncoding.EncodeToString(buffer)\n}\n\n// parseBotCommand - parse command-line arguments for one bot command\nfunc parseBotCommand(pathRaw, shellCmd string) (path string, command Command, err error) {\n\tif len(pathRaw) == 0 || pathRaw[0] != '/' {\n\t\treturn \"\", command, fmt.Errorf(\"error: path %s don't starts with /\", pathRaw)\n\t}\n\tif stringIsEmpty(shellCmd) {\n\t\treturn \"\", command, fmt.Errorf(\"error: shell command cannot be empty\")\n\t}\n\n\tparseAttrFn := func(varsParts []string) (command Command, err error) {\n\t\tfor _, oneVar := range varsParts {\n\t\t\toneVarParts := regexp.MustCompile(\"=\").Split(oneVar, 2)\n\t\t\tif len(oneVarParts) == 1 && oneVarParts[0] == \"md\" {\n\t\t\t\tcommand.isMarkdown = true\n\t\t\t} else if len(oneVarParts) != 2 {\n\t\t\t\terr = fmt.Errorf(\"error: parse command modificators: %s\", oneVar)\n\t\t\t\treturn\n\t\t\t} else if oneVarParts[0] == \"desc\" {\n\t\t\t\tcommand.description = oneVarParts[1]\n\t\t\t\tif command.description == \"\" {\n\t\t\t\t\terr = fmt.Errorf(\"error: command description cannot be empty\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else if oneVarParts[0] == \"vars\" {\n\t\t\t\tcommand.vars = regexp.MustCompile(\",\").Split(oneVarParts[1], -1)\n\t\t\t\tfor _, oneVarName := range command.vars {\n\t\t\t\t\tif oneVarName == \"\" {\n\t\t\t\t\t\terr = fmt.Errorf(\"error: var name cannot be empty\")\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\terr = fmt.Errorf(\"error: parse command modificators, not found %s\", oneVarParts[0])\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\treturn command, nil\n\t}\n\n\tpathParts := regexp.MustCompile(\":\").Split(pathRaw, -1)\n\tswitch {\n\tcase len(pathParts) == 1:\n\t\t// /, /cmd\n\t\tpath = pathParts[0]\n\tcase pathParts[0] == \"/\" && regexp.MustCompile(\"^(plain_text|image)$\").MatchString(pathParts[1]):\n\t\t// /:plain_text, /:image, /:plain_text:desc=name\n\t\tpath = \"/:\" + pathParts[1]\n\t\tif pathParts[1] == \"image\" {\n\t\t\treturn \"\", command, fmt.Errorf(\"/:image not implemented\")\n\t\t}\n\t\tif len(pathParts) > 2 {\n\t\t\tcommand, err = parseAttrFn(pathParts[2:])\n\t\t}\n\tcase len(pathParts) > 1:\n\t\t// commands with modificators :desc, :vars\n\t\tpath = pathParts[0]\n\t\tcommand, err = parseAttrFn(pathParts[1:])\n\t}\n\tif err != nil {\n\t\treturn \"\", command, err\n\t}\n\n\tcommand.shellCmd = shellCmd\n\n\treturn path, command, nil\n}\n\n// stringIsEmpty - check string is empty\nfunc stringIsEmpty(str string) bool {\n\tisEmpty, _ := regexp.MatchString(`^\\s*$`, str)\n\treturn isEmpty\n}\n\n// split string by chunks less maxSize size (whole rows)\nfunc splitStringLinesBySize(input string, maxSize int) []string {\n\tresult := []string{}\n\tparts := regexp.MustCompile(\"\\n\").Split(input, -1)\n\tchunks := []string{parts[0]}\n\tchunkSize := len(parts[0])\n\n\tfor _, part := range parts[1:] {\n\t\t// current + \"\\n\" + next > maxSize\n\t\tif chunkSize+1+len(part) > maxSize {\n\t\t\tresult = append(result, strings.Join(chunks, \"\\n\"))\n\t\t\tchunks = []string{part}\n\t\t\tchunkSize = len(part)\n\t\t} else {\n\t\t\tchunks = append(chunks, part)\n\t\t\tchunkSize += 1 + len(part)\n\t\t}\n\t}\n\tif len(chunks) > 0 {\n\t\tresult = append(result, strings.Join(chunks, \"\\n\"))\n\t}\n\n\treturn result\n}\n\n// create dir if it is not exists\nfunc createDirIfNeed(dir string) {\n\tif _, err := os.Stat(dir); err != nil {\n\t\terr = os.MkdirAll(dir, 0700)\n\t\tif err != nil {\n\t\t\tlog.Fatal(\"create dir error:\", dir)\n\t\t}\n\t}\n}\n\n// get home dir\nfunc getOsUserHomeDir() string {\n\thomeDir := os.Getenv(\"HOME\")\n\tif runtime.GOOS == \"windows\" {\n\t\thomeDir = os.Getenv(\"APPDATA\")\n\t}\n\treturn homeDir\n}\n\n// read default or user db file name\nfunc getDBFilePath(usersDBFile string, needCreateDir bool) (fileName string) {\n\tif usersDBFile == \"\" {\n\t\tdirName := getOsUserHomeDir() + string(os.PathSeparator) + \".config\"\n\t\tif needCreateDir {\n\t\t\tcreateDirIfNeed(dirName)\n\t\t}\n\t\tfileName = dirName + string(os.PathSeparator) + DBFileName\n\t} else {\n\t\tfileName = usersDBFile\n\t}\n\n\treturn fileName\n}\n\n// ------------------------------------------------------------------\n// getShellAndParams - get default shell and command\nfunc getShellAndParams(cmd string, customShell string, isWindows bool) (shell string, params []string, err error) {\n\tshell, params = \"sh\", []string{\"-c\", cmd}\n\tif isWindows {\n\t\tshell, params = \"cmd\", []string{\"/C\", cmd}\n\t}\n\n\t// custom shell\n\tswitch {\n\tcase customShell != \"sh\" && customShell != \"\":\n\t\tshell = customShell\n\tcase customShell == \"\":\n\t\tcmdLine, err := shellwords.Parse(cmd)\n\t\tif err != nil {\n\t\t\treturn shell, params, fmt.Errorf(\"failed to parse %q: %s\", cmd, err)\n\t\t}\n\n\t\tshell, params = cmdLine[0], cmdLine[1:]\n\t}\n\n\treturn shell, params, nil\n}\n\n// ------------------------------\ntype urlValue struct {\n\tURL *url.URL\n}\n\nfunc (v urlValue) String() string {\n\tif v.URL != nil {\n\t\treturn v.URL.String()\n\t}\n\treturn \"\"\n}\n\nfunc (v urlValue) Set(s string) error {\n\tu, err := url.Parse(s)\n\tif err != nil {\n\t\treturn err\n\t} else if u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn fmt.Errorf(\"missing host or scheme in '%s'\", s)\n\t}\n\n\t*v.URL = *u\n\treturn nil\n}\n"
  },
  {
    "path": "utils_test.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc Test_splitStringHalfBySpace(t *testing.T) {\n\tdata := []struct {\n\t\tin             string\n\t\toutOne, outTwo string\n\t}{\n\t\t{\n\t\t\t\"/cmd args\",\n\t\t\t\"/cmd\", \"args\",\n\t\t}, {\n\t\t\t\"/cmd   args\",\n\t\t\t\"/cmd\", \"args\",\n\t\t}, {\n\t\t\t\"/cmd\",\n\t\t\t\"/cmd\", \"\",\n\t\t}, {\n\t\t\t\"plain text\",\n\t\t\t\"plain\", \"text\",\n\t\t}, {\n\t\t\t\"plain     text\",\n\t\t\t\"plain\", \"text\",\n\t\t}, {\n\t\t\t\"\",\n\t\t\t\"\", \"\",\n\t\t},\n\t}\n\n\tfor _, item := range data {\n\t\tone, two := splitStringHalfBySpace(item.in)\n\t\tif !(one == item.outOne && two == item.outTwo) {\n\t\t\tt.Errorf(\"Failing for \\\"%s\\\"\\nexpected: (%#v, %#v)\\nreal: (%#v, %#v)\\n\", item.in, item.outOne, item.outTwo, one, two)\n\t\t}\n\t}\n}\n\nfunc Test_cleanUserName(t *testing.T) {\n\tdata := []struct {\n\t\tin  string\n\t\tout string\n\t}{\n\t\t{\n\t\t\t\"1234\",\n\t\t\t\"1234\",\n\t\t}, {\n\t\t\t\"name\",\n\t\t\t\"name\",\n\t\t}, {\n\t\t\t\"@name\",\n\t\t\t\"name\",\n\t\t}, {\n\t\t\t\" name@str \",\n\t\t\t\" namestr \",\n\t\t}, {\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t},\n\t}\n\n\tfor _, item := range data {\n\t\tout := cleanUserName(item.in)\n\t\tif out != item.out {\n\t\t\tt.Errorf(\"Failing for \\\"%s\\\"\\nexpected: %s, real: %s\\n\", item.in, item.out, out)\n\t\t}\n\t}\n}\n\nfunc Test_parseBotCommand(t *testing.T) {\n\tdata := []struct {\n\t\t// in\n\t\tpathRaw, shellCmd string\n\t\t// out\n\t\tpath    string\n\t\tcommand Command\n\t\terrFunc error\n\t}{\n\t\t{\n\t\t\tpathRaw:  \"/cmd\",\n\t\t\tshellCmd: \"ls\",\n\t\t\t// out\n\t\t\tpath: \"/cmd\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"ls\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tvars:        nil,\n\t\t\t\tisMarkdown:  false,\n\t\t\t},\n\t\t\terrFunc: nil,\n\t\t},\n\t\t{\n\t\t\tpathRaw:  \"/\",\n\t\t\tshellCmd: \"ls\",\n\t\t\t// out\n\t\t\tpath: \"/\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"ls\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tvars:        nil,\n\t\t\t\tisMarkdown:  false,\n\t\t\t},\n\t\t\terrFunc: nil,\n\t\t},\n\t\t// empty shell command\n\t\t{\n\t\t\tpathRaw:  \"/cmd\",\n\t\t\tshellCmd: \"\",\n\t\t\t// out\n\t\t\tpath: \"\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tvars:        nil,\n\t\t\t\tisMarkdown:  false,\n\t\t\t},\n\t\t\terrFunc: fmt.Errorf(\"error\"),\n\t\t},\n\t\t{\n\t\t\tpathRaw:  \"/cmd:vars=VAR1,VAR2:desc=Command name\",\n\t\t\tshellCmd: \"ls\",\n\t\t\t// out\n\t\t\tpath: \"/cmd\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"ls\",\n\t\t\t\tdescription: \"Command name\",\n\t\t\t\tvars:        []string{\"VAR1\", \"VAR2\"},\n\t\t\t\tisMarkdown:  false,\n\t\t\t},\n\t\t\terrFunc: nil,\n\t\t},\n\t\t{\n\t\t\t// markdown test\n\t\t\tpathRaw:  \"/cmd:vars=VAR1,VAR2:desc=Command name:md\",\n\t\t\tshellCmd: \"ls\",\n\t\t\t// out\n\t\t\tpath: \"/cmd\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"ls\",\n\t\t\t\tdescription: \"Command name\",\n\t\t\t\tvars:        []string{\"VAR1\", \"VAR2\"},\n\t\t\t\tisMarkdown:  true,\n\t\t\t},\n\t\t\terrFunc: nil,\n\t\t},\n\t\t{\n\t\t\tpathRaw:  \"/:plain_text\",\n\t\t\tshellCmd: \"ls\",\n\t\t\t// out\n\t\t\tpath: \"/:plain_text\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"ls\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tvars:        nil,\n\t\t\t\tisMarkdown:  false,\n\t\t\t},\n\t\t\terrFunc: nil,\n\t\t},\n\t\t{\n\t\t\tpathRaw:  \"/:image\",\n\t\t\tshellCmd: \"ls\",\n\t\t\t// out\n\t\t\tpath: \"\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"\",\n\t\t\t\tdescription: \"\",\n\t\t\t\tvars:        nil,\n\t\t\t\tisMarkdown:  false,\n\t\t\t},\n\t\t\terrFunc: fmt.Errorf(\"/:image not implemented\"),\n\t\t},\n\t\t{\n\t\t\tpathRaw:  \"/:plain_text:desc=Name\",\n\t\t\tshellCmd: \"ls\",\n\t\t\t// out\n\t\t\tpath: \"/:plain_text\",\n\t\t\tcommand: Command{\n\t\t\t\tshellCmd:    \"ls\",\n\t\t\t\tdescription: \"Name\",\n\t\t\t\tvars:        nil,\n\t\t\t\tisMarkdown:  false,\n\t\t\t},\n\t\t\terrFunc: nil,\n\t\t},\n\t}\n\n\tfor _, item := range data {\n\t\tpath, command, errFunc := parseBotCommand(item.pathRaw, item.shellCmd)\n\t\tcommandMust := fmt.Sprintf(\"%#v\", item.command)\n\t\tcommandGet := fmt.Sprintf(\"%#v\", command)\n\n\t\tif path != item.path || ((errFunc == nil) != (item.errFunc == nil) || commandGet != commandMust) {\n\t\t\tt.Errorf(\"Failing for %v (path: %s)\\nMust: %s\\nGot:  %#v\\n\", item, path, commandMust, command)\n\t\t}\n\t}\n\n\tinvalidPaths := []string{\n\t\t\"\",\n\t\t\" \",\n\t\t\"NotValidPath\",\n\t\t\" /cmd\",\n\t\t\"/:aaa\",\n\t\t\"/cmd:aaa=23\",\n\t\t\"/cmd:aaa\",\n\t\t\"/cmd:desc\",\n\t\t\"/cmd:desc=\",\n\t\t\"/cmd:vars=,,,,\",\n\t}\n\tfor _, path := range invalidPaths {\n\t\t_, _, errFunc := parseBotCommand(path, \"ls\")\n\t\tif errFunc == nil {\n\t\t\tt.Errorf(\"Failing check invalid path for: %s\", path)\n\t\t}\n\t}\n}\n\nfunc Test_stringIsEmpty(t *testing.T) {\n\tdata := []struct {\n\t\tin  string\n\t\tout bool\n\t}{\n\t\t{\n\t\t\t\"1234\",\n\t\t\tfalse,\n\t\t}, {\n\t\t\t\" str \",\n\t\t\tfalse,\n\t\t}, {\n\t\t\t\"\",\n\t\t\ttrue,\n\t\t}, {\n\t\t\t\"  \",\n\t\t\ttrue,\n\t\t}, {\n\t\t\t\"\\n\",\n\t\t\ttrue,\n\t\t}, {\n\t\t\t\"  \\ndew\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, item := range data {\n\t\tout := stringIsEmpty(item.in)\n\t\tif out != item.out {\n\t\t\tt.Errorf(\"Failing for %#v\\nexpected: %v, real: %v\\n\", item.in, item.out, out)\n\t\t}\n\t}\n}\n\nfunc Test_splitStringLinesBySize(t *testing.T) {\n\tdata := []struct {\n\t\tin      string\n\t\tmaxSize int\n\t\tout     []string\n\t}{\n\t\t{\n\t\t\t\"12345\",\n\t\t\t6,\n\t\t\t[]string{\"12345\"},\n\t\t}, {\n\t\t\t\"12345\\n67890\",\n\t\t\t11,\n\t\t\t[]string{\"12345\\n67890\"},\n\t\t}, {\n\t\t\t\"1234567890\\n1234567890\",\n\t\t\t3,\n\t\t\t[]string{\"1234567890\", \"1234567890\"},\n\t\t}, {\n\t\t\t\"12\\n34\\n56\\n78\\n90\",\n\t\t\t6,\n\t\t\t[]string{\"12\\n34\", \"56\\n78\", \"90\"},\n\t\t}, {\n\t\t\t\"12\\n34aaaaaaaaaaaaa\\n56\\n78\\n90\",\n\t\t\t6,\n\t\t\t[]string{\"12\", \"34aaaaaaaaaaaaa\", \"56\\n78\", \"90\"},\n\t\t},\n\t}\n\n\tfor _, item := range data {\n\t\tout := splitStringLinesBySize(item.in, item.maxSize)\n\t\tmustOut := fmt.Sprintf(\"%#v\", item.out)\n\t\tgetOut := fmt.Sprintf(\"%#v\", out)\n\t\tif mustOut != getOut {\n\t\t\tt.Errorf(\"Failing for %#v (by %d)\\nexpected: %s, real: %s\\n\", item.in, item.maxSize, mustOut, getOut)\n\t\t}\n\t}\n}\n\nfunc Test_getRandomCode(t *testing.T) {\n\trnd := getRandomCode()\n\tif len(rnd) == 0 {\n\t\tt.Errorf(\"getRandomCode() failed\")\n\t}\n}\n\nfunc Test_getOsUserHomeDir(t *testing.T) {\n\tuserDir := getOsUserHomeDir()\n\tif len(userDir) == 0 {\n\t\tt.Errorf(\"1. getOsUserHomeDir() failed\")\n\t}\n\t_, err := os.Stat(userDir)\n\tif err != nil {\n\t\tt.Errorf(\"2. getOsUserHomeDir() failed\")\n\t}\n}\n\nfunc Test_errChain(t *testing.T) {\n\terr := errChain()\n\tif err != nil {\n\t\tt.Errorf(\"1. errChain() empty failed\")\n\t}\n\n\terr = errChain(func() error { return nil })\n\tif err != nil {\n\t\tt.Errorf(\"2. errChain() failed\")\n\t}\n\n\terr = errChain(func() error { return nil }, func() error { return nil })\n\tif err != nil {\n\t\tt.Errorf(\"3. errChain() failed\")\n\t}\n\n\terr = errChain(func() error { return fmt.Errorf(\"error\") })\n\tif err == nil {\n\t\tt.Errorf(\"4. errChain() failed\")\n\t}\n\n\terr = errChain(func() error { return nil }, func() error { return fmt.Errorf(\"error\") })\n\tif err == nil {\n\t\tt.Errorf(\"5. errChain() failed\")\n\t}\n\n\tvar1 := false\n\terr = errChain(func() error { return fmt.Errorf(\"error\") }, func() error { var1 = true; return nil })\n\tif err == nil || var1 {\n\t\tt.Errorf(\"6. errChain() failed\")\n\t}\n}\n\nfunc Test_getShellAndParams(t *testing.T) {\n\tshell, params, err := getShellAndParams(\"ls\", \"sh\", false)\n\tif shell != \"sh\" || !reflect.DeepEqual(params, []string{\"-c\", \"ls\"}) || err != nil {\n\t\tt.Errorf(\"1. getShellAndParams() failed\")\n\t}\n\n\tshell, params, err = getShellAndParams(\"ls\", \"bash\", false)\n\tif shell != \"bash\" || !reflect.DeepEqual(params, []string{\"-c\", \"ls\"}) || err != nil {\n\t\tt.Errorf(\"3. getShellAndParams() failed\")\n\t}\n\n\tshell, params, err = getShellAndParams(\"ls -l -a\", \"\", false)\n\tif shell != \"ls\" || !reflect.DeepEqual(params, []string{\"-l\", \"-a\"}) || err != nil {\n\t\tt.Errorf(\"4. getShellAndParams() failed\")\n\t}\n\n\tshell, params, err = getShellAndParams(\"ls -l 'a b'\", \"\", false)\n\tif shell != \"ls\" || !reflect.DeepEqual(params, []string{\"-l\", \"a b\"}) || err != nil {\n\t\tt.Errorf(\"5. getShellAndParams() failed\")\n\t}\n\n\t_, _, err = getShellAndParams(\"ls '-l\", \"\", false)\n\tif err == nil {\n\t\tt.Errorf(\"6. getShellAndParams() failed\")\n\t}\n}\n\nfunc Test_flagURL(t *testing.T) {\n\tdata := []struct {\n\t\tin  string\n\t\terr string\n\t}{\n\t\t{\"http://example.com\", \"\"},\n\t\t{\"https://bot.example.com/path/to/bot\", \"\"},\n\t\t{\"https://\", \"missing host or scheme in 'https://'\"},\n\t\t{\"localhost\", \"missing host or scheme in 'localhost'\"},\n\t}\n\n\tu := urlValue{&url.URL{}}\n\tfor _, item := range data {\n\t\tvar errStr string\n\t\terr := u.Set(item.in)\n\t\tif err != nil {\n\t\t\terrStr = err.Error()\n\t\t}\n\n\t\tif errStr != item.err {\n\t\t\tt.Errorf(\"Failing for \\\"%s\\\"\\nexpected: (%#v)\\nreal: (%#v)\\n\", item.in, item.err, err)\n\t\t}\n\t}\n}\n"
  }
]