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: |
[](https://github.com/msoap/shell2telegram/releases/latest) [](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
-----------------------------------------------------------------------------------------------------------------------------------------
[](https://github.com/msoap/shell2telegram/actions/workflows/go.yml)
[](https://coveralls.io/github/msoap/shell2telegram?branch=master)
[](https://hub.docker.com/r/msoap/shell2telegram/)
[](https://github.com/msoap/shell2telegram#install)
[](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)
}
}
}
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
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 [\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.