Repository: msoap/shell2telegram Branch: master Commit: e836d1a12505 Files: 19 Total size: 73.9 KB Directory structure: gitextract_77deuaep/ ├── .github/ │ └── workflows/ │ ├── docker.yml │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── commands.go ├── go.mod ├── go.sum ├── shell2telegram.1 ├── shell2telegram.go ├── snapcraft.yaml ├── test-bot.Dockerfile ├── users.go ├── utils.go └── utils_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/docker.yml ================================================ name: Publish Docker image on: push: tags: - 'v*' jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest steps: - name: Check out the repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: | msoap/shell2telegram tags: | type=semver,pattern={{version}} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Push to Docker Hub uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v6 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: branches: [ master ] pull_request: branches: [ master ] schedule: - cron: '0 12 * * 0' jobs: build: runs-on: ubuntu-latest strategy: matrix: go: ['1.24.x', '1.25.x'] steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Install errcheck run: go install github.com/kisielk/errcheck@latest - name: errcheck run: errcheck -verbose ./... - name: gofmt check run: diff <(gofmt -d .) <(echo -n "") - name: Test run: go test -race -v ./... - name: Coveralls if: ${{ startsWith(matrix.go, '1.25') && github.event_name == 'push' }} env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go install github.com/mattn/goveralls@latest && \ go test -covermode=count -coverprofile=profile.cov ./... && \ goveralls -coverprofile=profile.cov -service=github ================================================ FILE: .github/workflows/release.yml ================================================ name: goreleaser on: push: tags: - 'v*' permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.x - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Post release run: ls -l ./dist/* ================================================ FILE: .gitignore ================================================ tags shell2telegram ================================================ FILE: .goreleaser.yml ================================================ release: name_template: "{{ .Version }} - {{ .Date }}" draft: true header: | [![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) builds: - env: - CGO_ENABLED=0 goos: - linux - darwin - windows goarch: - 386 - amd64 - arm - arm64 ignore: - goos: windows goarch: arm flags: - -trimpath ldflags: - -s -w -X main.version={{ .Version }} nfpms: - homepage: https://github.com/msoap/{{ .ProjectName }} description: Create Telegram bot from command-line. license: MIT formats: - deb - rpm bindir: /usr/bin contents: - src: shell2telegram.1 dst: /usr/share/man/man1/shell2telegram.1 - src: LICENSE dst: /usr/share/doc/shell2telegram/copyright - src: README.md dst: /usr/share/doc/shell2telegram/README.md archives: - format_overrides: - goos: windows format: zip files: - README* - LICENSE* - "*.1" checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Tag }}" changelog: sort: desc filters: exclude: - '^docs:' - '^test:' - '^Merge branch' - '^go fmt' ================================================ FILE: Dockerfile ================================================ # docker build -t msoap/shell2telegram . # build image FROM --platform=$BUILDPLATFORM golang:alpine as go_builder ARG TARGETPLATFORM ARG BUILDPLATFORM ARG TARGETOS ARG TARGETARCH RUN apk add --no-cache git ADD . $GOPATH/src/github.com/msoap/shell2telegram WORKDIR $GOPATH/src/github.com/msoap/shell2telegram ENV CGO_ENABLED=0 # GOARM=6 affects only "arm" builds ENV GOARM=6 # "amd64", "arm64" or "arm" (--platform=linux/amd64,linux/arm64,linux/arm/v6) ENV GOARCH=$TARGETARCH ENV GOOS=linux RUN echo "Building for $GOOS/$GOARCH" RUN go build -v -trimpath -ldflags="-w -s -X 'main.version=$(git describe --abbrev=0 --tags | sed s/v//)'" -o /go/bin/shell2telegram . # final image FROM alpine RUN apk add --no-cache ca-certificates COPY --from=go_builder /go/bin/shell2telegram /app/shell2telegram ENTRYPOINT ["/app/shell2telegram"] CMD ["-help"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Serhii Mudryk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ APP_NAME := shell2telegram APP_DESCRIPTION := $$(awk 'NR == 11, NR == 13' README.md) APP_URL := https://github.com/msoap/$(APP_NAME) APP_MAINTAINER := $$(git show HEAD | awk '$$1 == "Author:" {print $$2 " " $$3 " " $$4}') GIT_TAG := $$(git describe --tags --abbrev=0) run: go run . $${TB_ROOT:+-root-users=$$TB_ROOT} \ -add-exit \ -log-commands \ -persistent-users \ /date:desc="Get current date" date \ /:plain_text:desc="Numbers via cat -n" 'cat -n' \ /alarm:vars=SLEEP,MSG 'sleep $$SLEEP; echo Hello $$S2T_USERNAME, $$MSG' build: go build test: go test -race -cover -v ./... lint: golint ./... go vet ./... errcheck ./... update-from-github: go get -u github.com/msoap/$(APP_NAME) build-docker-image: docker 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)" docker build -t msoap/$(APP_NAME):latest . rm -f $(APP_NAME) gometalinter: gometalinter --vendor --cyclo-over=25 --line-length=150 --dupl-threshold=150 --min-occurrences=3 --enable=misspell --deadline=10m --exclude=SA1022 generate-manpage: cat README.md | grep -v "^\[" | perl -pe 's/\//' > $(APP_NAME).md docker run --rm -v $$PWD:/app -w /app msoap/ruby-ronn ronn $(APP_NAME).md mv ./$(APP_NAME) ./$(APP_NAME).1 rm ./$(APP_NAME).{md,html} create-debian-amd64-package: GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o $(APP_NAME) docker run --rm -v $$PWD:/app -w /app msoap/ruby-fpm \ fpm -s dir -t deb --force --name $(APP_NAME) -v $(GIT_TAG) \ --license="$$(head -1 LICENSE)" \ --url=$(APP_URL) \ --description="$(APP_DESCRIPTION)" \ --maintainer="$(APP_MAINTAINER)" \ --category=network \ ./$(APP_NAME)=/usr/bin/ \ ./$(APP_NAME).1=/usr/share/man/man1/ \ LICENSE=/usr/share/doc/$(APP_NAME)/copyright \ README.md=/usr/share/doc/$(APP_NAME)/ rm $(APP_NAME) ================================================ FILE: README.md ================================================ shell2telegram ----------------------------------------------------------------------------------------------------------------------------------------- [![Go build status](https://github.com/msoap/shell2telegram/actions/workflows/go.yml/badge.svg)](https://github.com/msoap/shell2telegram/actions/workflows/go.yml) [![Coverage Status](https://coveralls.io/repos/github/msoap/shell2telegram/badge.svg?branch=master)](https://coveralls.io/github/msoap/shell2telegram?branch=master) [![Docker Pulls](https://img.shields.io/docker/pulls/msoap/shell2telegram.svg?maxAge=3600)](https://hub.docker.com/r/msoap/shell2telegram/) [![Homebrew formula exists](https://img.shields.io/badge/homebrew-🍺-d7af72.svg)](https://github.com/msoap/shell2telegram#install) [![Report Card](https://goreportcard.com/badge/github.com/msoap/shell2telegram)](https://goreportcard.com/report/github.com/msoap/shell2telegram) Create Telegram bot from command-line Install ------- MacOS: brew tap msoap/tools brew install shell2telegram # update: brew upgrade shell2telegram Or download binaries from: [releases](https://github.com/msoap/shell2telegram/releases) (OS X/Linux/Windows/RaspberryPi) Or build from source: # set $GOPATH if needed go install github.com/msoap/shell2telegram@latest ln -s $GOPATH/bin/shell2telegram ~/bin/shell2telegram # or add $GOPATH/bin to $PATH Or build image and run with Docker. Example of `test-bot.Dockerfile` for bot who say current date: FROM msoap/shell2telegram # may be install some alpine packages: # RUN apk add --no-cache ... ENV TB_TOKEN=******* CMD ["/date", "date"] And build and run: docker build -f test-bot.Dockerfile -t test-bot . docker run --rm test-bot # or run with set token from command line docker run -e TB_TOKEN=******* --rm test-bot Using snap (Ubuntu or any Linux distribution with snap): # install stable version: sudo snap install shell2telegram # install the latest version: sudo snap install --edge shell2telegram # update sudo snap refresh shell2telegram Notice: the snap-package has its own sandbox with the `/bin`, `/usr/bin` directories which are not equal to system-wide `PATH` directories. Usage ----- Get token from [BotFather bot](https://telegram.me/BotFather), and set TB_TOKEN var in shell export TB_TOKEN=******* shell2telegram [options] /chat_command 'shell command' /chat_command2 'shell command2' options: -allow-users= : telegram users who are allowed to chat with the bot ("user1,user2") -root-users= : telegram users, who confirms new users in their private chat ("user1,user2") -allow-all : allow all users (DANGEROUS!) -add-exit : adding "/shell2telegram exit" command for terminate bot (for roots only) -log-commands : logging all commands -tb-token= : setting bot token (or set TB_TOKEN variable) -timeout=N : setting timeout for bot (default 60 sec) -description= : setting description of bot -bind-addr=<ADDRESS> : address to listen incoming webhook requests -webhook=<URL> : url for registering a webhook -persistent-users : load/save users from file (default ~/.config/shell2telegram.json) -users-db=<FILENAME> : file for store users -cache=N : caching command out for N seconds -one-thread : run each shell command in one thread -public : bot is public (don't add /auth* commands) -sh-timeout=N : set timeout for execute shell command (in seconds) -shell="shell" : shell for execute command, "" - without shell (default "sh") -version -help If not define -allow-users/-root-users options - authorize users via secret code from console or via chat with exists root users. All text after /chat_command will be sent to STDIN of shell command. Special chat commands --------------------- for private chats only: * `/:plain_text` - get user message without any /command. TODO: * `/:image` - for get image from user. Example: `/:image 'cat > file.jpg; echo ok'` * `/:file` - for get file from user * `/:location` - for get geo-location from user Possible long-running shell processes (for example alarm/timer bot). Autodetect images (png/jpg/gif/bmp) out from shell command, for example: `/get_image 'cat file.png'` Setting environment variables for shell commands: * S2T_LOGIN - telegram @login (may be empty) * S2T_USERID - telegram user ID * S2T_USERNAME - telegram user name * S2T_CHATID - chat ID Modificators for bot commands ----------------------------- * `:desc` - setting the description of command, `/cmd:desc="Command name" 'shell cmd'` * `:vars` - to create environment variables instead of text output to STDIN, `/cmd:vars=VAR1,VAR2 'echo $VAR1 / $VAR2'` * `:md` - to send message as markdown text, `/cmd:md 'echo "*bold* and _italic_"'` TODO: * `/cmd:cron=3600` — periodic exec command, `/cmd:on args` - on, `/cmd:off` - off Predefined bot commands ----------------------- * `/help` - list available commands * `/auth` - begin authorize new user * `/auth <CODE>` - authorize with code from console or from exists root user * `/authroot` - same for new root user * `/authroot <CODE>` - same for new root user for root users only: * `/shell2telegram stat` - show users statistics * `/shell2telegram search <query>` - search users by name/id * `/shell2telegram ban <user_id|@username>` - ban user * `/shell2telegram exit` - terminate bot (for run with -add-exit) * `/shell2telegram desc <description>` - set bot description * `/shell2telegram rm </command>` - delete command * `/shell2telegram broadcast_to_root <message>` - send message to all root users in private chat * `/shell2telegram message_to_user <user_id|@username> <message>` - send message to user in private chat * `/shell2telegram version` - show version Examples -------- # system information shell2telegram /top:desc="System information" 'top -l 1 | head -10' /date 'date' /ps 'ps aux -m | head -20' # sort any input shell2telegram /:plain_text sort # alarm bot: # /alarm time_in_seconds message shell2telegram /alarm:vars=SLEEP,MSG 'sleep $SLEEP; echo Hello $S2T_USERNAME; echo Alarm: $MSG' # sound volume control via telegram (Mac OS) shell2telegram /get 'osascript -e "output volume of (get volume settings)"' \ /up 'osascript -e "set volume output volume (($(osascript -e "output volume of (get volume settings)")+10))"' \ /down 'osascript -e "set volume output volume (($(osascript -e "output volume of (get volume settings)")-10))"' # using with webhook instead of poll shell2telegram -bind-addr=0.0.0.0:8080 -webhook=https://bot.example.com/path/to/bot \ /date /date # command with Markdown formating, calendar in monospace font shell2telegram /cal:md 'echo "\`\`\`$(ncal)\`\`\`"' Links ----- * [Telegram channel about shell2telegram](https://telegram.me/shell2telegram) * [About Telegram bots](https://core.telegram.org/bots) * [Golang bindings for the Telegram Bot API](https://github.com/go-telegram-bot-api/telegram-bot-api) * [shell2http - shell commands as http-server](https://github.com/msoap/shell2http) ================================================ FILE: commands.go ================================================ package main import ( "fmt" "log" "sort" "strings" "sync" "github.com/msoap/raphanus" ) // Ctx - context for bot command function (users, command, args, ...) type Ctx struct { appConfig *Config // configuration users *Users // all users commands Commands // all chat commands userID int // current user allowExec bool // is user authorized messageCmd string // command name messageArgs string // command arguments messageSignal chan<- BotMessage // for send telegram messages chatID int // chat for send replay exitSignal chan<- struct{} // for signal for terminate bot cache *raphanus.DB // cache for commands output cacheTTL int // cache timeout oneThreadMutex *sync.Mutex // mutex for run shell commands in one thread } // /auth and /authroot - authorize users func cmdAuth(ctx Ctx) (replayMsg string) { forRoot := ctx.messageCmd == "/authroot" if ctx.messageArgs == "" { replayMsg = "See code in terminal with shell2telegram or ask code from root user and type:\n" + ctx.messageCmd + " code" authCode := ctx.users.DoLogin(ctx.userID, forRoot) rootRoleStr := "" if forRoot { rootRoleStr = "root " } secretCodeMsg := fmt.Sprintf("Request %saccess for %s. Code: %s\n", rootRoleStr, ctx.users.String(ctx.userID), authCode) fmt.Print(secretCodeMsg) ctx.users.BroadcastForRoots(ctx.messageSignal, secretCodeMsg, 0) } else { if ctx.users.IsValidCode(ctx.userID, ctx.messageArgs, forRoot) { ctx.users.SetAuthorized(ctx.userID, forRoot) if forRoot { replayMsg = fmt.Sprintf("You (%s) authorized as root.", ctx.users.String(ctx.userID)) log.Print("root authorized: ", ctx.users.String(ctx.userID)) } else { replayMsg = fmt.Sprintf("You (%s) authorized.", ctx.users.String(ctx.userID)) log.Print("authorized: ", ctx.users.String(ctx.userID)) } } else { replayMsg = "Code is not valid." } } return replayMsg } // /help func cmdHelp(ctx Ctx) (replayMsg string) { helpMsg := []string{} if ctx.allowExec { for cmd, shellCmdRow := range ctx.commands { description := shellCmdRow.description if description == "" { description = shellCmdRow.shellCmd } helpMsg = append(helpMsg, cmd+" → "+description) } } sort.Strings(helpMsg) if !ctx.appConfig.isPublicBot { helpMsg = append(helpMsg, "/auth [code] → authorize user", "/authroot [code] → authorize user as root", ) } if ctx.users.IsRoot(ctx.userID) { helpMsgForRoot := []string{ "/shell2telegram ban <user_id|username> → ban user", "/shell2telegram broadcast_to_root <message> → send message to all root users in private chat", "/shell2telegram desc <bot description> → set bot description", "/shell2telegram message_to_user <user_id|username> <message> → send message to user in private chat", "/shell2telegram rm </command> → delete command", "/shell2telegram search <query> → search users by name/id", "/shell2telegram stat → get stat about users", "/shell2telegram version → show version", } if ctx.appConfig.addExit { helpMsgForRoot = append(helpMsgForRoot, "/shell2telegram exit → terminate bot") } sort.Strings(helpMsgForRoot) helpMsg = append(helpMsg, helpMsgForRoot...) } if ctx.appConfig.description != "" { replayMsg = ctx.appConfig.description } else { replayMsg = "This bot created with shell2telegram" } replayMsg += "\n\n" + "available commands:\n" + strings.Join(helpMsg, "\n") return replayMsg } // all commands from command-line func cmdUser(ctx Ctx) { if cmd, found := ctx.commands[ctx.messageCmd]; found { go func() { if ctx.appConfig.oneThread { ctx.oneThreadMutex.Lock() } replayMsgRaw := execShell( cmd.shellCmd, ctx.messageArgs, ctx.commands[ctx.messageCmd].vars, ctx.userID, ctx.chatID, ctx.users.list[ctx.userID].UserName, ctx.users.list[ctx.userID].FirstName+" "+ctx.users.list[ctx.userID].LastName, ctx.cache, ctx.cacheTTL, ctx.appConfig, ) if ctx.appConfig.oneThread { ctx.oneThreadMutex.Unlock() } sendMessage(ctx.messageSignal, ctx.chatID, replayMsgRaw, cmd.isMarkdown) }() } } // /shell2telegram stat func cmdShell2telegramStat(ctx Ctx) (replayMsg string) { for userID := range ctx.users.list { replayMsg += ctx.users.StringVerbose(userID) + "\n" } return replayMsg } // /shell2telegram search func cmdShell2telegramSearch(ctx Ctx) (replayMsg string) { query := ctx.messageArgs if query == "" { return "Please set query: /shell2telegram search <query>" } for _, userID := range ctx.users.Search(query) { replayMsg += ctx.users.StringVerbose(userID) + "\n" } return replayMsg } // /shell2telegram ban func cmdShell2telegramBan(ctx Ctx) (replayMsg string) { userName := ctx.messageArgs if userName == "" { return "Please set user_id or login: /shell2telegram ban <user_id|username>" } userID := ctx.users.FindByIDOrUserName(userName) if userID > 0 && ctx.users.BanUser(userID) { replayMsg = fmt.Sprintf("User %s banned", ctx.users.String(userID)) } else { replayMsg = "User not found" } return replayMsg } // set bot description func cmdShell2telegramDesc(ctx Ctx) (replayMsg string) { description := ctx.messageArgs if description == "" { return "Please set description: /shell2telegram desc <bot description>" } ctx.appConfig.description = description replayMsg = "Bot description set to: " + description return replayMsg } // /shell2telegram rm "/command" - delete command func cmdShell2telegramRm(ctx Ctx) (replayMsg string) { commandName := ctx.messageArgs if commandName == "" { return "Please set command for delete: /shell2telegram rm </command>" } if _, ok := ctx.commands[commandName]; ok { delete(ctx.commands, commandName) replayMsg = "Deleted command: " + commandName } else { replayMsg = fmt.Sprintf("Command %s not found", commandName) } return replayMsg } // /shell2telegram version - get version func cmdShell2telegramVersion(_ Ctx) (replayMsg string) { replayMsg = fmt.Sprintf("shell2telegram %s", version) return replayMsg } // /shell2telegram exit - terminate bot func cmdShell2telegramExit(ctx Ctx) (replayMsg string) { if ctx.appConfig.addExit { replayMsg = "bye..." go func() { ctx.exitSignal <- struct{}{} }() } return replayMsg } // /shell2telegram broadcast_to_root - broadcast message to root users in private chat func cmdShell2telegramBroadcastToRoot(ctx Ctx) (replayMsg string) { message := ctx.messageArgs if message == "" { replayMsg = "Please set message: /shell2telegram broadcast_to_root <message>" } else { ctx.users.BroadcastForRoots(ctx.messageSignal, fmt.Sprintf("Message from %s:\n%s", ctx.users.String(ctx.userID), message), ctx.userID, // don't send self ) replayMsg = "Message sent" } return replayMsg } // /shell2telegram message_to_user user_id|username "message" - send message to user in private chat func cmdShell2telegramMessageToUser(ctx Ctx) (replayMsg string) { userName, message := splitStringHalfBySpace(ctx.messageArgs) if userName == "" || message == "" { replayMsg = "Please set user_name and message: /shell2telegram message_to_user <user_id|username> <message>" } else { userID := ctx.users.FindByIDOrUserName(userName) if userID > 0 { ctx.users.SendMessageToPrivate(ctx.messageSignal, userID, message) replayMsg = "Message sent" } else { replayMsg = "User not found" } } return replayMsg } ================================================ FILE: go.mod ================================================ module github.com/msoap/shell2telegram go 1.14 require ( github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect github.com/mattn/go-shellwords v1.0.12 github.com/msoap/raphanus v0.14.0 github.com/technoweenie/multipartstreamer v1.0.1 // indirect gopkg.in/telegram-bot-api.v2 v2.2.1 ) ================================================ FILE: go.sum ================================================ github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9 h1:ViNuGS149jgnttqhc6XQNPwdupEMBXqCx9wtlW7P3sA= github.com/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9/go.mod h1:fLRUbhbSd5Px2yKUaGYYPltlyxi1guJz1vCmo1RQL50= github.com/msoap/raphanus v0.14.0 h1:g499/ayslkqV7H9dKhADgOEQV1nCuAfMOMIkzazsB6s= github.com/msoap/raphanus v0.14.0/go.mod h1:p3GKFEnntq4DvX4hT3Vg2GMHeXE2oyb1+kcB+WOF3ZM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/telegram-bot-api.v2 v2.2.1 h1:986tIxlvgcNDRh47hv0TapZu86Ejb6HEZG7oJWDynKU= gopkg.in/telegram-bot-api.v2 v2.2.1/go.mod h1:6qHx+TxEVOINu9hi66EARcbgYPcJnvFK7eE1zWsXhlU= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: shell2telegram.1 ================================================ .\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . .TH "SHELL2TELEGRAM" "" "January 2022" "" "" . .SH "NAME" \fBshell2telegram\fR . .SH "shell2telegram" Create Telegram bot from command\-line . .SH "Install" MacOS: . .IP "" 4 . .nf brew tap msoap/tools brew install shell2telegram # update: brew upgrade shell2telegram . .fi . .IP "" 0 . .P Or download binaries from: releases \fIhttps://github\.com/msoap/shell2telegram/releases\fR (OS X/Linux/Windows/RaspberryPi) . .P Or build from source: . .IP "" 4 . .nf # set $GOPATH if needed go install github\.com/msoap/shell2telegram@latest ln \-s $GOPATH/bin/shell2telegram ~/bin/shell2telegram # or add $GOPATH/bin to $PATH . .fi . .IP "" 0 . .P Or build image and run with Docker\. Example of \fBtest\-bot\.Dockerfile\fR for bot who say current date: . .IP "" 4 . .nf FROM msoap/shell2telegram # may be install some alpine packages: # RUN apk add \-\-no\-cache \.\.\. ENV TB_TOKEN=******* CMD ["/date", "date"] . .fi . .IP "" 0 . .P And build and run: . .IP "" 4 . .nf docker build \-f test\-bot\.Dockerfile \-t test\-bot \. docker run \-\-rm test\-bot # or run with set token from command line docker run \-e TB_TOKEN=******* \-\-rm test\-bot . .fi . .IP "" 0 . .P Using snap (Ubuntu or any Linux distribution with snap): . .IP "" 4 . .nf # install stable version: sudo snap install shell2telegram # install the latest version: sudo snap install \-\-edge shell2telegram # update sudo snap refresh shell2telegram . .fi . .IP "" 0 . .P Notice: 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\. . .SH "Usage" Get token from BotFather bot \fIhttps://telegram\.me/BotFather\fR, and set TB_TOKEN var in shell . .IP "" 4 . .nf export TB_TOKEN=******* shell2telegram [options] /chat_command \'shell command\' /chat_command2 \'shell command2\' options: \-allow\-users=<NAMES> : telegram users who are allowed to chat with the bot ("user1,user2") \-root\-users=<NAMES> : telegram users, who confirms new users in their private chat ("user1,user2") \-allow\-all : allow all users (DANGEROUS!) \-add\-exit : adding "/shell2telegram exit" command for terminate bot (for roots only) \-log\-commands : logging all commands \-tb\-token=<TOKEN> : setting bot token (or set TB_TOKEN variable) \-timeout=N : setting timeout for bot (default 60 sec) \-description=<TITLE> : setting description of bot \-bind\-addr=<ADDRESS> : address to listen incoming webhook requests \-webhook=<URL> : url for registering a webhook \-persistent\-users : load/save users from file (default ~/\.config/shell2telegram\.json) \-users\-db=<FILENAME> : file for store users \-cache=N : caching command out for N seconds \-one\-thread : run each shell command in one thread \-public : bot is public (don\'t add /auth* commands) \-sh\-timeout=N : set timeout for execute shell command (in seconds) \-shell="shell" : shell for execute command, "" \- without shell (default "sh") \-version \-help . .fi . .IP "" 0 . .P If not define \-allow\-users/\-root\-users options \- authorize users via secret code from console or via chat with exists root users\. . .P All text after /chat_command will be sent to STDIN of shell command\. . .SH "Special chat commands" for private chats only: . .IP "\(bu" 4 \fB/:plain_text\fR \- get user message without any /command\. . .IP "" 0 . .P TODO: . .IP "\(bu" 4 \fB/:image\fR \- for get image from user\. Example: \fB/:image \'cat > file\.jpg; echo ok\'\fR . .IP "\(bu" 4 \fB/:file\fR \- for get file from user . .IP "\(bu" 4 \fB/:location\fR \- for get geo\-location from user . .IP "" 0 . .P Possible long\-running shell processes (for example alarm/timer bot)\. . .P Autodetect images (png/jpg/gif/bmp) out from shell command, for example: \fB/get_image \'cat file\.png\'\fR . .P Setting environment variables for shell commands: . .IP "\(bu" 4 S2T_LOGIN \- telegram @login (may be empty) . .IP "\(bu" 4 S2T_USERID \- telegram user ID . .IP "\(bu" 4 S2T_USERNAME \- telegram user name . .IP "\(bu" 4 S2T_CHATID \- chat ID . .IP "" 0 . .SH "Modificators for bot commands" . .IP "\(bu" 4 \fB:desc\fR \- setting the description of command, \fB/cmd:desc="Command name" \'shell cmd\'\fR . .IP "\(bu" 4 \fB:vars\fR \- to create environment variables instead of text output to STDIN, \fB/cmd:vars=VAR1,VAR2 \'echo $VAR1 / $VAR2\'\fR . .IP "\(bu" 4 \fB:md\fR \- to send message as markdown text, \fB/cmd:md \'echo "*bold* and _italic_"\'\fR . .IP "" 0 . .P TODO: . .IP "\(bu" 4 \fB/cmd:cron=3600\fR — periodic exec command, \fB/cmd:on args\fR \- on, \fB/cmd:off\fR \- off . .IP "" 0 . .SH "Predefined bot commands" . .IP "\(bu" 4 \fB/help\fR \- list available commands . .IP "\(bu" 4 \fB/auth\fR \- begin authorize new user . .IP "\(bu" 4 \fB/auth <CODE>\fR \- authorize with code from console or from exists root user . .IP "\(bu" 4 \fB/authroot\fR \- same for new root user . .IP "\(bu" 4 \fB/authroot <CODE>\fR \- same for new root user . .IP "" 0 . .P for root users only: . .IP "\(bu" 4 \fB/shell2telegram stat\fR \- show users statistics . .IP "\(bu" 4 \fB/shell2telegram search <query>\fR \- search users by name/id . .IP "\(bu" 4 \fB/shell2telegram ban <user_id|@username>\fR \- ban user . .IP "\(bu" 4 \fB/shell2telegram exit\fR \- terminate bot (for run with \-add\-exit) . .IP "\(bu" 4 \fB/shell2telegram desc <description>\fR \- set bot description . .IP "\(bu" 4 \fB/shell2telegram rm </command>\fR \- delete command . .IP "\(bu" 4 \fB/shell2telegram broadcast_to_root <message>\fR \- send message to all root users in private chat . .IP "\(bu" 4 \fB/shell2telegram message_to_user <user_id|@username> <message>\fR \- send message to user in private chat . .IP "\(bu" 4 \fB/shell2telegram version\fR \- show version . .IP "" 0 . .SH "Examples" . .nf # system information shell2telegram /top:desc="System information" \'top \-l 1 | head \-10\' /date \'date\' /ps \'ps aux \-m | head \-20\' # sort any input shell2telegram /:plain_text sort # alarm bot: # /alarm time_in_seconds message shell2telegram /alarm:vars=SLEEP,MSG \'sleep $SLEEP; echo Hello $S2T_USERNAME; echo Alarm: $MSG\' # sound volume control via telegram (Mac OS) shell2telegram /get \'osascript \-e "output volume of (get volume settings)"\' \e /up \'osascript \-e "set volume output volume (($(osascript \-e "output volume of (get volume settings)")+10))"\' \e /down \'osascript \-e "set volume output volume (($(osascript \-e "output volume of (get volume settings)")\-10))"\' # using with webhook instead of poll shell2telegram \-bind\-addr=0\.0\.0\.0:8080 \-webhook=https://bot\.example\.com/path/to/bot \e /date /date # command with Markdown formating, calendar in monospace font shell2telegram /cal:md \'echo "\e`\e`\e`$(ncal)\e`\e`\e`"\' . .fi . .SH "Links" . .IP "\(bu" 4 Telegram channel about shell2telegram \fIhttps://telegram\.me/shell2telegram\fR . .IP "\(bu" 4 About Telegram bots \fIhttps://core\.telegram\.org/bots\fR . .IP "\(bu" 4 Golang bindings for the Telegram Bot API \fIhttps://github\.com/go\-telegram\-bot\-api/telegram\-bot\-api\fR . .IP "\(bu" 4 shell2http \- shell commands as http\-server \fIhttps://github\.com/msoap/shell2http\fR . .IP "" 0 ================================================ FILE: shell2telegram.go ================================================ package main import ( "flag" "fmt" "log" "net/http" "net/url" "os" "os/signal" "strings" "sync" "time" "github.com/msoap/raphanus" tgbotapi "gopkg.in/telegram-bot-api.v2" ) const ( // version - current version version = "1.10" // DefaultBotTimeout - bot default timeout DefaultBotTimeout = 60 // MessagesQueueSize - size of channel for bot messages MessagesQueueSize = 10 // MaxMessageLength - max length of one bot message MaxMessageLength = 4096 // SecondsForAutoSaveUsersToDB - save users to file every 1 min (if need) SecondsForAutoSaveUsersToDB = 60 // DBFileName - DB json name DBFileName = "shell2telegram.json" // shell2telegram command name for get plain text without /command cmdPlainText = "/:plain_text" ) // Command - one user command type Command struct { shellCmd string // shell command description string // command description for list in /help (/cmd:desc="Command name") vars []string // environment vars for user text, split by `/s+` to vars (/cmd:vars=SUBCOMMAND,ARGS) isMarkdown bool // send message in markdown format } // Commands - list of all commands type Commands map[string]Command // Config - config struct type Config struct { token string // bot token botTimeout int // bot timeout predefinedAllowedUsers []string // telegram users who are allowed to chat with the bot predefinedRootUsers []string // telegram users, who confirms new users in their private chat description string // description of bot bindAddr string // bind address to listen webhook requests webhookURL url.URL // url for the webhook usersDB string // file for store users shell string // custom shell cache int // caching command out (in seconds) shTimeout int // timeout for execute shell command (in seconds) addExit bool // adding /shell2telegram exit command allowAll bool // allow all user (DANGEROUS!) logCommands bool // logging all commands persistentUsers bool // load/save users from file isPublicBot bool // bot is public (don't add /auth* commands) oneThread bool // run each shell commands in one thread } // message types const ( msgIsText int8 = iota msgIsPhoto ) // BotMessage - record for send via channel for send message to telegram chat type BotMessage struct { message string fileName string photo []byte chatID int messageType int8 isMarkdown bool } // ---------------------------------------------------------------------------- // get config func getConfig() (commands Commands, appConfig Config, err error) { flag.StringVar(&appConfig.token, "tb-token", "", "setting bot `token` (or set TB_TOKEN variable)") flag.BoolVar(&appConfig.addExit, "add-exit", false, "adding \"/shell2telegram exit\" command for terminate bot (for roots only)") flag.IntVar(&appConfig.botTimeout, "timeout", DefaultBotTimeout, "setting timeout for bot (in `seconds`)") flag.StringVar(&appConfig.bindAddr, "bind-addr", "", "bind address to listen webhook requests, like: `0.0.0.0:8080`") flag.Var(&urlValue{&appConfig.webhookURL}, "webhook", "`url` of bot's webhook") flag.BoolVar(&appConfig.allowAll, "allow-all", false, "allow all users (DANGEROUS!)") flag.BoolVar(&appConfig.logCommands, "log-commands", false, "logging all commands") flag.StringVar(&appConfig.description, "description", "", "setting description of bot") flag.BoolVar(&appConfig.persistentUsers, "persistent-users", false, "load/save users from file (default ~/.config/shell2telegram.json)") flag.StringVar(&appConfig.usersDB, "users-db", "", "`file` for store users") flag.IntVar(&appConfig.cache, "cache", 0, "caching command out (in `seconds`)") flag.BoolVar(&appConfig.isPublicBot, "public", false, "bot is public (don't add /auth* commands)") flag.IntVar(&appConfig.shTimeout, "sh-timeout", 0, "set timeout for execute shell command (in `seconds`)") flag.StringVar(&appConfig.shell, "shell", "sh", "custom shell or \"\" for execute without shell") flag.BoolVar(&appConfig.oneThread, "one-thread", false, "run each shell command in one thread") logFilename := flag.String("log", "", "log `filename`, default - STDOUT") predefinedAllowedUsers := flag.String("allow-users", "", "telegram users who are allowed to chat with the bot (\"user1,user2\")") predefinedRootUsers := flag.String("root-users", "", "telegram users, who confirms new users in their private chat (\"user1,user2\")") showVersion := flag.Bool("version", false, "get version") flag.Usage = func() { fmt.Printf("usage: %s [options] %s\n%s\n%s\n\noptions:\n", os.Args[0], `/chat_command "shell command" /chat_command2 "shell command2"`, "All text after /chat_command will be sent to STDIN of shell command.", "If chat command is /:plain_text - get user message without any /command (for private chats only)", ) flag.PrintDefaults() os.Exit(0) } flag.Parse() if *showVersion { fmt.Println(version) os.Exit(0) } // setup log file if len(*logFilename) > 0 { fhLog, err := os.OpenFile(*logFilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) if err != nil { log.Fatalf("error opening log file: %v", err) } log.SetOutput(fhLog) } // setup users and roots if *predefinedAllowedUsers != "" { appConfig.predefinedAllowedUsers = strings.Split(*predefinedAllowedUsers, ",") } if *predefinedRootUsers != "" { appConfig.predefinedRootUsers = strings.Split(*predefinedRootUsers, ",") } commands = Commands{} // need >= 2 arguments and count of it must be even args := flag.Args() if len(args) < 2 || len(args)%2 == 1 { return commands, appConfig, fmt.Errorf("error: need pairs of /chat-command and shell-command") } for i := 0; i < len(args); i += 2 { path, command, err := parseBotCommand(args[i], args[i+1]) // (/path, shell_command) if err != nil { return commands, appConfig, err } commands[path] = command } if appConfig.token == "" { if appConfig.token = os.Getenv("TB_TOKEN"); appConfig.token == "" { return commands, appConfig, fmt.Errorf("TB_TOKEN environment var not found. See https://core.telegram.org/bots#botfather for more information") } } return commands, appConfig, nil } // ---------------------------------------------------------------------------- func sendMessage(messageSignal chan<- BotMessage, chatID int, message []byte, isMarkdown bool) { go func() { var fileName string fileType := http.DetectContentType(message) switch fileType { case "image/png": fileName = "file.png" case "image/jpeg": fileName = "file.jpeg" case "image/gif": fileName = "file.gif" case "image/bmp": fileName = "file.bmp" case "video/mp4": // TODO: nedded migrate to new telegram-bot-api library log.Printf("not supported") return default: fileName = "message" } if fileName == "message" { // is text message messageString := string(message) var messagesList []string if len(messageString) <= MaxMessageLength { messagesList = []string{messageString} } else { messagesList = splitStringLinesBySize(messageString, MaxMessageLength) } for _, messageChunk := range messagesList { messageSignal <- BotMessage{ chatID: chatID, messageType: msgIsText, message: messageChunk, isMarkdown: isMarkdown, } } } else { // is image messageSignal <- BotMessage{ chatID: chatID, messageType: msgIsPhoto, fileName: fileName, photo: message, } } }() } // ---------------------------------------------------------------------------- func main() { commands, appConfig, err := getConfig() if err != nil { log.Fatal(err) } bot, err := tgbotapi.NewBotAPI(appConfig.token) if err != nil { log.Fatal(err) } log.Printf("Authorized on bot account: @%s", bot.Self.UserName) tgbotConfig := tgbotapi.NewUpdate(0) tgbotConfig.Timeout = appConfig.botTimeout var botUpdatesChan <-chan tgbotapi.Update var server *http.Server if appConfig.bindAddr != "" { _, err = bot.SetWebhook(tgbotapi.WebhookConfig{URL: &appConfig.webhookURL}) if err != nil { log.Fatal(err) } botUpdatesChan = bot.ListenForWebhook(appConfig.webhookURL.Path) server = &http.Server{Addr: appConfig.bindAddr} go func() { log.Println("Listening incoming requests at ", appConfig.bindAddr) log.Fatal(server.ListenAndServe()) }() } else { botUpdatesChan, err = bot.GetUpdatesChan(tgbotConfig) if err != nil { log.Fatal(err) } } users := NewUsers(appConfig) messageSignal := make(chan BotMessage, MessagesQueueSize) vacuumTicker := time.Tick(SecondsForOldUsersBeforeVacuum * time.Second) saveToBDTicker := make(<-chan time.Time) oneThreadMutex := sync.Mutex{} exitSignal := make(chan struct{}) systemExitSignal := make(chan os.Signal, 1) signal.Notify(systemExitSignal, os.Interrupt) if appConfig.persistentUsers { saveToBDTicker = time.Tick(SecondsForAutoSaveUsersToDB * time.Second) } var cache raphanus.DB if appConfig.cache > 0 { cache = raphanus.New() } // all /shell2telegram sub-commands handlers internalCommands := map[string]func(Ctx) string{ "stat": cmdShell2telegramStat, "ban": cmdShell2telegramBan, "search": cmdShell2telegramSearch, "desc": cmdShell2telegramDesc, "rm": cmdShell2telegramRm, "exit": cmdShell2telegramExit, "version": cmdShell2telegramVersion, "broadcast_to_root": cmdShell2telegramBroadcastToRoot, "message_to_user": cmdShell2telegramMessageToUser, } doExit := false for !doExit { select { case telegramUpdate := <-botUpdatesChan: var messageCmd, messageArgs string allUserMessage := telegramUpdate.Message.Text if len(allUserMessage) > 0 && allUserMessage[0] == '/' { messageCmd, messageArgs = splitStringHalfBySpace(allUserMessage) } else { messageCmd, messageArgs = cmdPlainText, allUserMessage } allowPlainText := false if _, ok := commands[cmdPlainText]; ok { allowPlainText = true } replayMsg := "" if len(messageCmd) > 0 && (messageCmd != cmdPlainText || allowPlainText) { users.AddNew(telegramUpdate.Message) userID := telegramUpdate.Message.From.ID allowExec := appConfig.allowAll || users.IsAuthorized(userID) ctx := Ctx{ appConfig: &appConfig, users: &users, commands: commands, userID: userID, allowExec: allowExec, messageCmd: messageCmd, messageArgs: messageArgs, messageSignal: messageSignal, chatID: telegramUpdate.Message.Chat.ID, exitSignal: exitSignal, cache: &cache, oneThreadMutex: &oneThreadMutex, } switch { // commands ................................. case !appConfig.isPublicBot && (messageCmd == "/auth" || messageCmd == "/authroot"): replayMsg = cmdAuth(ctx) case messageCmd == "/help": replayMsg = cmdHelp(ctx) case messageCmd == "/shell2telegram" && users.IsRoot(userID): var messageSubCmd string messageSubCmd, messageArgs = splitStringHalfBySpace(messageArgs) ctx.messageArgs = messageArgs if cmdHandler, ok := internalCommands[messageSubCmd]; ok { replayMsg = cmdHandler(ctx) } else { replayMsg = "Sub-command not found" } case allowExec && (allowPlainText && messageCmd == cmdPlainText || messageCmd[0] == '/'): cmdUser(ctx) } // switch for commands if appConfig.logCommands { log.Printf("%s: %s", users.String(userID), allUserMessage) } sendMessage(messageSignal, telegramUpdate.Message.Chat.ID, []byte(replayMsg), false) } case botMessage := <-messageSignal: switch { case botMessage.messageType == msgIsText && !stringIsEmpty(botMessage.message): messageConfig := tgbotapi.NewMessage(botMessage.chatID, botMessage.message) if botMessage.isMarkdown { messageConfig.ParseMode = tgbotapi.ModeMarkdown } _, err = bot.Send(messageConfig) case botMessage.messageType == msgIsPhoto && len(botMessage.photo) > 0: bytesPhoto := tgbotapi.FileBytes{Name: botMessage.fileName, Bytes: botMessage.photo} _, err = bot.Send(tgbotapi.NewPhotoUpload(botMessage.chatID, bytesPhoto)) } if err != nil { log.Printf("failed to send message: %s", err) } case <-saveToBDTicker: users.SaveToDB(appConfig.usersDB) case <-vacuumTicker: users.ClearOldUsers() case <-systemExitSignal: go func() { exitSignal <- struct{}{} }() case <-exitSignal: if appConfig.persistentUsers { users.needSaveDB = true users.SaveToDB(appConfig.usersDB) } if server != nil { log.Println(server.Close()) } doExit = true } } } ================================================ FILE: snapcraft.yaml ================================================ name: shell2telegram version: '1.10' summary: Telegram bot constructor from command-line description: | Telegram bot constructor from command-line, written in Go. Settings through two command line arguments, bot-command and shell command. grade: stable confinement: strict base: core18 parts: shell2telegram: plugin: go go-importpath: github.com/msoap/shell2telegram source: . source-type: git apps: shell2telegram: command: bin/shell2telegram plugs: [network, home] ================================================ FILE: test-bot.Dockerfile ================================================ FROM msoap/shell2telegram ENV TB_TOKEN=******* CMD ["/date", "date"] ================================================ FILE: users.go ================================================ package main import ( "encoding/json" "fmt" "io/ioutil" "log" "strconv" "strings" "time" tgbotapi "gopkg.in/telegram-bot-api.v2" ) // User - one telegram user who interact with bot type User struct { UserID int `json:"user_id"` // telegram UserID UserName string `json:"user_name"` // telegram @login FirstName string `json:"first_name"` // telegram name LastName string `json:"last_name"` // -//- AuthCode string `json:"auth_code"` // code for authorize AuthCodeRoot string `json:"auth_code_root"` // code for authorize root IsAuthorized bool `json:"is_authorized"` // user allow chat with bot IsRoot bool `json:"is_root"` // user is root, allow authorize/ban other users, remove commands, stop bot PrivateChatID int `json:"private_chat_id"` // last private chat with bot Counter int `json:"counter"` // how many commands send LastAccessTime time.Time `json:"last_access_time"` // time of last command } // Users in chat type Users struct { list map[int]*User predefinedAllowedUsers map[string]bool predefinedRootUsers map[string]bool needSaveDB bool // non-saved changes in list } // UsersDB - save list of Users into JSON type UsersDB struct { Users []User `json:"users"` DateTime time.Time `json:"date_time"` } // SecondsForOldUsersBeforeVacuum - clear old users after 20 minutes after login const SecondsForOldUsersBeforeVacuum = 1200 // NewUsers - create Users object func NewUsers(appConfig Config) Users { users := Users{ predefinedAllowedUsers: map[string]bool{}, predefinedRootUsers: map[string]bool{}, list: map[int]*User{}, needSaveDB: true, } if appConfig.persistentUsers { users.LoadFromDB(appConfig.usersDB) } for _, name := range appConfig.predefinedAllowedUsers { users.predefinedAllowedUsers[name] = true } for _, name := range appConfig.predefinedRootUsers { users.predefinedAllowedUsers[name] = true users.predefinedRootUsers[name] = true } return users } // AddNew - add new user if not exists func (users *Users) AddNew(tgbotMessage tgbotapi.Message) { privateChatID := 0 if !tgbotMessage.Chat.IsGroup() { privateChatID = tgbotMessage.Chat.ID } UserID := tgbotMessage.From.ID if _, ok := users.list[UserID]; ok && privateChatID > 0 && privateChatID != users.list[UserID].PrivateChatID { users.list[UserID].PrivateChatID = privateChatID users.needSaveDB = true } else if !ok { users.list[UserID] = &User{ UserID: UserID, UserName: tgbotMessage.From.UserName, FirstName: tgbotMessage.From.FirstName, LastName: tgbotMessage.From.LastName, IsAuthorized: users.predefinedAllowedUsers[tgbotMessage.From.UserName], IsRoot: users.predefinedRootUsers[tgbotMessage.From.UserName], PrivateChatID: privateChatID, } users.needSaveDB = true } // collect stat users.list[UserID].LastAccessTime = time.Now() if users.list[UserID].IsAuthorized { users.list[UserID].Counter++ } } // DoLogin - generate secret code func (users *Users) DoLogin(userID int, forRoot bool) string { code := getRandomCode() if forRoot { users.list[userID].IsRoot = false users.list[userID].AuthCodeRoot = code } else { users.list[userID].IsAuthorized = false users.list[userID].AuthCode = code } users.needSaveDB = true return code } // SetAuthorized - set user authorized or authorized as root func (users *Users) SetAuthorized(userID int, forRoot bool) { users.list[userID].IsAuthorized = true users.list[userID].AuthCode = "" if forRoot { users.list[userID].IsRoot = true users.list[userID].AuthCodeRoot = "" } users.needSaveDB = true } // IsValidCode - check secret code for user func (users Users) IsValidCode(userID int, code string, forRoot bool) bool { var result bool if forRoot { result = code != "" && code == users.list[userID].AuthCodeRoot } else { result = code != "" && code == users.list[userID].AuthCode } return result } // IsAuthorized - check user is authorized func (users Users) IsAuthorized(userID int) bool { isAuthorized := false if _, ok := users.list[userID]; ok && users.list[userID].IsAuthorized { isAuthorized = true } return isAuthorized } // IsRoot - check user is root func (users Users) IsRoot(userID int) bool { isRoot := false if _, ok := users.list[userID]; ok && users.list[userID].IsRoot { isRoot = true } return isRoot } // BroadcastForRoots - send message to all root users func (users Users) BroadcastForRoots(messageSignal chan<- BotMessage, message string, excludeID int) { for userID, user := range users.list { if user.IsRoot && user.PrivateChatID > 0 && (excludeID == 0 || excludeID != userID) { sendMessage(messageSignal, user.PrivateChatID, []byte(message), false) } } } // String - format user name func (users Users) String(userID int) string { result := fmt.Sprintf("%s %s", users.list[userID].FirstName, users.list[userID].LastName) if users.list[userID].UserName != "" { result += fmt.Sprintf(" (@%s)", users.list[userID].UserName) } return result } // StringVerbose - format user name with all fields func (users Users) StringVerbose(userID int) string { user := users.list[userID] result := fmt.Sprintf("%s: id: %d, auth: %v, root: %v, count: %d, last: %v", users.String(userID), userID, user.IsAuthorized, user.IsRoot, user.Counter, user.LastAccessTime.Format("2006-01-02 15:04:05"), ) return result } // ClearOldUsers - clear old users without login func (users *Users) ClearOldUsers() { for id, user := range users.list { if !user.IsAuthorized && !user.IsRoot && user.Counter == 0 && time.Since(user.LastAccessTime).Seconds() > SecondsForOldUsersBeforeVacuum { log.Printf("Vacuum: %d, %s", id, users.String(id)) delete(users.list, id) users.needSaveDB = true } } } // GetUserIDByName - find user by login func (users Users) GetUserIDByName(userName string) int { userID := 0 for id, user := range users.list { if userName == user.UserName { userID = id break } } return userID } // BanUser - ban user by ID func (users *Users) BanUser(userID int) bool { if _, ok := users.list[userID]; ok { users.list[userID].IsAuthorized = false users.list[userID].IsRoot = false if users.list[userID].UserName != "" { delete(users.predefinedAllowedUsers, users.list[userID].UserName) delete(users.predefinedRootUsers, users.list[userID].UserName) } users.needSaveDB = true return true } return false } // Search - search users func (users Users) Search(query string) (result []int) { queryUserID, _ := strconv.Atoi(query) query = strings.ToLower(query) queryAsLogin := cleanUserName(query) for userID, user := range users.list { if queryUserID == userID || strings.Contains(strings.ToLower(user.UserName), queryAsLogin) || strings.Contains(strings.ToLower(user.FirstName), query) || strings.Contains(strings.ToLower(user.LastName), query) { result = append(result, userID) } } return result } // FindByIDOrUserName - find users or by ID or by @name func (users Users) FindByIDOrUserName(userName string) int { userID, err := strconv.Atoi(userName) if err == nil { if _, ok := users.list[userID]; !ok { userID = 0 } } else { userName = cleanUserName(userName) userID = users.GetUserIDByName(userName) } return userID } // SendMessageToPrivate - send message to user to private chat func (users Users) SendMessageToPrivate(messageSignal chan<- BotMessage, userID int, message string) bool { if user, ok := users.list[userID]; ok && user.PrivateChatID > 0 { sendMessage(messageSignal, user.PrivateChatID, []byte(message), false) return true } return false } // LoadFromDB - load users list from json file func (users *Users) LoadFromDB(usersDBFile string) { usersList := UsersDB{} fileNamePath := getDBFilePath(usersDBFile, false) usersJSON, err := ioutil.ReadFile(fileNamePath) if err == nil { if err = json.Unmarshal(usersJSON, &usersList); err == nil { for _, user := range usersList.Users { user := user users.list[user.UserID] = &user } } } if err == nil { log.Printf("Loaded usersDB json from: %s, %d users", fileNamePath, len(usersList.Users)) } else { log.Printf("Load usersDB (%s) error: %s", fileNamePath, err) } users.needSaveDB = false } // SaveToDB - save users list to json file func (users *Users) SaveToDB(usersDBFile string) { if users.needSaveDB { usersList := UsersDB{ Users: []User{}, DateTime: time.Now(), } for _, user := range users.list { usersList.Users = append(usersList.Users, *user) } fileNamePath := getDBFilePath(usersDBFile, true) jsonBytes, err := json.MarshalIndent(usersList, "", " ") if err == nil { err = ioutil.WriteFile(fileNamePath, jsonBytes, 0644) } if err == nil { log.Printf("Saved usersDB json to: %s", fileNamePath) users.needSaveDB = false } else { log.Printf("Save usersDB (%s) error: %s", fileNamePath, err) } } } ================================================ FILE: utils.go ================================================ package main import ( "context" "crypto/rand" "encoding/base64" "fmt" "io" "log" "net/url" "os" "os/exec" "regexp" "runtime" "strconv" "strings" "time" shellwords "github.com/mattn/go-shellwords" "github.com/msoap/raphanus" raphanuscommon "github.com/msoap/raphanus/common" ) // codeBytesLength - length of random code in bytes const codeBytesLength = 15 // exec shell commands with text to STDIN func execShell(shellCmd, input string, varsNames []string, userID, chatID int, userName, userDisplayName string, cache *raphanus.DB, cacheTTL int, config *Config) (result []byte) { cacheKey := shellCmd + "/" + input if cacheTTL > 0 { if cacheData, err := cache.GetBytes(cacheKey); err != raphanuscommon.ErrKeyNotExists && err != nil { log.Printf("get from cache failed: %s", err) } else if err == nil { // cache hit return cacheData } } shell, params, err := getShellAndParams(shellCmd, config.shell, runtime.GOOS == "windows") if err != nil { log.Print("parse shell failed: ", err) return nil } ctx := context.Background() if config.shTimeout > 0 { var cancelFn context.CancelFunc ctx, cancelFn = context.WithTimeout(ctx, time.Duration(config.shTimeout)*time.Second) defer cancelFn() } osExecCommand := exec.CommandContext(ctx, shell, params...) // #nosec osExecCommand.Stderr = os.Stderr // copy variables from parent process osExecCommand.Env = append(osExecCommand.Env, os.Environ()...) if input != "" { if len(varsNames) > 0 { // set user input to shell vars arguments := regexp.MustCompile(`\s+`).Split(input, len(varsNames)) for i, arg := range arguments { osExecCommand.Env = append(osExecCommand.Env, fmt.Sprintf("%s=%s", varsNames[i], arg)) } } else { var stdin io.WriteCloser errExec := errChain(func() (err error) { stdin, err = osExecCommand.StdinPipe() return err }, func() error { _, err = io.WriteString(stdin, input) return err }, func() error { return stdin.Close() }) if errExec != nil { log.Print("get STDIN error: ", err) } } } // set S2T_* env vars s2tVariables := [...]struct{ name, value string }{ {"S2T_LOGIN", userName}, {"S2T_USERID", strconv.Itoa(userID)}, {"S2T_USERNAME", userDisplayName}, {"S2T_CHATID", strconv.Itoa(chatID)}, } for _, row := range s2tVariables { osExecCommand.Env = append(osExecCommand.Env, fmt.Sprintf("%s=%s", row.name, row.value)) } shellOut, err := osExecCommand.Output() if err != nil { log.Print("exec error: ", err) result = []byte(fmt.Sprintf("exec error: %s", err)) } else { result = shellOut } if cacheTTL > 0 { if err := cache.SetBytes(cacheKey, result, cacheTTL); err != nil { log.Printf("set to cache failed: %s", err) } } return result } // errChain - handle errors on few functions func errChain(chainFuncs ...func() error) error { for _, fn := range chainFuncs { if err := fn(); err != nil { return err } } return nil } // return 2 strings, second="" if string don't contain space func splitStringHalfBySpace(str string) (one, two string) { array := regexp.MustCompile(`\s+`).Split(str, 2) one, two = array[0], "" if len(array) > 1 { two = array[1] } return one, two } // cleanUserName - remove @ from telegram username func cleanUserName(in string) string { return regexp.MustCompile("@").ReplaceAllLiteralString(in, "") } // getRandomCode - generate random code for authorize user func getRandomCode() string { buffer := make([]byte, codeBytesLength) _, err := rand.Read(buffer) if err != nil { log.Fatalf("Get code error: %s", err) } return base64.URLEncoding.EncodeToString(buffer) } // parseBotCommand - parse command-line arguments for one bot command func parseBotCommand(pathRaw, shellCmd string) (path string, command Command, err error) { if len(pathRaw) == 0 || pathRaw[0] != '/' { return "", command, fmt.Errorf("error: path %s don't starts with /", pathRaw) } if stringIsEmpty(shellCmd) { return "", command, fmt.Errorf("error: shell command cannot be empty") } parseAttrFn := func(varsParts []string) (command Command, err error) { for _, oneVar := range varsParts { oneVarParts := regexp.MustCompile("=").Split(oneVar, 2) if len(oneVarParts) == 1 && oneVarParts[0] == "md" { command.isMarkdown = true } else if len(oneVarParts) != 2 { err = fmt.Errorf("error: parse command modificators: %s", oneVar) return } else if oneVarParts[0] == "desc" { command.description = oneVarParts[1] if command.description == "" { err = fmt.Errorf("error: command description cannot be empty") return } } else if oneVarParts[0] == "vars" { command.vars = regexp.MustCompile(",").Split(oneVarParts[1], -1) for _, oneVarName := range command.vars { if oneVarName == "" { err = fmt.Errorf("error: var name cannot be empty") return } } } else { err = fmt.Errorf("error: parse command modificators, not found %s", oneVarParts[0]) return } } return command, nil } pathParts := regexp.MustCompile(":").Split(pathRaw, -1) switch { case len(pathParts) == 1: // /, /cmd path = pathParts[0] case pathParts[0] == "/" && regexp.MustCompile("^(plain_text|image)$").MatchString(pathParts[1]): // /:plain_text, /:image, /:plain_text:desc=name path = "/:" + pathParts[1] if pathParts[1] == "image" { return "", command, fmt.Errorf("/:image not implemented") } if len(pathParts) > 2 { command, err = parseAttrFn(pathParts[2:]) } case len(pathParts) > 1: // commands with modificators :desc, :vars path = pathParts[0] command, err = parseAttrFn(pathParts[1:]) } if err != nil { return "", command, err } command.shellCmd = shellCmd return path, command, nil } // stringIsEmpty - check string is empty func stringIsEmpty(str string) bool { isEmpty, _ := regexp.MatchString(`^\s*$`, str) return isEmpty } // split string by chunks less maxSize size (whole rows) func splitStringLinesBySize(input string, maxSize int) []string { result := []string{} parts := regexp.MustCompile("\n").Split(input, -1) chunks := []string{parts[0]} chunkSize := len(parts[0]) for _, part := range parts[1:] { // current + "\n" + next > maxSize if chunkSize+1+len(part) > maxSize { result = append(result, strings.Join(chunks, "\n")) chunks = []string{part} chunkSize = len(part) } else { chunks = append(chunks, part) chunkSize += 1 + len(part) } } if len(chunks) > 0 { result = append(result, strings.Join(chunks, "\n")) } return result } // create dir if it is not exists func createDirIfNeed(dir string) { if _, err := os.Stat(dir); err != nil { err = os.MkdirAll(dir, 0700) if err != nil { log.Fatal("create dir error:", dir) } } } // get home dir func getOsUserHomeDir() string { homeDir := os.Getenv("HOME") if runtime.GOOS == "windows" { homeDir = os.Getenv("APPDATA") } return homeDir } // read default or user db file name func getDBFilePath(usersDBFile string, needCreateDir bool) (fileName string) { if usersDBFile == "" { dirName := getOsUserHomeDir() + string(os.PathSeparator) + ".config" if needCreateDir { createDirIfNeed(dirName) } fileName = dirName + string(os.PathSeparator) + DBFileName } else { fileName = usersDBFile } return fileName } // ------------------------------------------------------------------ // getShellAndParams - get default shell and command func getShellAndParams(cmd string, customShell string, isWindows bool) (shell string, params []string, err error) { shell, params = "sh", []string{"-c", cmd} if isWindows { shell, params = "cmd", []string{"/C", cmd} } // custom shell switch { case customShell != "sh" && customShell != "": shell = customShell case customShell == "": cmdLine, err := shellwords.Parse(cmd) if err != nil { return shell, params, fmt.Errorf("failed to parse %q: %s", cmd, err) } shell, params = cmdLine[0], cmdLine[1:] } return shell, params, nil } // ------------------------------ type urlValue struct { URL *url.URL } func (v urlValue) String() string { if v.URL != nil { return v.URL.String() } return "" } func (v urlValue) Set(s string) error { u, err := url.Parse(s) if err != nil { return err } else if u.Scheme == "" || u.Host == "" { return fmt.Errorf("missing host or scheme in '%s'", s) } *v.URL = *u return nil } ================================================ FILE: utils_test.go ================================================ package main import ( "fmt" "net/url" "os" "reflect" "testing" ) func Test_splitStringHalfBySpace(t *testing.T) { data := []struct { in string outOne, outTwo string }{ { "/cmd args", "/cmd", "args", }, { "/cmd args", "/cmd", "args", }, { "/cmd", "/cmd", "", }, { "plain text", "plain", "text", }, { "plain text", "plain", "text", }, { "", "", "", }, } for _, item := range data { one, two := splitStringHalfBySpace(item.in) if !(one == item.outOne && two == item.outTwo) { t.Errorf("Failing for \"%s\"\nexpected: (%#v, %#v)\nreal: (%#v, %#v)\n", item.in, item.outOne, item.outTwo, one, two) } } } func Test_cleanUserName(t *testing.T) { data := []struct { in string out string }{ { "1234", "1234", }, { "name", "name", }, { "@name", "name", }, { " name@str ", " namestr ", }, { "", "", }, } for _, item := range data { out := cleanUserName(item.in) if out != item.out { t.Errorf("Failing for \"%s\"\nexpected: %s, real: %s\n", item.in, item.out, out) } } } func Test_parseBotCommand(t *testing.T) { data := []struct { // in pathRaw, shellCmd string // out path string command Command errFunc error }{ { pathRaw: "/cmd", shellCmd: "ls", // out path: "/cmd", command: Command{ shellCmd: "ls", description: "", vars: nil, isMarkdown: false, }, errFunc: nil, }, { pathRaw: "/", shellCmd: "ls", // out path: "/", command: Command{ shellCmd: "ls", description: "", vars: nil, isMarkdown: false, }, errFunc: nil, }, // empty shell command { pathRaw: "/cmd", shellCmd: "", // out path: "", command: Command{ shellCmd: "", description: "", vars: nil, isMarkdown: false, }, errFunc: fmt.Errorf("error"), }, { pathRaw: "/cmd:vars=VAR1,VAR2:desc=Command name", shellCmd: "ls", // out path: "/cmd", command: Command{ shellCmd: "ls", description: "Command name", vars: []string{"VAR1", "VAR2"}, isMarkdown: false, }, errFunc: nil, }, { // markdown test pathRaw: "/cmd:vars=VAR1,VAR2:desc=Command name:md", shellCmd: "ls", // out path: "/cmd", command: Command{ shellCmd: "ls", description: "Command name", vars: []string{"VAR1", "VAR2"}, isMarkdown: true, }, errFunc: nil, }, { pathRaw: "/:plain_text", shellCmd: "ls", // out path: "/:plain_text", command: Command{ shellCmd: "ls", description: "", vars: nil, isMarkdown: false, }, errFunc: nil, }, { pathRaw: "/:image", shellCmd: "ls", // out path: "", command: Command{ shellCmd: "", description: "", vars: nil, isMarkdown: false, }, errFunc: fmt.Errorf("/:image not implemented"), }, { pathRaw: "/:plain_text:desc=Name", shellCmd: "ls", // out path: "/:plain_text", command: Command{ shellCmd: "ls", description: "Name", vars: nil, isMarkdown: false, }, errFunc: nil, }, } for _, item := range data { path, command, errFunc := parseBotCommand(item.pathRaw, item.shellCmd) commandMust := fmt.Sprintf("%#v", item.command) commandGet := fmt.Sprintf("%#v", command) if path != item.path || ((errFunc == nil) != (item.errFunc == nil) || commandGet != commandMust) { t.Errorf("Failing for %v (path: %s)\nMust: %s\nGot: %#v\n", item, path, commandMust, command) } } invalidPaths := []string{ "", " ", "NotValidPath", " /cmd", "/:aaa", "/cmd:aaa=23", "/cmd:aaa", "/cmd:desc", "/cmd:desc=", "/cmd:vars=,,,,", } for _, path := range invalidPaths { _, _, errFunc := parseBotCommand(path, "ls") if errFunc == nil { t.Errorf("Failing check invalid path for: %s", path) } } } func Test_stringIsEmpty(t *testing.T) { data := []struct { in string out bool }{ { "1234", false, }, { " str ", false, }, { "", true, }, { " ", true, }, { "\n", true, }, { " \ndew", false, }, } for _, item := range data { out := stringIsEmpty(item.in) if out != item.out { t.Errorf("Failing for %#v\nexpected: %v, real: %v\n", item.in, item.out, out) } } } func Test_splitStringLinesBySize(t *testing.T) { data := []struct { in string maxSize int out []string }{ { "12345", 6, []string{"12345"}, }, { "12345\n67890", 11, []string{"12345\n67890"}, }, { "1234567890\n1234567890", 3, []string{"1234567890", "1234567890"}, }, { "12\n34\n56\n78\n90", 6, []string{"12\n34", "56\n78", "90"}, }, { "12\n34aaaaaaaaaaaaa\n56\n78\n90", 6, []string{"12", "34aaaaaaaaaaaaa", "56\n78", "90"}, }, } for _, item := range data { out := splitStringLinesBySize(item.in, item.maxSize) mustOut := fmt.Sprintf("%#v", item.out) getOut := fmt.Sprintf("%#v", out) if mustOut != getOut { t.Errorf("Failing for %#v (by %d)\nexpected: %s, real: %s\n", item.in, item.maxSize, mustOut, getOut) } } } func Test_getRandomCode(t *testing.T) { rnd := getRandomCode() if len(rnd) == 0 { t.Errorf("getRandomCode() failed") } } func Test_getOsUserHomeDir(t *testing.T) { userDir := getOsUserHomeDir() if len(userDir) == 0 { t.Errorf("1. getOsUserHomeDir() failed") } _, err := os.Stat(userDir) if err != nil { t.Errorf("2. getOsUserHomeDir() failed") } } func Test_errChain(t *testing.T) { err := errChain() if err != nil { t.Errorf("1. errChain() empty failed") } err = errChain(func() error { return nil }) if err != nil { t.Errorf("2. errChain() failed") } err = errChain(func() error { return nil }, func() error { return nil }) if err != nil { t.Errorf("3. errChain() failed") } err = errChain(func() error { return fmt.Errorf("error") }) if err == nil { t.Errorf("4. errChain() failed") } err = errChain(func() error { return nil }, func() error { return fmt.Errorf("error") }) if err == nil { t.Errorf("5. errChain() failed") } var1 := false err = errChain(func() error { return fmt.Errorf("error") }, func() error { var1 = true; return nil }) if err == nil || var1 { t.Errorf("6. errChain() failed") } } func Test_getShellAndParams(t *testing.T) { shell, params, err := getShellAndParams("ls", "sh", false) if shell != "sh" || !reflect.DeepEqual(params, []string{"-c", "ls"}) || err != nil { t.Errorf("1. getShellAndParams() failed") } shell, params, err = getShellAndParams("ls", "bash", false) if shell != "bash" || !reflect.DeepEqual(params, []string{"-c", "ls"}) || err != nil { t.Errorf("3. getShellAndParams() failed") } shell, params, err = getShellAndParams("ls -l -a", "", false) if shell != "ls" || !reflect.DeepEqual(params, []string{"-l", "-a"}) || err != nil { t.Errorf("4. getShellAndParams() failed") } shell, params, err = getShellAndParams("ls -l 'a b'", "", false) if shell != "ls" || !reflect.DeepEqual(params, []string{"-l", "a b"}) || err != nil { t.Errorf("5. getShellAndParams() failed") } _, _, err = getShellAndParams("ls '-l", "", false) if err == nil { t.Errorf("6. getShellAndParams() failed") } } func Test_flagURL(t *testing.T) { data := []struct { in string err string }{ {"http://example.com", ""}, {"https://bot.example.com/path/to/bot", ""}, {"https://", "missing host or scheme in 'https://'"}, {"localhost", "missing host or scheme in 'localhost'"}, } u := urlValue{&url.URL{}} for _, item := range data { var errStr string err := u.Set(item.in) if err != nil { errStr = err.Error() } if errStr != item.err { t.Errorf("Failing for \"%s\"\nexpected: (%#v)\nreal: (%#v)\n", item.in, item.err, err) } } }