Full Code of msoap/shell2telegram for AI

master e836d1a12505 cached
19 files
73.9 KB
23.4k tokens
77 symbols
1 requests
Download .txt
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/\<img.+\>//' > $(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
================================================
<img src="https://raw.githubusercontent.com/msoap/shell2telegram/misc/img/shell2telegram_icon.png" width="32" height="32"> 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=<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

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)
		}
	}
}
Download .txt
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
Download .txt
SYMBOL INDEX (77 symbols across 5 files)

FILE: commands.go
  type Ctx (line 14) | type Ctx struct
  function cmdAuth (line 31) | func cmdAuth(ctx Ctx) (replayMsg string) {
  function cmdHelp (line 66) | func cmdHelp(ctx Ctx) (replayMsg string) {
  function cmdUser (line 119) | func cmdUser(ctx Ctx) {
  function cmdShell2telegramStat (line 147) | func cmdShell2telegramStat(ctx Ctx) (replayMsg string) {
  function cmdShell2telegramSearch (line 156) | func cmdShell2telegramSearch(ctx Ctx) (replayMsg string) {
  function cmdShell2telegramBan (line 171) | func cmdShell2telegramBan(ctx Ctx) (replayMsg string) {
  function cmdShell2telegramDesc (line 190) | func cmdShell2telegramDesc(ctx Ctx) (replayMsg string) {
  function cmdShell2telegramRm (line 204) | func cmdShell2telegramRm(ctx Ctx) (replayMsg string) {
  function cmdShell2telegramVersion (line 221) | func cmdShell2telegramVersion(_ Ctx) (replayMsg string) {
  function cmdShell2telegramExit (line 227) | func cmdShell2telegramExit(ctx Ctx) (replayMsg string) {
  function cmdShell2telegramBroadcastToRoot (line 238) | func cmdShell2telegramBroadcastToRoot(ctx Ctx) (replayMsg string) {
  function cmdShell2telegramMessageToUser (line 255) | func cmdShell2telegramMessageToUser(ctx Ctx) (replayMsg string) {

FILE: shell2telegram.go
  constant version (line 21) | version = "1.10"
  constant DefaultBotTimeout (line 24) | DefaultBotTimeout = 60
  constant MessagesQueueSize (line 27) | MessagesQueueSize = 10
  constant MaxMessageLength (line 30) | MaxMessageLength = 4096
  constant SecondsForAutoSaveUsersToDB (line 33) | SecondsForAutoSaveUsersToDB = 60
  constant DBFileName (line 36) | DBFileName = "shell2telegram.json"
  constant cmdPlainText (line 39) | cmdPlainText = "/:plain_text"
  type Command (line 43) | type Command struct
  type Commands (line 51) | type Commands
  type Config (line 54) | type Config struct
  constant msgIsText (line 76) | msgIsText int8 = iota
  constant msgIsPhoto (line 77) | msgIsPhoto
  type BotMessage (line 81) | type BotMessage struct
  function getConfig (line 92) | func getConfig() (commands Commands, appConfig Config, err error) {
  function sendMessage (line 172) | func sendMessage(messageSignal chan<- BotMessage, chatID int, message []...
  function main (line 226) | func main() {

FILE: users.go
  type User (line 16) | type User struct
  type Users (line 31) | type Users struct
    method AddNew (line 71) | func (users *Users) AddNew(tgbotMessage tgbotapi.Message) {
    method DoLogin (line 102) | func (users *Users) DoLogin(userID int, forRoot bool) string {
    method SetAuthorized (line 117) | func (users *Users) SetAuthorized(userID int, forRoot bool) {
    method IsValidCode (line 128) | func (users Users) IsValidCode(userID int, code string, forRoot bool) ...
    method IsAuthorized (line 139) | func (users Users) IsAuthorized(userID int) bool {
    method IsRoot (line 149) | func (users Users) IsRoot(userID int) bool {
    method BroadcastForRoots (line 159) | func (users Users) BroadcastForRoots(messageSignal chan<- BotMessage, ...
    method String (line 168) | func (users Users) String(userID int) string {
    method StringVerbose (line 177) | func (users Users) StringVerbose(userID int) string {
    method ClearOldUsers (line 191) | func (users *Users) ClearOldUsers() {
    method GetUserIDByName (line 203) | func (users Users) GetUserIDByName(userName string) int {
    method BanUser (line 216) | func (users *Users) BanUser(userID int) bool {
    method Search (line 233) | func (users Users) Search(query string) (result []int) {
    method FindByIDOrUserName (line 251) | func (users Users) FindByIDOrUserName(userName string) int {
    method SendMessageToPrivate (line 266) | func (users Users) SendMessageToPrivate(messageSignal chan<- BotMessag...
    method LoadFromDB (line 275) | func (users *Users) LoadFromDB(usersDBFile string) {
    method SaveToDB (line 298) | func (users *Users) SaveToDB(usersDBFile string) {
  type UsersDB (line 39) | type UsersDB struct
  constant SecondsForOldUsersBeforeVacuum (line 45) | SecondsForOldUsersBeforeVacuum = 1200
  function NewUsers (line 48) | func NewUsers(appConfig Config) Users {

FILE: utils.go
  constant codeBytesLength (line 25) | codeBytesLength = 15
  function execShell (line 28) | func execShell(shellCmd, input string, varsNames []string, userID, chatI...
  function errChain (line 111) | func errChain(chainFuncs ...func() error) error {
  function splitStringHalfBySpace (line 122) | func splitStringHalfBySpace(str string) (one, two string) {
  function cleanUserName (line 133) | func cleanUserName(in string) string {
  function getRandomCode (line 138) | func getRandomCode() string {
  function parseBotCommand (line 149) | func parseBotCommand(pathRaw, shellCmd string) (path string, command Com...
  function stringIsEmpty (line 217) | func stringIsEmpty(str string) bool {
  function splitStringLinesBySize (line 223) | func splitStringLinesBySize(input string, maxSize int) []string {
  function createDirIfNeed (line 248) | func createDirIfNeed(dir string) {
  function getOsUserHomeDir (line 258) | func getOsUserHomeDir() string {
  function getDBFilePath (line 267) | func getDBFilePath(usersDBFile string, needCreateDir bool) (fileName str...
  function getShellAndParams (line 283) | func getShellAndParams(cmd string, customShell string, isWindows bool) (...
  type urlValue (line 306) | type urlValue struct
    method String (line 310) | func (v urlValue) String() string {
    method Set (line 317) | func (v urlValue) Set(s string) error {

FILE: utils_test.go
  function Test_splitStringHalfBySpace (line 11) | func Test_splitStringHalfBySpace(t *testing.T) {
  function Test_cleanUserName (line 45) | func Test_cleanUserName(t *testing.T) {
  function Test_parseBotCommand (line 76) | func Test_parseBotCommand(t *testing.T) {
  function Test_stringIsEmpty (line 223) | func Test_stringIsEmpty(t *testing.T) {
  function Test_splitStringLinesBySize (line 257) | func Test_splitStringLinesBySize(t *testing.T) {
  function Test_getRandomCode (line 296) | func Test_getRandomCode(t *testing.T) {
  function Test_getOsUserHomeDir (line 303) | func Test_getOsUserHomeDir(t *testing.T) {
  function Test_errChain (line 314) | func Test_errChain(t *testing.T) {
  function Test_getShellAndParams (line 347) | func Test_getShellAndParams(t *testing.T) {
  function Test_flagURL (line 374) | func Test_flagURL(t *testing.T) {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (83K chars).
[
  {
    "path": ".github/workflows/docker.yml",
    "chars": 1112,
    "preview": "name: Publish Docker image\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  push_to_registry:\n    name: Push Docker image to"
  },
  {
    "path": ".github/workflows/go.yml",
    "chars": 1010,
    "preview": "name: Go\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n  schedule:\n    - cron: '0 12 * "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 642,
    "preview": "name: goreleaser\n\non:\n  push:\n    tags:\n      - 'v*'\n\npermissions:\n  contents: write\n\njobs:\n  goreleaser:\n    runs-on: u"
  },
  {
    "path": ".gitignore",
    "chars": 20,
    "preview": "tags\nshell2telegram\n"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 1501,
    "preview": "release:\n  name_template: \"{{ .Version }} - {{ .Date }}\"\n  draft: true\n  header: |\n    [![Github Releases ({{ .Tag }})]("
  },
  {
    "path": "Dockerfile",
    "chars": 851,
    "preview": "# docker build -t msoap/shell2telegram .\n\n# build image\nFROM --platform=$BUILDPLATFORM golang:alpine as go_builder\n\nARG "
  },
  {
    "path": "LICENSE",
    "chars": 1081,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Serhii Mudryk\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "Makefile",
    "chars": 1950,
    "preview": "APP_NAME := shell2telegram\nAPP_DESCRIPTION := $$(awk 'NR == 11, NR == 13' README.md)\nAPP_URL := https://github.com/msoap"
  },
  {
    "path": "README.md",
    "chars": 7598,
    "preview": "<img src=\"https://raw.githubusercontent.com/msoap/shell2telegram/misc/img/shell2telegram_icon.png\" width=\"32\" height=\"32"
  },
  {
    "path": "commands.go",
    "chars": 7604,
    "preview": "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 bo"
  },
  {
    "path": "go.mod",
    "chars": 316,
    "preview": "module github.com/msoap/shell2telegram\n\ngo 1.14\n\nrequire (\n\tgithub.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incom"
  },
  {
    "path": "go.sum",
    "chars": 5666,
    "preview": "github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:cl"
  },
  {
    "path": "shell2telegram.1",
    "chars": 7448,
    "preview": ".\\\" generated with Ronn/v0.7.3\n.\\\" http://github.com/rtomayko/ronn/tree/0.7.3\n.\n.TH \"SHELL2TELEGRAM\" \"\" \"January 2022\" \""
  },
  {
    "path": "shell2telegram.go",
    "chars": 12947,
    "preview": "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\"git"
  },
  {
    "path": "snapcraft.yaml",
    "chars": 501,
    "preview": "name: shell2telegram\nversion: '1.10'\nsummary: Telegram bot constructor from command-line\ndescription: |\n  Telegram bot c"
  },
  {
    "path": "test-bot.Dockerfile",
    "chars": 70,
    "preview": "FROM msoap/shell2telegram\n\nENV TB_TOKEN=*******\nCMD [\"/date\", \"date\"]\n"
  },
  {
    "path": "users.go",
    "chars": 9132,
    "preview": "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/te"
  },
  {
    "path": "utils.go",
    "chars": 8397,
    "preview": "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\"re"
  },
  {
    "path": "utils_test.go",
    "chars": 7856,
    "preview": "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) {"
  }
]

About this extraction

This page contains the full source code of the msoap/shell2telegram GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (73.9 KB), approximately 23.4k tokens, and a symbol index with 77 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!