├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── bin └── bot ├── bot.service ├── cmd ├── main.go └── main_test.go ├── go.mod ├── go.sum └── internal ├── app ├── clear_text.go ├── run.go └── ticker.go ├── config ├── environment.go └── environment_test.go └── data ├── send_messages.go └── user_data.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "dev" ] 6 | pull_request: 7 | branches: [ "dev" ] 8 | 9 | jobs: 10 | 11 | audit: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: 1.19 20 | 21 | - name: Verify dependencies 22 | run: go mod verify 23 | 24 | - name: Run go vet 25 | run: go vet ./cmd/... 26 | 27 | - name: Install staticcheck 28 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 29 | 30 | - name: Run staticcheck 31 | run: staticcheck ./cmd/... 32 | 33 | - name: Install golint 34 | run: go install golang.org/x/lint/golint@latest 35 | 36 | - name: Run golint 37 | run: golint ./cmd/... 38 | 39 | - name: Test 40 | run: go test -v ./cmd/... 41 | 42 | 43 | deploy: 44 | runs-on: ubuntu-20.04 45 | needs: audit 46 | steps: 47 | - uses: actions/checkout@v3 48 | 49 | - name: Set up Go 50 | uses: actions/setup-go@v4 51 | with: 52 | go-version: 1.19 53 | 54 | - name: Build 55 | run: CGO_ENABLED=0 GOOS=linux go build -o ./bin/bot -ldflags '-w -s' ./cmd/... 56 | 57 | - name: Deploy 58 | uses: webfactory/ssh-agent@v0.5.0 59 | with: 60 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 61 | 62 | - name: Run Command 63 | run: | 64 | ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null ${{secrets.USER_NAME}}@${{secrets.SSH_HOST}} ' 65 | cd ${{ secrets.HOME }} && 66 | git checkout dev && 67 | git fetch --all && 68 | git merge dev && 69 | sudo systemctl restart bot 70 | ' 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .github/ 3 | cmd/ 4 | internal/ 5 | .gitignore 6 | bot.service 7 | Dockerfile 8 | go.mod 9 | go.sum 10 | Makefile 11 | README.md 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine3.14 AS builder 2 | 3 | WORKDIR /go/src/kickHisAss 4 | 5 | COPY . . 6 | 7 | RUN go build -o /go/src/kickHisAss/bot /go/src/kickHisAss/cmd/main.go 8 | 9 | FROM alpine:3.14 10 | 11 | COPY --from=builder /go/src/kickHisAss/bot / 12 | 13 | ENV AI_TOKEN="XXX" 14 | ENV BOT_TOKEN="XXXX:XXXX" 15 | ENV CHANNEL="-100XXX" 16 | 17 | EXPOSE 3000 18 | CMD ["/bot"] 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE = bot 2 | DATE ?= $(shell date +%FT%T%z) 3 | VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \ 4 | cat $(CURDIR)/.version 2> /dev/null || echo v0) 5 | PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) 6 | TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' $(PKGS)) 7 | BIN = $(CURDIR)/bin 8 | 9 | GO = go 10 | GODOC = godoc 11 | TIMEOUT = 15 12 | V = 0 13 | Q = $(if $(filter 1,$V),,@) 14 | M = $(shell printf "\033[34;1m▶\033[0m") 15 | 16 | export GO111MODULE=on 17 | 18 | .PHONY: all 19 | all: $(BIN) ; $(info $(M) building executable…) @ ## Build program binary 20 | $Q $(GO) build \ 21 | -tags release \ 22 | -ldflags '-X $(PACKAGE)/cmd.Version=$(VERSION) -X $(PACKAGE)/cmd.BuildDate=$(DATE)' \ 23 | -o $(BIN)/$(PACKAGE) ./cmd/... 24 | 25 | # Tools 26 | 27 | $(BIN): 28 | @mkdir -p $@ 29 | $(BIN)/%: | $(BIN) ; $(info $(M) building $(REPOSITORY)…) 30 | $Q tmp=$$(mktemp -d); \ 31 | env GO111MODULE=off GOCACHE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(REPOSITORY) \ 32 | || ret=$$?; \ 33 | rm -rf $$tmp ; exit $$ret 34 | 35 | # Misc 36 | 37 | .PHONY: clean 38 | clean: ; $(info $(M) cleaning…) @ ## Cleanup everything 39 | @rm -rf $(BIN) 40 | @rm -rf test/tests.* test/coverage.* 41 | 42 | .PHONY: help 43 | help: 44 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 45 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 46 | 47 | .PHONY: version 48 | version: 49 | @echo $(VERSION) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bot for AI 2 | 3 | 4 | ```shell 5 | App for the @GolangHouse channel telegram. 6 | For communication with GPT-3.5 Turbo model and users. 7 | ``` 8 | 9 | ### RUN 10 | 11 | ```shell 12 | $ export AI_TOKEN="XXX" 13 | $ export BOT_TOKEN="XXX:XXX" 14 | $ export CHANNEL="-100XXX" 15 | 16 | go run ./cmd/... 17 | ``` -------------------------------------------------------------------------------- /bin/bot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarlikAlmighty/askAI/fd489d705712a5fbeaffb153b37323b3ab31f5e4/bin/bot -------------------------------------------------------------------------------- /bot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Bot 3 | After=network.target 4 | 5 | [Service] 6 | Environment=AI_TOKEN="XXX" 7 | Environment=BOT_TOKEN="XXX:XXX" 8 | Environment=CHANNEL="-100XXX" 9 | Type=simple 10 | PIDFile=/home/ubuntu/bot/bot.pid 11 | WorkingDirectory=/home/ubuntu/bot 12 | ExecStart=/home/ubuntu/bot/bin/bot 13 | ExecStop=/bin/kill -INT $MAINPID 14 | User=ubuntu 15 | Group=ubuntu 16 | Restart=always 17 | StandardOutput=journal 18 | StandardError=journal 19 | 20 | [Install] 21 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/MarlikAlmighty/kickHisAss/internal/app" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | if err := app.Run(); err != nil { 10 | log.Fatalf("[Fatal]: start bot %v\n", err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cmd/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/MarlikAlmighty/kickHisAss/internal/app" 5 | "testing" 6 | ) 7 | 8 | func Test_Main(t *testing.T) { 9 | if err := app.Run(); err == nil { 10 | t.Errorf("Want err != nil, got %v == nil\n", err) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MarlikAlmighty/kickHisAss 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 7 | github.com/kelseyhightower/envconfig v1.4.0 8 | ) 9 | 10 | require github.com/sashabaranov/go-openai v1.9.5 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 2 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 3 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 4 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 5 | github.com/sashabaranov/go-openai v1.9.5 h1:z1VCMXsfnug+U0ceTTIXr/L26AYl9jafqA9lptlSX0c= 6 | github.com/sashabaranov/go-openai v1.9.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 7 | -------------------------------------------------------------------------------- /internal/app/clear_text.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import "regexp" 4 | 5 | // clearText 6 | func clearText(text string, reg *regexp.Regexp) string { 7 | return reg.ReplaceAllString(text, "") 8 | } 9 | -------------------------------------------------------------------------------- /internal/app/run.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "regexp" 5 | "time" 6 | 7 | "github.com/MarlikAlmighty/kickHisAss/internal/data" 8 | 9 | "github.com/MarlikAlmighty/kickHisAss/internal/config" 10 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 11 | "github.com/sashabaranov/go-openai" 12 | ) 13 | 14 | // Run start bot 15 | func Run() error { 16 | 17 | // get env 18 | cfg := config.New() 19 | if err := cfg.GetEnv(); err != nil { 20 | return err 21 | } 22 | 23 | // new where we saved data 24 | users := data.New() 25 | 26 | // client ai 27 | clientAI := openai.NewClient(cfg.AiToken) 28 | 29 | // Start botAPI with token 30 | bot, err := tgbotapi.NewBotAPI(cfg.BotToken) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // for limit request 36 | limiter := time.Tick(time.Minute / 3) 37 | 38 | // regexp for clear text 39 | reg := regexp.MustCompile(`^@ai\s+`) 40 | 41 | // clear data every 10 minutes 42 | go CleanUserData(users) 43 | 44 | u := tgbotapi.NewUpdate(0) 45 | u.Timeout = 60 46 | 47 | updates := bot.GetUpdatesChan(u) 48 | 49 | for update := range updates { 50 | 51 | if update.Message != nil { 52 | 53 | // only my channel from config 54 | if update.Message.Chat.ID != cfg.Channel { 55 | continue 56 | } 57 | 58 | mess := update.Message.Text 59 | 60 | if matched := reg.MatchString(mess); matched { 61 | 62 | <-limiter 63 | 64 | userID := update.Message.Chat.ID 65 | messID := update.Message.MessageID 66 | 67 | mess = clearText(mess, reg) 68 | 69 | if err = users.Send(clientAI, bot, userID, messID, mess); err != nil { 70 | return err 71 | } 72 | } 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /internal/app/ticker.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/MarlikAlmighty/kickHisAss/internal/data" 5 | "time" 6 | ) 7 | 8 | // CleanUserData function clearing map with user data 9 | func CleanUserData(users *data.UserData) { 10 | 11 | ticker := time.NewTicker(10 * time.Minute) 12 | defer ticker.Stop() 13 | 14 | go func() { 15 | for { 16 | select { 17 | case <-ticker.C: 18 | users.Clear() 19 | } 20 | } 21 | }() 22 | } 23 | -------------------------------------------------------------------------------- /internal/config/environment.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/kelseyhightower/envconfig" 5 | ) 6 | 7 | // Config struct 8 | type Config struct { 9 | BotToken string `required:"true" split_words:"true"` 10 | AiToken string `required:"true" split_words:"true"` 11 | Channel int64 `required:"true"` 12 | } 13 | 14 | // New config 15 | func New() *Config { 16 | return &Config{} 17 | } 18 | 19 | // GetEnv configuration init 20 | func (cnf *Config) GetEnv() error { 21 | if err := envconfig.Process("", cnf); err != nil { 22 | return err 23 | } 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/config/environment_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestConfig_GetEnv(t *testing.T) { 9 | 10 | if err := os.Setenv("BOT_TOKEN", "TEST_BOT_TOKEN"); err != nil { 11 | t.Errorf("Error: %v", err) 12 | } 13 | 14 | if err := os.Setenv("AI_TOKEN", "TEST_AI_TOKEN"); err != nil { 15 | t.Errorf("Error: %v", err) 16 | } 17 | 18 | if err := os.Setenv("CHANNEL", "123"); err != nil { 19 | t.Errorf("Error: %v", err) 20 | } 21 | 22 | cfg := New() 23 | if err := cfg.GetEnv(); err != nil { 24 | t.Errorf("Error: %v", err) 25 | } 26 | 27 | type fields struct { 28 | BotToken string 29 | AiToken string 30 | Channel int64 31 | } 32 | 33 | f := fields{ 34 | BotToken: cfg.BotToken, 35 | AiToken: cfg.AiToken, 36 | Channel: cfg.Channel, 37 | } 38 | 39 | tests := []struct { 40 | name string 41 | fields fields 42 | wantErr bool 43 | }{ 44 | {"Configuration", f, false}, 45 | } 46 | 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | cnf := &Config{ 50 | BotToken: tt.fields.BotToken, 51 | AiToken: tt.fields.AiToken, 52 | Channel: tt.fields.Channel, 53 | } 54 | if err := cnf.GetEnv(); (err != nil) != tt.wantErr { 55 | t.Errorf("GetEnv() error = %v, wantErr %v", err, tt.wantErr) 56 | } 57 | }) 58 | } 59 | 60 | if err := os.Unsetenv("BOT_TOKEN"); err != nil { 61 | t.Error(err) 62 | } 63 | 64 | if err := os.Unsetenv("AI_TOKEN"); err != nil { 65 | t.Error(err) 66 | } 67 | 68 | if err := os.Unsetenv("CHANNEL"); err != nil { 69 | t.Error(err) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/data/send_messages.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 8 | "github.com/sashabaranov/go-openai" 9 | ) 10 | 11 | func (data *UserData) Send(client *openai.Client, bot *tgbotapi.BotAPI, userID int64, messID int, mess string) error { 12 | 13 | if len(mess) > 4097 { 14 | 15 | msg := tgbotapi.NewMessage(userID, "this model's maximum context length is 4097 tokens...") 16 | msg.ReplyToMessageID = messID 17 | 18 | if _, err := bot.Send(msg); err != nil { 19 | log.Printf("send message to user error: %v\n", err) 20 | return err 21 | } 22 | 23 | return nil 24 | } 25 | 26 | messages := data.Get(userID) 27 | 28 | if len(mess) > 4097 { 29 | log.Printf("this model's maximum context length is 4097 tokens...") 30 | data.Clear() 31 | } 32 | 33 | messages = append(messages, openai.ChatCompletionMessage{ 34 | Role: openai.ChatMessageRoleUser, 35 | Content: mess, 36 | }) 37 | 38 | var ( 39 | resp openai.ChatCompletionResponse 40 | err error 41 | ) 42 | 43 | if resp, err = client.CreateChatCompletion( 44 | context.Background(), 45 | openai.ChatCompletionRequest{ 46 | Model: openai.GPT3Dot5Turbo, 47 | Messages: messages, 48 | }, 49 | ); err != nil { 50 | log.Printf("chat completion error: %v\n", err) 51 | return err 52 | } 53 | 54 | // -1001285932539 55 | msg := tgbotapi.NewMessage(userID, resp.Choices[0].Message.Content) 56 | msg.ReplyToMessageID = messID 57 | 58 | if _, err = bot.Send(msg); err != nil { 59 | log.Printf("send message to user error: %v\n", err) 60 | return err 61 | } 62 | 63 | messages = append(messages, openai.ChatCompletionMessage{ 64 | Role: openai.ChatMessageRoleAssistant, 65 | Content: resp.Choices[0].Message.Content, 66 | }) 67 | 68 | data.Set(userID, messages) 69 | 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/data/user_data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/sashabaranov/go-openai" 5 | "sync" 6 | ) 7 | 8 | // Pusher for assertion 9 | type Pusher interface { 10 | Set(userID, msg string) 11 | Get(userID int64) []openai.ChatCompletionMessage 12 | Clear() 13 | } 14 | 15 | // UserData save users in memory 16 | type UserData struct { 17 | ID map[int64][]openai.ChatCompletionMessage 18 | mux sync.Mutex 19 | } 20 | 21 | // New simple constructor 22 | func New() *UserData { 23 | return &UserData{ 24 | ID: make(map[int64][]openai.ChatCompletionMessage), 25 | } 26 | } 27 | 28 | // Set added data to map 29 | func (data *UserData) Set(userID int64, msg []openai.ChatCompletionMessage) { 30 | data.mux.Lock() 31 | mp := data.ID[userID] 32 | mp = append(mp, msg...) 33 | data.ID[userID] = mp 34 | data.mux.Unlock() 35 | } 36 | 37 | // Get data from map 38 | func (data *UserData) Get(userID int64) []openai.ChatCompletionMessage { 39 | data.mux.Lock() 40 | mp := data.ID[userID] 41 | data.mux.Unlock() 42 | return mp 43 | } 44 | 45 | // Clear map 46 | func (data *UserData) Clear() { 47 | data.mux.Lock() 48 | if len(data.ID) > 0 { 49 | data.ID = make(map[int64][]openai.ChatCompletionMessage) 50 | } 51 | data.mux.Unlock() 52 | } 53 | --------------------------------------------------------------------------------