├── .gitignore ├── LICENSE ├── README.md ├── cSpell.json ├── cmd └── botatobot │ └── main.go ├── go.mod ├── go.sum ├── justfile └── pkg ├── env.go ├── generator.go ├── handlers.go └── health.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | output/ 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # secrets 19 | .env 20 | 21 | # logs 22 | logs/ 23 | 24 | # editors 25 | .vscode/ 26 | __debug_bin 27 | 28 | #builds 29 | build/ 30 | 31 | .ROADMAP.md 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Samuel Cristobal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Botatobot 🥔 2 | 3 | Yet another Telegram bot, this one generates stable diffusion images on request. 4 | 5 | ![sample image](https://user-images.githubusercontent.com/9478529/216794269-bedc1fa7-3a46-41aa-8ecd-c31175544d44.jpg) 6 | 7 | ## Yes, but how? 8 | 9 | In a nutshell, Botatobot listen for users requests for images and push them in a (bounded) worker queue, the queue gets processed by a server running a Stable Diffusion inside a either locally or as a service, like replicate.com 10 | 11 | ## Pre-requisites 12 | 13 | To run Stable Diffusion, you have two options: 14 | 15 | - Locally, using a [Cog Container](https://github.com/replicate/cog). 16 | - Remotely, using [Replicate.com](https://www.replicate.com). 17 | 18 | To run the Telegram bot server you need [Go](https://go.dev/doc/install) and a [Telegram](https://www.telegram.com) account. 19 | 20 | ### Host a Cog Stable Diffusion server on your machine 🐳 21 | 22 | First you need to [install Cog](https://github.com/replicate/cog?tab=readme-ov-file#install), [Docker](https://docs.docker.com/get-docker/) and the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html). 23 | 24 | Then clone the pre-configured [cog-stable-diffusion](https://github.com/replicate/cog-stable-diffusion) repository. Follow their instructions in the README to ensure the model is running correctly. 25 | 26 | In particular you need to download the weights first 27 | 28 | ```bash 29 | cog run script/download-weights 30 | ``` 31 | 32 | build your container with 33 | 34 | ```bash 35 | cog build -t stable-diffusion 36 | ``` 37 | 38 | and run it with 39 | 40 | ```bash 41 | docker run -d -p 5001:5000 --gpus all stable-diffusion 42 | ``` 43 | 44 | This will download, create and run a container with the stable diffusion image running on port 5001, eg.`http://127.0.0.1:5001/predictions` 45 | 46 | ### Getting a Replicate.com token and model version 47 | 48 | Go to [Replicate.com api-tokens](https://replicate.com/signin?next=/account/api-tokens) and generate your token, keep it safe. 49 | 50 | You will also need to choose a Stable Diffusion model version. You can find the available versions here 51 | 52 | ### Getting a Telegram bot token 🤖 53 | 54 | 1. Talk to [@BotFather](https://t.me/BotFather) on Telegram 55 | 2. Send `/newbot` to create a new bot 56 | 3. Follow the instructions to set a name and username for your bot 57 | 4. Copy the token that BotFather gives you 58 | 59 | The token looks like this: `123456789:ABCDEF1234567890ABCDEF1234567890ABC` 60 | 61 | ### Configure and run Botatobot 🥔 62 | 63 | #### Configure 64 | 65 | ##### If running Stable Diffusion locally 66 | 67 | Set the following environment variables or create a `.env` file with the following content: 68 | 69 | ```text 70 | TELEGRAMBOT_TOKEN=123456789:ABCDEF1234567890ABCDEF1234567890ABC 71 | MODEL_URL=http://127.0.0.1:5001/predictions 72 | ``` 73 | 74 | The `BOT_TOKEN` you got from the [@BotFather](https://t.me/BotFather), and `MODEL_URL` indicates where the Cog Stable Diffusion is running, most likely a docker container in your local machine. 75 | 76 | There is an optional variable `OUTPUT_PATH` that indicates the path where the generated images will be saved. 77 | 78 | ##### If using replicate.com 79 | 80 | You need the Telegram and Replicate tokens and the Stable Diffusion model version, create an `.env` file or make them available in the environment. 81 | 82 | ```text 83 | BOT_TOKEN=123456789:ABCDEF1234567890ABCDEF1234567890ABC 84 | REPLICATE_TOKEN=1234567890abdfeghijklmnopqrstuvwxyz 85 | REPLICATE_VERSION=a9758cbfbd5f3c2094457d996681af52552901775aa2d6dd0b17fd15df959bef 86 | ``` 87 | 88 | The `BOT_TOKEN` you got from the [@BotFather](https://t.me/BotFather),`REPLICATE_TOKEN` and `REPLICATE_VERSION` you get from replicate.com. 89 | 90 | Additionally you can set `REPLICATE_URL` to a custom url, and `OUTPUT_PATH` to indicate the path where the generated images will be saved. 91 | 92 | #### Build and run 93 | 94 | Build with`go build -o build/botatobot cmd/botatobot/main.go` and then run `./build/botatobot` 95 | 96 | ## Usage 97 | 98 | Tell the bot `/help` to let him self explain. 99 | 100 | ## Notes 101 | 102 | In some scenarios, like deploying to Heroku or other platforms you need a http rest health endpoint. Botatobot includes such functionality, to activate it include a `LOCAL_PORT` variable. 103 | 104 | ## Acknowledgments 105 | 106 | Botatobot uses the excellent [go-telegram](https://pkg.go.dev/github.com/go-telegram/bot) package to interact with the Telegram API. Go check it out! 107 | -------------------------------------------------------------------------------- /cSpell.json: -------------------------------------------------------------------------------- 1 | { 2 | "dictionaries": [], 3 | "dictionaryDefinitions": [], 4 | "ignorePaths": [], 5 | "ignoreWords": [], 6 | "import": [ 7 | "@cspell/dict-python/cspell-ext.json" 8 | ], 9 | "version": "0.2", 10 | "words": [ 11 | "botato", 12 | "Botatobot", 13 | "Botatoide", 14 | "botfather", 15 | "godotenv", 16 | "joho", 17 | "newbot", 18 | "showme", 19 | "telegrambot" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /cmd/botatobot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "scristobal/botatobot/pkg" 9 | 10 | "github.com/go-telegram/bot" 11 | ) 12 | 13 | func main() { 14 | 15 | if err := pkg.FromEnv(); err != nil { 16 | log.Fatalf("Error loading configuration: %v", err) 17 | } 18 | 19 | botato, err := bot.New(pkg.TELEGRAMBOT_TOKEN) 20 | 21 | if err != nil { 22 | log.Fatalf("Error creating bot: %v", err) 23 | } 24 | 25 | botato.RegisterHandler(bot.HandlerTypeMessageText, string(pkg.GenerateCommand), bot.MatchTypePrefix, pkg.GenerateHandler) 26 | botato.RegisterHandler(bot.HandlerTypeMessageText, string(pkg.HelpCommand), bot.MatchTypePrefix, pkg.HelpHandler) 27 | 28 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 29 | defer cancel() 30 | 31 | generator := pkg.NewImageGenerator() 32 | ctx = context.WithValue(ctx, pkg.ImageGeneratorKey, generator) 33 | 34 | go botato.Start(ctx) 35 | 36 | log.Println("Bot online, listening to messages...") 37 | 38 | if pkg.LOCAL_PORT != "" { 39 | log.Printf("Starting health check server on port %s\n", pkg.LOCAL_PORT) 40 | go pkg.StartHealthCheckServer() 41 | } 42 | 43 | <-ctx.Done() 44 | } 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module scristobal/botatobot 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/go-telegram/bot v0.8.2 7 | github.com/google/uuid v1.4.0 8 | github.com/joho/godotenv v1.5.1 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-telegram/bot v0.8.2 h1:5EeOHM6p4H1X1IyXB0uaqw1tnWqLCW8StJAwqfQh+UU= 2 | github.com/go-telegram/bot v0.8.2/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= 3 | github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 4 | github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 6 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 7 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o build/botatobot cmd/botatobot/main.go 3 | run: build 4 | ./build/botatobot -------------------------------------------------------------------------------- /pkg/env.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/joho/godotenv" 9 | ) 10 | 11 | var ( 12 | LOCAL_PORT string 13 | TELEGRAMBOT_TOKEN string 14 | MODEL_URL string 15 | OUTPUT_PATH string 16 | REPLICATE_URL string 17 | REPLICATE_TOKEN string 18 | REPLICATE_VERSION string 19 | ) 20 | 21 | func FromEnv() error { 22 | err := godotenv.Load() 23 | 24 | var ok bool 25 | 26 | if err != nil { 27 | log.Println("No .env file found, using environment variables instead.") 28 | } 29 | 30 | LOCAL_PORT, ok = os.LookupEnv("LOCAL_PORT") 31 | 32 | if !ok { 33 | log.Println("LOCAL_PORT not found, health http rest will not start.") 34 | } 35 | 36 | TELEGRAMBOT_TOKEN, ok = os.LookupEnv("BOT_TOKEN") 37 | 38 | if !ok { 39 | return fmt.Errorf("BOT_TOKEN not found. Talk to @botfather in Telegram and get one") 40 | } 41 | 42 | OUTPUT_PATH, ok = os.LookupEnv("OUTPUT_PATH") 43 | 44 | if !ok { 45 | log.Println("OUTPUT_PATH not found, files will not be saved locally.") 46 | } 47 | 48 | MODEL_URL, ok = os.LookupEnv("MODEL_URL") 49 | 50 | if ok { 51 | return nil 52 | } 53 | 54 | log.Println("MODEL_URL not found, loading replicate.com config.") 55 | 56 | REPLICATE_URL, ok = os.LookupEnv("REPLICATE_URL") 57 | 58 | if !ok { 59 | REPLICATE_URL = "https://api.replicate.com/v1/predictions" 60 | log.Printf("REPLICATE_URL not found, using default %s\n", REPLICATE_URL) 61 | 62 | } 63 | 64 | REPLICATE_TOKEN, ok = os.LookupEnv("REPLICATE_TOKEN") 65 | 66 | if !ok { 67 | return fmt.Errorf("REPLICATE_TOKEN not found, get a token or set RUN_LOCAL=true and use your own image generator") 68 | } 69 | 70 | REPLICATE_VERSION, ok = os.LookupEnv("REPLICATE_VERSION") 71 | 72 | if !ok { 73 | return fmt.Errorf("REPLICATE_VERSION not found, go to replicate.com and choose a model version") 74 | } 75 | 76 | return nil 77 | 78 | } 79 | -------------------------------------------------------------------------------- /pkg/generator.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "net/http" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type imageGeneratorKeyTypeWrapper string 15 | 16 | const ( 17 | ImageGeneratorKey = imageGeneratorKeyTypeWrapper("replicate") 18 | ) 19 | 20 | type imageGenerator struct { 21 | client *http.Client 22 | } 23 | 24 | func NewImageGenerator() *imageGenerator { 25 | return &imageGenerator{ 26 | client: &http.Client{}, 27 | } 28 | } 29 | 30 | func (g *imageGenerator) GenerateImageFromPrompt(prompt string) ([]byte, error) { 31 | 32 | input := modelInput{ 33 | Prompt: prompt, 34 | Seed: rand.Intn(1_000_00), 35 | Num_inference_steps: 50, 36 | Guidance_scale: 7.5, 37 | } 38 | 39 | if MODEL_URL == "" { 40 | return g.generateRemote(input) 41 | } else { 42 | return g.generateLocal(input) 43 | } 44 | 45 | } 46 | 47 | type modelInput struct { 48 | Prompt string `json:"prompt"` 49 | Seed int `json:"seed,omitempty"` 50 | Num_inference_steps int `json:"num_inference_steps,omitempty"` 51 | Guidance_scale float32 `json:"guidance_scale,omitempty"` 52 | } 53 | 54 | type postResponse struct { 55 | Urls struct { 56 | Get string `json:"get"` 57 | } `json:"urls"` 58 | } 59 | 60 | type getResponse struct { 61 | Output []string `json:"output"` 62 | Error string `json:"error"` 63 | } 64 | 65 | func (g *imageGenerator) generateLocal(modelInput modelInput) ([]byte, error) { 66 | 67 | input, err := json.Marshal(modelInput) 68 | 69 | if err != nil { 70 | return []byte{}, fmt.Errorf("fail to serialize job parameters: %v", err) 71 | } 72 | 73 | res, err := http.Post(MODEL_URL, "application/json", strings.NewReader(fmt.Sprintf(`{"input": %s}`, input))) 74 | 75 | if err != nil { 76 | return []byte{}, fmt.Errorf("failed to run the model: %s", err) 77 | 78 | } 79 | 80 | defer res.Body.Close() 81 | 82 | body, err := io.ReadAll(res.Body) 83 | 84 | if err != nil { 85 | return []byte{}, fmt.Errorf("can't read model response: %s", err) 86 | 87 | } 88 | 89 | type apiResponse struct { 90 | Status string `json:"status"` 91 | Output []string `json:"output"` // (base64) data URLs 92 | } 93 | 94 | response := apiResponse{} 95 | 96 | json.Unmarshal(body, &response) 97 | 98 | var output string 99 | if len(response.Output) > 0 { 100 | output = response.Output[0] 101 | 102 | // remove the data URL prefix 103 | data := strings.SplitAfter(output, ",")[1] 104 | 105 | decoded, err := base64.StdEncoding.DecodeString(data) 106 | 107 | if err != nil { 108 | return []byte{}, fmt.Errorf("can't decode model response: %s", err) 109 | 110 | } 111 | 112 | return decoded, nil 113 | } else { 114 | return []byte{}, fmt.Errorf("no output in model response") 115 | } 116 | 117 | } 118 | 119 | func (g *imageGenerator) generateRemote(modelInput modelInput) ([]byte, error) { 120 | 121 | // 1st request to launch job 122 | response, err := g.postJob(modelInput) 123 | 124 | if err != nil { 125 | // TODO: maybe the api needs more time, try again later 126 | return []byte{}, fmt.Errorf("failed to post job: %s", err) 127 | } 128 | 129 | // 2nd request to get job result, eg. output urls 130 | get_url := response.Urls.Get 131 | 132 | output_urls, err := g.getResponse(get_url) 133 | 134 | if err != nil { 135 | // TODO: maybe the api needs more time, try again later 136 | return []byte{}, fmt.Errorf("failed to get job response: %s", err) 137 | } 138 | 139 | // 3rd request to get image(s) 140 | 141 | // TODO: add support for multiple images 142 | output_url := output_urls.Output[0] 143 | 144 | data, err := g.getImageData(output_url) 145 | 146 | if err != nil { 147 | // TODO: maybe the api needs more time, try again later 148 | return []byte{}, fmt.Errorf("failed to get image data: %s", err) 149 | } 150 | 151 | return data, nil 152 | } 153 | 154 | func (g *imageGenerator) postJob(modelInput modelInput) (postResponse, error) { 155 | 156 | input, err := json.Marshal(modelInput) 157 | 158 | if err != nil { 159 | return postResponse{}, fmt.Errorf("fail to serialize job parameters: %v", err) 160 | } 161 | 162 | reqBody := strings.NewReader(fmt.Sprintf(`{"version": "%s", "input": %s}`, REPLICATE_VERSION, input)) 163 | 164 | req, err := http.NewRequest("POST", REPLICATE_URL, reqBody) 165 | 166 | if err != nil { 167 | return postResponse{}, fmt.Errorf("fail to create request: %v", err) 168 | } 169 | 170 | req.Header.Add("Content-Type", "application/json") 171 | 172 | req.Header.Add("Authorization", fmt.Sprintf("Token %s", REPLICATE_TOKEN)) 173 | 174 | res, err := g.client.Do(req) 175 | 176 | if err != nil { 177 | return postResponse{}, fmt.Errorf("failed to run the model: %s", err) 178 | } 179 | 180 | defer res.Body.Close() 181 | 182 | body, err := io.ReadAll(res.Body) 183 | 184 | if err != nil { 185 | return postResponse{}, fmt.Errorf("can't read model response: %s", err) 186 | } 187 | 188 | var response postResponse 189 | 190 | json.Unmarshal(body, &response) 191 | 192 | if response.Urls.Get == "" { 193 | return postResponse{}, fmt.Errorf("can't decode model response: %s", err) 194 | } 195 | 196 | return response, nil 197 | } 198 | 199 | func (g *imageGenerator) getResponse(url string) (getResponse, error) { 200 | 201 | req, err := http.NewRequest("GET", url, nil) 202 | 203 | if err != nil { 204 | return getResponse{}, fmt.Errorf("fail to create request: %v", err) 205 | } 206 | 207 | req.Header.Add("Authorization", fmt.Sprintf("Token %s", REPLICATE_TOKEN)) 208 | 209 | time.Sleep(5 * time.Second) 210 | 211 | res, err := g.client.Do(req) 212 | 213 | if err != nil { 214 | return getResponse{}, fmt.Errorf("failed to run the model: %s", err) 215 | } 216 | 217 | defer res.Body.Close() 218 | 219 | body, err := io.ReadAll(res.Body) 220 | 221 | if err != nil { 222 | return getResponse{}, fmt.Errorf("can't read model response: %s", err) 223 | } 224 | 225 | var resp getResponse 226 | 227 | json.Unmarshal(body, &resp) 228 | 229 | if resp.Error != "" { 230 | return getResponse{}, fmt.Errorf("problem running the model: %s", resp.Error) 231 | } 232 | 233 | if len(resp.Output) == -1 { 234 | return getResponse{}, fmt.Errorf("no output in response") 235 | } 236 | 237 | if len(resp.Output) == 0 { 238 | return getResponse{}, fmt.Errorf("empty output in response") 239 | } 240 | 241 | return resp, nil 242 | 243 | } 244 | 245 | func (g *imageGenerator) getImageData(url string) ([]byte, error) { 246 | 247 | req, err := http.NewRequest("GET", url, nil) 248 | 249 | if err != nil { 250 | return []byte{}, fmt.Errorf("fail to create request: %v", err) 251 | } 252 | 253 | req.Header.Add("Authorization", fmt.Sprintf("Token %s", REPLICATE_TOKEN)) 254 | 255 | res, err := g.client.Do(req) 256 | 257 | if err != nil { 258 | return []byte{}, fmt.Errorf("failed to run the model: %s", err) 259 | } 260 | 261 | defer res.Body.Close() 262 | 263 | body, err := io.ReadAll(res.Body) 264 | 265 | if err != nil { 266 | return []byte{}, fmt.Errorf("can't read model response: %s", err) 267 | } 268 | 269 | return body, nil 270 | } 271 | -------------------------------------------------------------------------------- /pkg/handlers.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "log" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/go-telegram/bot" 12 | "github.com/go-telegram/bot/models" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | type command string 17 | 18 | const ( 19 | HelpCommand command = "/help" 20 | GenerateCommand command = "/generate" 21 | ) 22 | 23 | func GenerateHandler(ctx context.Context, b *bot.Bot, update *models.Update) { 24 | 25 | if update == nil { 26 | log.Println("got an empty update, skipping") 27 | return 28 | } 29 | 30 | message := update.Message 31 | 32 | if message == nil { 33 | log.Printf("got no message from non-empty update, skipping") 34 | return 35 | } 36 | 37 | // prompt is message.Text after removing the command 38 | prompt := message.Text[(len(GenerateCommand) + 1):] 39 | prompt = strings.TrimSpace(prompt) 40 | 41 | // TODO: sanitize prompt 42 | 43 | log.Printf("User `%s` requested `%s`\n", message.Chat.Username, prompt) 44 | 45 | generator := ctx.Value(ImageGeneratorKey).(*imageGenerator) 46 | 47 | output, err := generator.GenerateImageFromPrompt(prompt) 48 | 49 | if err != nil { 50 | 51 | _, err := b.SendMessage(ctx, &bot.SendMessageParams{ 52 | ChatID: message.Chat.ID, 53 | Text: fmt.Sprintf("Sorry, but something went wrong 😭 %s", err), 54 | ReplyToMessageID: message.ID, 55 | }) 56 | 57 | if err != nil { 58 | log.Printf("Error sending message: %s", err) 59 | } 60 | return 61 | } 62 | 63 | _, err = b.SendPhoto(ctx, &bot.SendPhotoParams{ 64 | ChatID: message.Chat.ID, 65 | Caption: fmt.Sprint(prompt), 66 | Photo: &models.InputFileUpload{ 67 | Data: bytes.NewReader(output), 68 | Filename: filepath.Base(fmt.Sprintf("%s.png", uuid.New())), 69 | }, 70 | DisableNotification: true, 71 | }) 72 | 73 | if err != nil { 74 | log.Printf("Error sending photo: %s", err) 75 | } 76 | } 77 | 78 | func HelpHandler(ctx context.Context, b *bot.Bot, update *models.Update) { 79 | 80 | if update == nil { 81 | log.Println("empty update") 82 | return 83 | } 84 | 85 | message := update.Message 86 | 87 | if message == nil { 88 | log.Println("empty message") 89 | return 90 | } 91 | 92 | b.SendMessage(ctx, &bot.SendMessageParams{ 93 | ChatID: message.Chat.ID, 94 | Text: "Hi! I'm a 🤖 that generates images from text. Use the /generate command follow by a prompt, like this: \n\n /generate a cat in space \n\nBy default I will generate 5 images, but you can modify the seed, guidance and steps like so\n\n /generate a cat in space &seed_1234 &steps_50 &guidance_7.5\n\nCheck my status with /status\n\nHave fun!", 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/health.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | func httpHealthServer(handler func(w http.ResponseWriter, r *http.Request)) { 10 | 11 | s := http.Server{ 12 | Addr: fmt.Sprintf(":%s", LOCAL_PORT), 13 | ReadTimeout: 30 * time.Second, 14 | WriteTimeout: 90 * time.Second, 15 | IdleTimeout: 120 * time.Second, 16 | } 17 | 18 | mux := http.NewServeMux() 19 | 20 | mux.HandleFunc("/", handler) 21 | 22 | s.Handler = mux 23 | 24 | err := s.ListenAndServe() 25 | 26 | if err != nil { 27 | if err != http.ErrServerClosed { 28 | panic(err) 29 | } 30 | } 31 | } 32 | 33 | func StartHealthCheckServer() { 34 | httpHealthServer(func(w http.ResponseWriter, r *http.Request) { 35 | w.WriteHeader(http.StatusOK) 36 | w.Write([]byte("OK")) 37 | }) 38 | } 39 | --------------------------------------------------------------------------------